From f0ab80d312f901e3b0da60ec623bcf612ce353e6 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Wed, 2 Oct 2024 13:17:48 -0400 Subject: [PATCH] Convert DirectMessage System to Riverpod --- lib/di_initialization.dart | 10 - lib/main.dart | 6 - lib/models/auth/profile.dart | 10 + lib/models/direct_message.dart | 23 +- lib/models/direct_message_thread.dart | 19 +- .../direct_message_services.dart | 163 +++++++++ .../direct_message_services.g.dart | 344 ++++++++++++++++++ lib/screens/message_thread_screen.dart | 229 ++++++------ .../message_threads_browser_screen.dart | 36 +- lib/screens/messages_new_thread.dart | 32 +- lib/services/direct_message_service.dart | 167 --------- lib/services/notifications_manager.dart | 61 ++-- 12 files changed, 729 insertions(+), 371 deletions(-) create mode 100644 lib/riverpod_controllers/direct_message_services.dart create mode 100644 lib/riverpod_controllers/direct_message_services.g.dart delete mode 100644 lib/services/direct_message_service.dart diff --git a/lib/di_initialization.dart b/lib/di_initialization.dart index fb46d77..f46daa0 100644 --- a/lib/di_initialization.dart +++ b/lib/di_initialization.dart @@ -18,7 +18,6 @@ import 'models/instance_info.dart'; import 'services/auth_service.dart'; import 'services/blocks_manager.dart'; import 'services/connections_manager.dart'; -import 'services/direct_message_service.dart'; import 'services/entry_manager_service.dart'; import 'services/feature_version_checker.dart'; import 'services/fediverse_server_validator.dart'; @@ -128,9 +127,6 @@ Future dependencyInjectionInitialization() async { getIt.registerSingleton>( ActiveProfileSelector((p) => FollowRequestsManager(p)) ..subscribeToProfileSwaps()); - getIt.registerSingleton>( - ActiveProfileSelector((p) => DirectMessageService(p)) - ..subscribeToProfileSwaps()); getIt.registerSingleton>( ActiveProfileSelector((p) => InteractionsManager(p)) ..subscribeToProfileSwaps()); @@ -167,12 +163,6 @@ void clearCaches() { _logger.severe('Error clearing IConnections Repo: $error'), ); - getIt>().activeEntry.match( - onSuccess: (service) => service.clear(), - onError: (error) => - _logger.severe('Error clearing DirectMessageService Repo: $error'), - ); - getIt>().activeEntry.match( onSuccess: (service) => service.clear(), onError: (error) => diff --git a/lib/main.dart b/lib/main.dart index 6ee1f9b..9f40d57 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,7 +16,6 @@ import 'routes.dart'; import 'services/auth_service.dart'; import 'services/blocks_manager.dart'; import 'services/connections_manager.dart'; -import 'services/direct_message_service.dart'; import 'services/entry_manager_service.dart'; import 'services/follow_requests_manager.dart'; import 'services/gallery_service.dart'; @@ -147,11 +146,6 @@ class _AppState extends fr.ConsumerState { create: (_) => getIt>(), ), - ChangeNotifierProvider< - ActiveProfileSelector>( - create: (_) => - getIt>(), - ), ChangeNotifierProvider< ActiveProfileSelector>( create: (_) => diff --git a/lib/models/auth/profile.dart b/lib/models/auth/profile.dart index b300ba4..018a439 100644 --- a/lib/models/auth/profile.dart +++ b/lib/models/auth/profile.dart @@ -1,3 +1,4 @@ +import 'basic_credentials.dart'; import 'credentials_intf.dart'; class Profile { @@ -30,6 +31,15 @@ class Profile { loggedIn: false, ); + factory Profile.empty() => Profile( + credentials: BasicCredentials.empty(), + username: '', + userId: '', + avatar: '', + serverName: '', + loggedIn: false, + ); + factory Profile.fromJson( Map json, ICredentials Function(Map json) credentialsFromJson, diff --git a/lib/models/direct_message.dart b/lib/models/direct_message.dart index e104ecd..ee62eda 100644 --- a/lib/models/direct_message.dart +++ b/lib/models/direct_message.dart @@ -62,8 +62,27 @@ class DirectMessage { identical(this, other) || other is DirectMessage && runtimeType == other.runtimeType && - id == other.id; + id == other.id && + senderId == other.senderId && + senderScreenName == other.senderScreenName && + recipientId == other.recipientId && + recipientScreenName == other.recipientScreenName && + title == other.title && + text == other.text && + createdAt == other.createdAt && + seen == other.seen && + parentUri == other.parentUri; @override - int get hashCode => id.hashCode; + int get hashCode => + id.hashCode ^ + senderId.hashCode ^ + senderScreenName.hashCode ^ + recipientId.hashCode ^ + recipientScreenName.hashCode ^ + title.hashCode ^ + text.hashCode ^ + createdAt.hashCode ^ + seen.hashCode ^ + parentUri.hashCode; } diff --git a/lib/models/direct_message_thread.dart b/lib/models/direct_message_thread.dart index 96ad0d6..963d21c 100644 --- a/lib/models/direct_message_thread.dart +++ b/lib/models/direct_message_thread.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import '../globals.dart'; @@ -23,6 +24,13 @@ class DirectMessageThread { required this.parentUri, }); + DirectMessageThread deepCopy() => DirectMessageThread( + messages: List.from(messages), + participants: List.from(participants), + title: title, + parentUri: parentUri, + ); + get allSeen => messages.isEmpty ? false : messages @@ -83,8 +91,15 @@ class DirectMessageThread { identical(this, other) || other is DirectMessageThread && runtimeType == other.runtimeType && - parentUri == other.parentUri; + title == other.title && + parentUri == other.parentUri && + listEquals(messages, other.messages) && + listEquals(participants, other.participants); @override - int get hashCode => parentUri.hashCode; + int get hashCode => + title.hashCode ^ + parentUri.hashCode ^ + Object.hashAll(messages) ^ + Object.hashAll(participants); } diff --git a/lib/riverpod_controllers/direct_message_services.dart b/lib/riverpod_controllers/direct_message_services.dart new file mode 100644 index 0000000..1980149 --- /dev/null +++ b/lib/riverpod_controllers/direct_message_services.dart @@ -0,0 +1,163 @@ +import 'package:logging/logging.dart'; +import 'package:relatica/models/connection.dart'; +import 'package:relatica/models/direct_message_thread.dart'; +import 'package:relatica/models/exec_error.dart'; +import 'package:result_monad/result_monad.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../friendica_client/friendica_client.dart'; +import '../friendica_client/paging_data.dart'; +import '../globals.dart'; +import '../models/auth/oauth_credentials.dart'; +import '../models/auth/profile.dart'; +import '../models/direct_message.dart'; +import '../services/feature_version_checker.dart'; + +part 'direct_message_services.g.dart'; + +@Riverpod(keepAlive: true) +class DirectMessageThreadIds extends _$DirectMessageThreadIds { + static final _logger = Logger('DirectMessageThreadIdsProvider'); + late Profile userProfile; + + @override + List build(Profile profile) { + userProfile = profile; + update(); + return []; + } + + Future update() async { + final threads = []; + await DirectMessagingClient(userProfile) + .getDirectMessages(PagingData()) + .match( + onSuccess: (update) { + final newThreads = DirectMessageThread.createThreads(update); + for (final t in newThreads) { + threads.add(t.parentUri); + ref + .read(directMessageThreadServiceProvider(userProfile, t.parentUri) + .notifier) + .update(t); + } + _logger.fine( + 'Updated ${update.length} direct messages, across ${newThreads.length} threads'); + }, + onError: (error) { + _logger.severe('Error getting direct messages: $error'); + }, + ); + state = threads; + } + + FutureResult newThread( + Connection receiver, String text) async { + if (userProfile.credentials is OAuthCredentials) { + final result = getIt() + .canUseFeatureResult(RelaticaFeatures.directMessageCreation); + if (result.isFailure) { + return result.errorCast(); + } + } + + final result = await DirectMessagingClient(userProfile).postDirectMessage( + null, + receiver.id, + text, + ); + result.match(onSuccess: (newMessage) { + DirectMessageThread.createThreads([newMessage]).forEach((thread) { + state = [...state, thread.parentUri]; + ref + .read(directMessageThreadServiceProvider( + userProfile, thread.parentUri) + .notifier) + .update(thread); + }); + }, onError: (error) { + _logger.severe('Error getting direct messages: $error'); + }); + + ref.invalidateSelf(); + return result.execErrorCast(); + } +} + +@Riverpod(keepAlive: true) +class DirectMessageThreadService extends _$DirectMessageThreadService { + static final _logger = Logger('DirectMessageThreadServiceProvider'); + String threadId = ''; + + @override + DirectMessageThread build(Profile profile, String id) { + _logger.severe('build id = $id'); + threadId = id; + state = DirectMessageThread( + messages: [], + participants: [], + title: 'Uninitialized', + parentUri: '', + ); + return state; + } + + void update(DirectMessageThread thread) { + print('OldThread == NewThread? ${state == thread}'); + state = thread; + } + + FutureResult newReplyMessage( + DirectMessage original, String text) async { + if (!state.messages.contains(original)) { + final error = + 'Message is not for this thread: ${state.parentUri}, $original'; + _logger.severe(error); + return buildErrorResult( + type: ErrorType.notFound, + message: error, + ); + } + + if (profile.credentials is OAuthCredentials) { + final result = getIt() + .canUseFeatureResult(RelaticaFeatures.directMessageCreation); + if (result.isFailure) { + return result.errorCast(); + } + } + + final result = await DirectMessagingClient(profile).postDirectMessage( + original.id, + original.senderId, + text, + ); + result.match(onSuccess: (newMessage) { + state.messages.add(newMessage); + }, onError: (error) { + _logger.severe('Error getting direct messages: $error'); + }); + + update(state); + return result.execErrorCast(); + } + + Future markMessageRead(DirectMessage m) async { + final oldIndex = state.messages.indexOf(m); + if (oldIndex < 0) { + _logger.severe('Message is not for this thread: ${state.parentUri}, $m'); + return; + } + + await DirectMessagingClient(profile).markDirectMessageRead(m).match( + onSuccess: (updatedItem) { + final newState = state.deepCopy(); + newState.messages[oldIndex] = updatedItem; + update(newState); + }, + onError: (error) { + _logger.severe('Error getting direct messages: $error'); + }, + ); + } +} diff --git a/lib/riverpod_controllers/direct_message_services.g.dart b/lib/riverpod_controllers/direct_message_services.g.dart new file mode 100644 index 0000000..b7768d8 --- /dev/null +++ b/lib/riverpod_controllers/direct_message_services.g.dart @@ -0,0 +1,344 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'direct_message_services.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$directMessageThreadIdsHash() => + r'30b269250935a6966d4fa47c479d29a4bb562729'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$DirectMessageThreadIds + extends BuildlessNotifier> { + late final Profile profile; + + List build( + Profile profile, + ); +} + +/// See also [DirectMessageThreadIds]. +@ProviderFor(DirectMessageThreadIds) +const directMessageThreadIdsProvider = DirectMessageThreadIdsFamily(); + +/// See also [DirectMessageThreadIds]. +class DirectMessageThreadIdsFamily extends Family> { + /// See also [DirectMessageThreadIds]. + const DirectMessageThreadIdsFamily(); + + /// See also [DirectMessageThreadIds]. + DirectMessageThreadIdsProvider call( + Profile profile, + ) { + return DirectMessageThreadIdsProvider( + profile, + ); + } + + @override + DirectMessageThreadIdsProvider getProviderOverride( + covariant DirectMessageThreadIdsProvider provider, + ) { + return call( + provider.profile, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'directMessageThreadIdsProvider'; +} + +/// See also [DirectMessageThreadIds]. +class DirectMessageThreadIdsProvider + extends NotifierProviderImpl> { + /// See also [DirectMessageThreadIds]. + DirectMessageThreadIdsProvider( + Profile profile, + ) : this._internal( + () => DirectMessageThreadIds()..profile = profile, + from: directMessageThreadIdsProvider, + name: r'directMessageThreadIdsProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$directMessageThreadIdsHash, + dependencies: DirectMessageThreadIdsFamily._dependencies, + allTransitiveDependencies: + DirectMessageThreadIdsFamily._allTransitiveDependencies, + profile: profile, + ); + + DirectMessageThreadIdsProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.profile, + }) : super.internal(); + + final Profile profile; + + @override + List runNotifierBuild( + covariant DirectMessageThreadIds notifier, + ) { + return notifier.build( + profile, + ); + } + + @override + Override overrideWith(DirectMessageThreadIds Function() create) { + return ProviderOverride( + origin: this, + override: DirectMessageThreadIdsProvider._internal( + () => create()..profile = profile, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + profile: profile, + ), + ); + } + + @override + NotifierProviderElement> + createElement() { + return _DirectMessageThreadIdsProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is DirectMessageThreadIdsProvider && other.profile == profile; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, profile.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin DirectMessageThreadIdsRef on NotifierProviderRef> { + /// The parameter `profile` of this provider. + Profile get profile; +} + +class _DirectMessageThreadIdsProviderElement + extends NotifierProviderElement> + with DirectMessageThreadIdsRef { + _DirectMessageThreadIdsProviderElement(super.provider); + + @override + Profile get profile => (origin as DirectMessageThreadIdsProvider).profile; +} + +String _$directMessageThreadServiceHash() => + r'f6a518e07e5e017ef0e2b9f1821408bbca77aef5'; + +abstract class _$DirectMessageThreadService + extends BuildlessNotifier { + late final Profile profile; + late final String id; + + DirectMessageThread build( + Profile profile, + String id, + ); +} + +/// See also [DirectMessageThreadService]. +@ProviderFor(DirectMessageThreadService) +const directMessageThreadServiceProvider = DirectMessageThreadServiceFamily(); + +/// See also [DirectMessageThreadService]. +class DirectMessageThreadServiceFamily extends Family { + /// See also [DirectMessageThreadService]. + const DirectMessageThreadServiceFamily(); + + /// See also [DirectMessageThreadService]. + DirectMessageThreadServiceProvider call( + Profile profile, + String id, + ) { + return DirectMessageThreadServiceProvider( + profile, + id, + ); + } + + @override + DirectMessageThreadServiceProvider getProviderOverride( + covariant DirectMessageThreadServiceProvider provider, + ) { + return call( + provider.profile, + provider.id, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'directMessageThreadServiceProvider'; +} + +/// See also [DirectMessageThreadService]. +class DirectMessageThreadServiceProvider extends NotifierProviderImpl< + DirectMessageThreadService, DirectMessageThread> { + /// See also [DirectMessageThreadService]. + DirectMessageThreadServiceProvider( + Profile profile, + String id, + ) : this._internal( + () => DirectMessageThreadService() + ..profile = profile + ..id = id, + from: directMessageThreadServiceProvider, + name: r'directMessageThreadServiceProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$directMessageThreadServiceHash, + dependencies: DirectMessageThreadServiceFamily._dependencies, + allTransitiveDependencies: + DirectMessageThreadServiceFamily._allTransitiveDependencies, + profile: profile, + id: id, + ); + + DirectMessageThreadServiceProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.profile, + required this.id, + }) : super.internal(); + + final Profile profile; + final String id; + + @override + DirectMessageThread runNotifierBuild( + covariant DirectMessageThreadService notifier, + ) { + return notifier.build( + profile, + id, + ); + } + + @override + Override overrideWith(DirectMessageThreadService Function() create) { + return ProviderOverride( + origin: this, + override: DirectMessageThreadServiceProvider._internal( + () => create() + ..profile = profile + ..id = id, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + profile: profile, + id: id, + ), + ); + } + + @override + NotifierProviderElement + createElement() { + return _DirectMessageThreadServiceProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is DirectMessageThreadServiceProvider && + other.profile == profile && + other.id == id; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, profile.hashCode); + hash = _SystemHash.combine(hash, id.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin DirectMessageThreadServiceRef + on NotifierProviderRef { + /// The parameter `profile` of this provider. + Profile get profile; + + /// The parameter `id` of this provider. + String get id; +} + +class _DirectMessageThreadServiceProviderElement + extends NotifierProviderElement with DirectMessageThreadServiceRef { + _DirectMessageThreadServiceProviderElement(super.provider); + + @override + Profile get profile => (origin as DirectMessageThreadServiceProvider).profile; + @override + String get id => (origin as DirectMessageThreadServiceProvider).id; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/screens/message_thread_screen.dart b/lib/screens/message_thread_screen.dart index 140d134..7ec2e55 100644 --- a/lib/screens/message_thread_screen.dart +++ b/lib/screens/message_thread_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:provider/provider.dart'; import 'package:result_monad/result_monad.dart'; @@ -7,15 +8,14 @@ import '../controls/padding.dart'; import '../controls/responsive_max_width.dart'; import '../controls/standard_appbar.dart'; import '../globals.dart'; +import '../models/auth/profile.dart'; import '../models/direct_message_thread.dart'; -import '../models/exec_error.dart'; +import '../riverpod_controllers/direct_message_services.dart'; import '../services/auth_service.dart'; -import '../services/direct_message_service.dart'; -import '../utils/active_profile_selector.dart'; import '../utils/clipboard_utils.dart'; import '../utils/snackbar_builder.dart'; -class MessageThreadScreen extends StatefulWidget { +class MessageThreadScreen extends ConsumerStatefulWidget { final String parentThreadId; const MessageThreadScreen({ @@ -24,141 +24,128 @@ class MessageThreadScreen extends StatefulWidget { }); @override - State createState() => _MessageThreadScreenState(); + ConsumerState createState() => + _MessageThreadScreenState(); } -class _MessageThreadScreenState extends State { +class _MessageThreadScreenState extends ConsumerState { final textController = TextEditingController(); @override Widget build(BuildContext context) { - final service = context - .watch>() - .activeEntry - .value; - final result = service.getThreadByParentUri(widget.parentThreadId); - final title = result.fold( - onSuccess: (t) => t.title.isEmpty ? 'Thread' : t.title, - onError: (_) => 'Thread'); + final profile = context.watch().currentProfile; + final t = ref.watch( + directMessageThreadServiceProvider(profile, widget.parentThreadId)); + final title = t.title.isEmpty ? 'Thread' : t.title; return Scaffold( appBar: StandardAppBar.build(context, title), - body: buildBody(result, service), + body: buildBody(profile, t), ); } Widget buildBody( - Result result, - DirectMessageService service, + Profile profile, + DirectMessageThread thread, ) { - return result.fold( - onSuccess: (thread) { - final yourId = getIt().currentProfile.userId; - final yourAvatarUrl = getIt().currentProfile.avatar; - final participants = - Map.fromEntries(thread.participants.map((p) => MapEntry(p.id, p))); - return Center( - child: Column( - children: [ - Expanded( - child: ResponsiveMaxWidth( - child: ListView.separated( - itemBuilder: (context, index) { - final m = thread.messages[index]; - final textPieces = m.text.split('...\n'); - final text = textPieces.length == 1 - ? textPieces[0] - : textPieces[1]; - final imageUrl = m.senderId == yourId - ? yourAvatarUrl - : participants[m.senderId]?.avatarUrl ?? ''; - return ListTile( - onTap: m.seen - ? null - : () => service.markMessageRead( - widget.parentThreadId, m), - onLongPress: () async { - await copyToClipboard(context: context, text: m.text); - }, - leading: ImageControl( - imageUrl: imageUrl, - iconOverride: const Icon(Icons.person), - width: 32.0, - onTap: null, - ), - title: Text( - text, - style: m.seen - ? null - : const TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Text(DateTime.fromMillisecondsSinceEpoch( - m.createdAt * 1000) - .toString()), - ); + final service = ref.read( + directMessageThreadServiceProvider(profile, thread.parentUri).notifier); + final yourId = getIt().currentProfile.userId; + final yourAvatarUrl = getIt().currentProfile.avatar; + final participants = + Map.fromEntries(thread.participants.map((p) => MapEntry(p.id, p))); + return Center( + child: Column( + children: [ + Expanded( + child: ResponsiveMaxWidth( + child: ListView.separated( + itemBuilder: (context, index) { + final m = thread.messages[index]; + final textPieces = m.text.split('...\n'); + final text = + textPieces.length == 1 ? textPieces[0] : textPieces[1]; + final imageUrl = m.senderId == yourId + ? yourAvatarUrl + : participants[m.senderId]?.avatarUrl ?? ''; + return ListTile( + onTap: m.seen ? null : () => service.markMessageRead(m), + onLongPress: () async { + await copyToClipboard(context: context, text: m.text); }, - separatorBuilder: (_, __) => const Divider(), - itemCount: thread.messages.length), - ), - ), - const VerticalDivider(), - Padding( - padding: const EdgeInsets.all(8.0), - child: ResponsiveMaxWidth( - child: TextFormField( - controller: textController, - textCapitalization: TextCapitalization.sentences, - spellCheckConfiguration: const SpellCheckConfiguration(), - maxLines: 4, - decoration: InputDecoration( - labelText: 'Reply Text', - border: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.surface, - ), - borderRadius: BorderRadius.circular(5.0), + leading: ImageControl( + imageUrl: imageUrl, + iconOverride: const Icon(Icons.person), + width: 32.0, + onTap: null, ), + title: Text( + text, + style: m.seen + ? null + : const TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text( + DateTime.fromMillisecondsSinceEpoch(m.createdAt * 1000) + .toString()), + ); + }, + separatorBuilder: (_, __) => const Divider(), + itemCount: thread.messages.length), + ), + ), + const VerticalDivider(), + Padding( + padding: const EdgeInsets.all(8.0), + child: ResponsiveMaxWidth( + child: TextFormField( + controller: textController, + textCapitalization: TextCapitalization.sentences, + spellCheckConfiguration: const SpellCheckConfiguration(), + maxLines: 4, + decoration: InputDecoration( + labelText: 'Reply Text', + border: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.surface, ), + borderRadius: BorderRadius.circular(5.0), ), ), ), - ElevatedButton( - onPressed: () async { - if (textController.text.isEmpty) { - buildSnackbar(context, "Can't submit an empty reply"); - return; - } - final othersMessages = - thread.messages.where((m) => m.senderId != yourId); - if (othersMessages.isEmpty) { - buildSnackbar( - context, "Have to wait for a response before sending"); - return; - } - await service - .newReplyMessage( - thread.parentUri, - othersMessages.last, - textController.text, - ) - .match(onSuccess: (_) { - setState(() { - textController.clear(); - }); - }, onError: (error) { - if (mounted) { - buildSnackbar(context, error.message); - } - }); - }, - child: const Text('Submit'), - ), - const VerticalPadding(), - ], - )); - }, - onError: (error) => Center( - child: Text('Error getting thread: $error'), - ), - ); + ), + ), + ElevatedButton( + onPressed: () async { + if (textController.text.isEmpty) { + buildSnackbar(context, "Can't submit an empty reply"); + return; + } + final othersMessages = + thread.messages.where((m) => m.senderId != yourId); + if (othersMessages.isEmpty) { + buildSnackbar( + context, "Have to wait for a response before sending"); + return; + } + await service + .newReplyMessage( + othersMessages.last, + textController.text, + ) + .match(onSuccess: (_) { + setState(() { + textController.clear(); + }); + }, onError: (error) { + if (mounted) { + buildSnackbar(context, error.message); + } + }); + }, + child: const Text('Submit'), + ), + const VerticalPadding(), + ], + )); } } diff --git a/lib/screens/message_threads_browser_screen.dart b/lib/screens/message_threads_browser_screen.dart index 3b868eb..c8cfda0 100644 --- a/lib/screens/message_threads_browser_screen.dart +++ b/lib/screens/message_threads_browser_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; @@ -7,27 +8,26 @@ import '../controls/responsive_max_width.dart'; import '../controls/standard_appbar.dart'; import '../controls/status_and_refresh_button.dart'; import '../globals.dart'; +import '../models/auth/profile.dart'; +import '../riverpod_controllers/direct_message_services.dart'; import '../routes.dart'; -import '../services/direct_message_service.dart'; +import '../services/auth_service.dart'; import '../services/network_status_service.dart'; -import '../utils/active_profile_selector.dart'; import '../utils/dateutils.dart'; -class MessagesScreen extends StatelessWidget { +class MessagesScreen extends ConsumerWidget { const MessagesScreen({super.key}); @override - Widget build(BuildContext context) { - final service = context - .watch>() - .activeEntry - .value; + Widget build(BuildContext context, WidgetRef ref) { + final profile = context.watch().currentProfile; + final service = ref.watch(directMessageThreadIdsProvider(profile).notifier); final nss = getIt(); return Scaffold( appBar: StandardAppBar.build(context, 'Direct Message Threads', actions: [ StatusAndRefreshButton( valueListenable: nss.directMessageUpdateStatus, - refreshFunction: () async => await service.updateThreads(), + refreshFunction: () async => await service.update(), busyColor: Theme.of(context).colorScheme.surface, ), IconButton( @@ -39,24 +39,24 @@ class MessagesScreen extends StatelessWidget { ]), body: RefreshIndicator( onRefresh: () async { - service.updateThreads(); + await service.update(); }, - child: Center(child: buildBody(context, service)), + child: Center(child: buildBody(profile, ref)), ), ); } - Widget buildBody(BuildContext context, DirectMessageService service) { - final threads = service.getThreads(); - threads.sort((t1, t2) => - t2.messages.last.createdAt.compareTo(t1.messages.last.createdAt)); - return threads.isEmpty + Widget buildBody(Profile profile, WidgetRef ref) { + final threadIds = ref.watch(directMessageThreadIdsProvider(profile)); + return threadIds.isEmpty ? const Text('No Direct Message Threads') : ResponsiveMaxWidth( child: ListView.separated( - itemCount: threads.length, + itemCount: threadIds.length, itemBuilder: (context, index) { - final thread = threads[index]; + final threadId = threadIds[index]; + final thread = ref.watch( + directMessageThreadServiceProvider(profile, threadId)); final style = thread.allSeen ? null : const TextStyle(fontWeight: FontWeight.bold); diff --git a/lib/screens/messages_new_thread.dart b/lib/screens/messages_new_thread.dart index 70e0e9f..c400046 100644 --- a/lib/screens/messages_new_thread.dart +++ b/lib/screens/messages_new_thread.dart @@ -1,17 +1,20 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:multi_trigger_autocomplete/multi_trigger_autocomplete.dart'; +import 'package:provider/provider.dart'; import '../controls/autocomplete/mention_autocomplete_options.dart'; import '../controls/padding.dart'; import '../controls/standard_appbar.dart'; import '../globals.dart'; +import '../riverpod_controllers/direct_message_services.dart'; +import '../services/auth_service.dart'; import '../services/connections_manager.dart'; -import '../services/direct_message_service.dart'; import '../utils/active_profile_selector.dart'; import '../utils/snackbar_builder.dart'; -class MessagesNewThread extends StatelessWidget { +class MessagesNewThread extends ConsumerWidget { final receiverController = TextEditingController(); final replyController = TextEditingController(); final focusNode = FocusNode(); @@ -19,14 +22,15 @@ class MessagesNewThread extends StatelessWidget { MessagesNewThread({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return Scaffold( appBar: StandardAppBar.build(context, 'New Thread'), - body: buildBody(context), + body: buildBody(context, ref), ); } - Widget buildBody(BuildContext context) { + Widget buildBody(BuildContext context, WidgetRef ref) { + final profile = context.watch().currentProfile; final border = OutlineInputBorder( borderSide: BorderSide( color: Theme.of(context).colorScheme.surface, @@ -88,16 +92,14 @@ class MessagesNewThread extends StatelessWidget { const VerticalPadding(), ElevatedButton( onPressed: () async { - final result = - await getIt>() - .activeEntry - .andThen((manager) => manager.getByHandle( - receiverController.text.trim().substring(1))) - .andThenAsync((connection) async => - getIt>() - .activeEntry - .andThenAsync((dms) async => dms.newThread( - connection, replyController.text))); + final result = await getIt< + ActiveProfileSelector>() + .activeEntry + .andThen((manager) => manager.getByHandle( + receiverController.text.trim().substring(1))) + .andThenAsync((connection) async => await ref + .read(directMessageThreadIdsProvider(profile).notifier) + .newThread(connection, replyController.text)); result.match(onSuccess: (_) { if (context.canPop()) { context.pop(); diff --git a/lib/services/direct_message_service.dart b/lib/services/direct_message_service.dart deleted file mode 100644 index 7236677..0000000 --- a/lib/services/direct_message_service.dart +++ /dev/null @@ -1,167 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:logging/logging.dart'; -import 'package:result_monad/result_monad.dart'; - -import '../friendica_client/friendica_client.dart'; -import '../friendica_client/paging_data.dart'; -import '../globals.dart'; -import '../models/auth/oauth_credentials.dart'; -import '../models/auth/profile.dart'; -import '../models/connection.dart'; -import '../models/direct_message.dart'; -import '../models/direct_message_thread.dart'; -import '../models/exec_error.dart'; -import 'feature_version_checker.dart'; - -class DirectMessageService extends ChangeNotifier { - static final _logger = Logger('$DirectMessageService'); - final _threads = {}; - final Profile profile; - var _firstLoading = true; - - DirectMessageService(this.profile); - - void clear() { - _threads.clear(); - _firstLoading = true; - notifyListeners(); - } - - List getThreads({bool unreadyOnly = false}) { - if (_threads.isEmpty && _firstLoading) { - updateThreads(); - _firstLoading = false; - } - - if (unreadyOnly) { - return _threads.values.where((t) => !t.allSeen).toList(); - } - - return _threads.values.toList(); - } - - Result getThreadByParentUri(String uri) { - if (_threads.containsKey(uri)) { - return Result.ok(_threads[uri]!); - } - - return buildErrorResult( - type: ErrorType.notFound, message: 'Thread ID not found: $uri'); - } - - Future updateThreads() async { - await DirectMessagingClient(profile).getDirectMessages(PagingData()).match( - onSuccess: (update) { - final newThreads = DirectMessageThread.createThreads(update); - _threads.clear(); - for (final t in newThreads) { - //TODO do merge operation - _threads[t.parentUri] = t; - } - _logger.fine( - 'Updated ${update.length} direct messages, across ${newThreads.length} threads'); - notifyListeners(); - }, - onError: (error) { - _logger.severe('Error getting direct messages: $error'); - }, - ); - - notifyListeners(); - } - - FutureResult newThread( - Connection receiver, String text) async { - if (profile.credentials is OAuthCredentials) { - final result = getIt() - .canUseFeatureResult(RelaticaFeatures.directMessageCreation); - if (result.isFailure) { - return result.errorCast(); - } - } - - final result = await DirectMessagingClient(profile).postDirectMessage( - null, - receiver.id, - text, - ); - result.match(onSuccess: (newMessage) { - DirectMessageThread.createThreads([newMessage]).forEach((thread) { - _threads[thread.parentUri] = thread; - }); - notifyListeners(); - }, onError: (error) { - _logger.severe('Error getting direct messages: $error'); - }); - - return result.execErrorCast(); - } - - FutureResult newReplyMessage( - String threadId, DirectMessage original, String text) async { - final thread = _threads[threadId]; - if (thread == null) { - final error = 'Message is not for this thread: $threadId, $original'; - _logger.severe(error); - return buildErrorResult( - type: ErrorType.notFound, - message: error, - ); - } - - if (!thread.messages.contains(original)) { - final error = 'Message is not for this thread: $threadId, $original'; - _logger.severe(error); - return buildErrorResult( - type: ErrorType.notFound, - message: error, - ); - } - - if (profile.credentials is OAuthCredentials) { - final result = getIt() - .canUseFeatureResult(RelaticaFeatures.directMessageCreation); - if (result.isFailure) { - return result.errorCast(); - } - } - - final result = await DirectMessagingClient(profile).postDirectMessage( - original.id, - original.senderId, - text, - ); - result.match(onSuccess: (newMessage) { - thread.messages.add(newMessage); - notifyListeners(); - }, onError: (error) { - _logger.severe('Error getting direct messages: $error'); - }); - - return result.execErrorCast(); - } - - Future markMessageRead(String threadId, DirectMessage m) async { - final thread = _threads[threadId]; - if (thread == null) { - _logger.severe('Message is not for this thread: $threadId, $m'); - return; - } - final oldIndex = thread.messages.indexOf(m); - if (oldIndex < 0) { - _logger.severe('Message is not for this thread: $threadId, $m'); - return; - } - - await DirectMessagingClient(profile).markDirectMessageRead(m).match( - onSuccess: (update) { - thread.messages.removeAt(oldIndex); - thread.messages.insert(oldIndex, update); - notifyListeners(); - }, - onError: (error) { - _logger.severe('Error getting direct messages: $error'); - }, - ); - } -} diff --git a/lib/services/notifications_manager.dart b/lib/services/notifications_manager.dart index ee6698a..2c5a3b7 100644 --- a/lib/services/notifications_manager.dart +++ b/lib/services/notifications_manager.dart @@ -14,7 +14,6 @@ import '../models/user_notification.dart'; import '../serializers/mastodon/follow_request_mastodon_extensions.dart'; import '../utils/active_profile_selector.dart'; import 'auth_service.dart'; -import 'direct_message_service.dart'; import 'feature_version_checker.dart'; import 'follow_requests_manager.dart'; import 'network_status_service.dart'; @@ -184,40 +183,42 @@ class NotificationsManager extends ChangeNotifier { List buildUnreadMessageNotifications( bool useActualRequests) { - final myId = profile.userId; - final dmsResult = getIt>() - .getForProfile(profile) - .transform((d) => d.getThreads(unreadyOnly: true).map((t) { - final fromAccount = - t.participants.firstWhere((p) => p.id != myId); - final latestMessage = t.messages - .reduce((s, m) => s.createdAt > m.createdAt ? s : m); - return UserNotification( - id: (fromAccount.hashCode ^ - t.parentUri.hashCode ^ - t.title.hashCode) - .toString(), - type: NotificationType.direct_message, - fromId: fromAccount.id, - fromName: fromAccount.name, - fromUrl: fromAccount.profileUrl, - timestamp: latestMessage.createdAt, - iid: t.parentUri, - dismissed: false, - content: '${fromAccount.name} sent you a direct message', - link: ''); - }).toList()) - .getValueOrElse(() => []); + // TODO Re-wire into DMS once this is converted to Riverpod Controller + // final myId = profile.userId; + // final dmsResult = getIt>() + // .getForProfile(profile) + // .transform((d) => d.getThreads(unreadyOnly: true).map((t) { + // final fromAccount = + // t.participants.firstWhere((p) => p.id != myId); + // final latestMessage = t.messages + // .reduce((s, m) => s.createdAt > m.createdAt ? s : m); + // return UserNotification( + // id: (fromAccount.hashCode ^ + // t.parentUri.hashCode ^ + // t.title.hashCode) + // .toString(), + // type: NotificationType.direct_message, + // fromId: fromAccount.id, + // fromName: fromAccount.name, + // fromUrl: fromAccount.profileUrl, + // timestamp: latestMessage.createdAt, + // iid: t.parentUri, + // dismissed: false, + // content: '${fromAccount.name} sent you a direct message', + // link: ''); + // }).toList()) + // .getValueOrElse(() => []); final followRequestResult = !useActualRequests - ? [] + ? [] : getIt>() .getForProfile(profile) .transform( (fm) => fm.requests.map((r) => r.toUserNotification()).toList()) .getValueOrElse(() => []); - return [...dmsResult, ...followRequestResult]; + // return [...dmsResult, ...followRequestResult]; + return followRequestResult; } void updateNotification(UserNotification notification) {} @@ -229,9 +230,9 @@ class NotificationsManager extends ChangeNotifier { getIt().startNotificationUpdate(); if (DateTime.now().difference(lastDmsUpdate) > minimumDmsAndCrsUpdateDuration) { - await getIt>() - .getForProfile(profile) - .transformAsync((dms) async => await dms.updateThreads()); + // await getIt>() + // .getForProfile(profile) + // .transformAsync((dms) async => await dms.updateThreads()); lastDmsUpdate = DateTime.now(); }