Convert DirectMessage System to Riverpod

This commit is contained in:
Hank Grabowski 2024-10-02 13:17:48 -04:00
parent bb0a6bd36b
commit f0ab80d312
12 changed files with 729 additions and 371 deletions

View file

@ -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<void> dependencyInjectionInitialization() async {
getIt.registerSingleton<ActiveProfileSelector<FollowRequestsManager>>(
ActiveProfileSelector((p) => FollowRequestsManager(p))
..subscribeToProfileSwaps());
getIt.registerSingleton<ActiveProfileSelector<DirectMessageService>>(
ActiveProfileSelector((p) => DirectMessageService(p))
..subscribeToProfileSwaps());
getIt.registerSingleton<ActiveProfileSelector<InteractionsManager>>(
ActiveProfileSelector((p) => InteractionsManager(p))
..subscribeToProfileSwaps());
@ -167,12 +163,6 @@ void clearCaches() {
_logger.severe('Error clearing IConnections Repo: $error'),
);
getIt<ActiveProfileSelector<DirectMessageService>>().activeEntry.match(
onSuccess: (service) => service.clear(),
onError: (error) =>
_logger.severe('Error clearing DirectMessageService Repo: $error'),
);
getIt<ActiveProfileSelector<EntryManagerService>>().activeEntry.match(
onSuccess: (service) => service.clear(),
onError: (error) =>

View file

@ -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<App> {
create: (_) =>
getIt<ActiveProfileSelector<FollowRequestsManager>>(),
),
ChangeNotifierProvider<
ActiveProfileSelector<DirectMessageService>>(
create: (_) =>
getIt<ActiveProfileSelector<DirectMessageService>>(),
),
ChangeNotifierProvider<
ActiveProfileSelector<InteractionsManager>>(
create: (_) =>

View file

@ -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<String, dynamic> json,
ICredentials Function(Map<String, dynamic> json) credentialsFromJson,

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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<String> build(Profile profile) {
userProfile = profile;
update();
return [];
}
Future<void> update() async {
final threads = <String>[];
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<DirectMessage, ExecError> newThread(
Connection receiver, String text) async {
if (userProfile.credentials is OAuthCredentials) {
final result = getIt<FriendicaVersionChecker>()
.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<DirectMessage, ExecError> 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<FriendicaVersionChecker>()
.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<void> 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');
},
);
}
}

View file

@ -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<List<String>> {
late final Profile profile;
List<String> build(
Profile profile,
);
}
/// See also [DirectMessageThreadIds].
@ProviderFor(DirectMessageThreadIds)
const directMessageThreadIdsProvider = DirectMessageThreadIdsFamily();
/// See also [DirectMessageThreadIds].
class DirectMessageThreadIdsFamily extends Family<List<String>> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'directMessageThreadIdsProvider';
}
/// See also [DirectMessageThreadIds].
class DirectMessageThreadIdsProvider
extends NotifierProviderImpl<DirectMessageThreadIds, List<String>> {
/// 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<String> 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<DirectMessageThreadIds, List<String>>
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<List<String>> {
/// The parameter `profile` of this provider.
Profile get profile;
}
class _DirectMessageThreadIdsProviderElement
extends NotifierProviderElement<DirectMessageThreadIds, List<String>>
with DirectMessageThreadIdsRef {
_DirectMessageThreadIdsProviderElement(super.provider);
@override
Profile get profile => (origin as DirectMessageThreadIdsProvider).profile;
}
String _$directMessageThreadServiceHash() =>
r'f6a518e07e5e017ef0e2b9f1821408bbca77aef5';
abstract class _$DirectMessageThreadService
extends BuildlessNotifier<DirectMessageThread> {
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<DirectMessageThread> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? 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<DirectMessageThreadService, DirectMessageThread>
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<DirectMessageThread> {
/// The parameter `profile` of this provider.
Profile get profile;
/// The parameter `id` of this provider.
String get id;
}
class _DirectMessageThreadServiceProviderElement
extends NotifierProviderElement<DirectMessageThreadService,
DirectMessageThread> 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

View file

@ -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<MessageThreadScreen> createState() => _MessageThreadScreenState();
ConsumerState<MessageThreadScreen> createState() =>
_MessageThreadScreenState();
}
class _MessageThreadScreenState extends State<MessageThreadScreen> {
class _MessageThreadScreenState extends ConsumerState<MessageThreadScreen> {
final textController = TextEditingController();
@override
Widget build(BuildContext context) {
final service = context
.watch<ActiveProfileSelector<DirectMessageService>>()
.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<AccountsService>().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<DirectMessageThread, ExecError> result,
DirectMessageService service,
Profile profile,
DirectMessageThread thread,
) {
return result.fold(
onSuccess: (thread) {
final yourId = getIt<AccountsService>().currentProfile.userId;
final yourAvatarUrl = getIt<AccountsService>().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<AccountsService>().currentProfile.userId;
final yourAvatarUrl = getIt<AccountsService>().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(),
],
));
}
}

View file

@ -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<ActiveProfileSelector<DirectMessageService>>()
.activeEntry
.value;
Widget build(BuildContext context, WidgetRef ref) {
final profile = context.watch<AccountsService>().currentProfile;
final service = ref.watch(directMessageThreadIdsProvider(profile).notifier);
final nss = getIt<NetworkStatusService>();
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);

View file

@ -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<AccountsService>().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<ActiveProfileSelector<ConnectionsManager>>()
.activeEntry
.andThen((manager) => manager.getByHandle(
receiverController.text.trim().substring(1)))
.andThenAsync((connection) async =>
getIt<ActiveProfileSelector<DirectMessageService>>()
.activeEntry
.andThenAsync((dms) async => dms.newThread(
connection, replyController.text)));
final result = await getIt<
ActiveProfileSelector<ConnectionsManager>>()
.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();

View file

@ -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 = <String, DirectMessageThread>{};
final Profile profile;
var _firstLoading = true;
DirectMessageService(this.profile);
void clear() {
_threads.clear();
_firstLoading = true;
notifyListeners();
}
List<DirectMessageThread> 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<DirectMessageThread, ExecError> getThreadByParentUri(String uri) {
if (_threads.containsKey(uri)) {
return Result.ok(_threads[uri]!);
}
return buildErrorResult(
type: ErrorType.notFound, message: 'Thread ID not found: $uri');
}
Future<void> 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<DirectMessage, ExecError> newThread(
Connection receiver, String text) async {
if (profile.credentials is OAuthCredentials) {
final result = getIt<FriendicaVersionChecker>()
.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<DirectMessage, ExecError> 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<FriendicaVersionChecker>()
.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<void> 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');
},
);
}
}

View file

@ -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<UserNotification> buildUnreadMessageNotifications(
bool useActualRequests) {
final myId = profile.userId;
final dmsResult = getIt<ActiveProfileSelector<DirectMessageService>>()
.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<ActiveProfileSelector<DirectMessageService>>()
// .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
? []
? <UserNotification>[]
: getIt<ActiveProfileSelector<FollowRequestsManager>>()
.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<NetworkStatusService>().startNotificationUpdate();
if (DateTime.now().difference(lastDmsUpdate) >
minimumDmsAndCrsUpdateDuration) {
await getIt<ActiveProfileSelector<DirectMessageService>>()
.getForProfile(profile)
.transformAsync((dms) async => await dms.updateThreads());
// await getIt<ActiveProfileSelector<DirectMessageService>>()
// .getForProfile(profile)
// .transformAsync((dms) async => await dms.updateThreads());
lastDmsUpdate = DateTime.now();
}