🔊 Add more logging and optimzation

This commit is contained in:
2025-08-16 23:39:41 +08:00
parent 007b46b080
commit 6892afb974

View File

@@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:developer' as developer;
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@@ -41,6 +42,8 @@ import 'package:island/widgets/stickers/picker.dart';
part 'room.g.dart'; part 'room.g.dart';
final isSyncingProvider = StateProvider.autoDispose<bool>((ref) => false);
final appLifecycleStateProvider = StreamProvider<AppLifecycleState>((ref) { final appLifecycleStateProvider = StreamProvider<AppLifecycleState>((ref) {
final controller = StreamController<AppLifecycleState>(); final controller = StreamController<AppLifecycleState>();
@@ -97,8 +100,11 @@ class MessagesNotifier extends _$MessagesNotifier {
_room = room; _room = room;
_identity = identity; _identity = identity;
developer.log('MessagesNotifier built for room $roomId', name: 'MessagesNotifier');
ref.listen(appLifecycleStateProvider, (_, next) { ref.listen(appLifecycleStateProvider, (_, next) {
if (next.hasValue && next.value == AppLifecycleState.resumed) { if (next.hasValue && next.value == AppLifecycleState.resumed) {
developer.log('App resumed, syncing messages', name: 'MessagesNotifier');
syncMessages(); syncMessages();
} }
}); });
@@ -110,6 +116,7 @@ class MessagesNotifier extends _$MessagesNotifier {
int offset = 0, int offset = 0,
int take = 20, int take = 20,
}) async { }) async {
developer.log('Getting cached messages from offset $offset, take $take', name: 'MessagesNotifier');
final dbMessages = await _database.getMessagesForRoom( final dbMessages = await _database.getMessagesForRoom(
_roomId, _roomId,
offset: offset, offset: offset,
@@ -120,9 +127,7 @@ class MessagesNotifier extends _$MessagesNotifier {
if (offset == 0) { if (offset == 0) {
final pendingForRoom = final pendingForRoom =
_pendingMessages.values _pendingMessages.values.where((msg) => msg.roomId == _roomId).toList();
.where((msg) => msg.roomId == _roomId)
.toList();
final allMessages = [...pendingForRoom, ...dbLocalMessages]; final allMessages = [...pendingForRoom, ...dbLocalMessages];
allMessages.sort((a, b) => b.createdAt.compareTo(a.createdAt)); allMessages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
@@ -144,6 +149,7 @@ class MessagesNotifier extends _$MessagesNotifier {
int offset = 0, int offset = 0,
int take = 20, int take = 20,
}) async { }) async {
developer.log('Fetching messages from API, offset $offset, take $take', name: 'MessagesNotifier');
if (_totalCount == null) { if (_totalCount == null) {
final response = await _apiClient.get( final response = await _apiClient.get(
'/sphere/chat/$_roomId/messages', '/sphere/chat/$_roomId/messages',
@@ -164,8 +170,7 @@ class MessagesNotifier extends _$MessagesNotifier {
final List<dynamic> data = response.data; final List<dynamic> data = response.data;
_totalCount = int.parse(response.headers['x-total']?.firstOrNull ?? '0'); _totalCount = int.parse(response.headers['x-total']?.firstOrNull ?? '0');
final messages = final messages = data.map((json) {
data.map((json) {
final remoteMessage = SnChatMessage.fromJson(json); final remoteMessage = SnChatMessage.fromJson(json);
return LocalChatMessage.fromRemoteMessage( return LocalChatMessage.fromRemoteMessage(
remoteMessage, remoteMessage,
@@ -185,18 +190,25 @@ class MessagesNotifier extends _$MessagesNotifier {
return messages; return messages;
} }
Future<bool> syncMessages() async { Future<void> syncMessages() async {
developer.log('Starting message sync', name: 'MessagesNotifier');
ref.read(isSyncingProvider.notifier).state = true;
try {
final dbMessages = await _database.getMessagesForRoom( final dbMessages = await _database.getMessagesForRoom(
_room.id, _room.id,
offset: 0, offset: 0,
limit: 1, limit: 1,
); );
final lastMessage = final lastMessage =
dbMessages.isEmpty dbMessages.isEmpty ? null : _database.companionToMessage(dbMessages.first);
? null
: _database.companionToMessage(dbMessages.first); if (lastMessage == null) {
if (lastMessage == null) return false; developer.log('No local messages, fetching from network', name: 'MessagesNotifier');
try { final newMessages = await _fetchAndCacheMessages(offset: 0, take: _pageSize);
state = AsyncValue.data(newMessages);
return;
}
final resp = await _apiClient.post( final resp = await _apiClient.post(
'/sphere/chat/${_room.id}/sync', '/sphere/chat/${_room.id}/sync',
data: { data: {
@@ -206,6 +218,7 @@ class MessagesNotifier extends _$MessagesNotifier {
); );
final response = MessageSyncResponse.fromJson(resp.data); final response = MessageSyncResponse.fromJson(resp.data);
developer.log('Sync response: ${response.changes.length} changes', name: 'MessagesNotifier');
for (final change in response.changes) { for (final change in response.changes) {
switch (change.action) { switch (change.action) {
case MessageChangeAction.create: case MessageChangeAction.create:
@@ -219,10 +232,13 @@ class MessagesNotifier extends _$MessagesNotifier {
break; break;
} }
} }
} catch (err) { } catch (err, stackTrace) {
developer.log('Error syncing messages', name: 'MessagesNotifier', error: err, stackTrace: stackTrace);
showErrorAlert(err); showErrorAlert(err);
} finally {
developer.log('Finished message sync', name: 'MessagesNotifier');
ref.read(isSyncingProvider.notifier).state = false;
} }
return true;
} }
Future<List<LocalChatMessage>> listMessages({ Future<List<LocalChatMessage>> listMessages({
@@ -261,23 +277,17 @@ class MessagesNotifier extends _$MessagesNotifier {
} }
Future<List<LocalChatMessage>> loadInitial() async { Future<List<LocalChatMessage>> loadInitial() async {
try { developer.log('Loading initial messages', name: 'MessagesNotifier');
final synced = await syncMessages(); syncMessages();
final messages = await listMessages( final messages = await _getCachedMessages(offset: 0, take: _pageSize);
offset: 0,
take: _pageSize,
synced: synced,
);
_currentPage = 0; _currentPage = 0;
_hasMore = messages.length == _pageSize; _hasMore = messages.length == _pageSize;
return messages; return messages;
} catch (_) {
rethrow;
}
} }
Future<void> loadMore() async { Future<void> loadMore() async {
if (!_hasMore || state is AsyncLoading) return; if (!_hasMore || state is AsyncLoading) return;
developer.log('Loading more messages', name: 'MessagesNotifier');
try { try {
final currentMessages = state.value ?? []; final currentMessages = state.value ?? [];
@@ -292,7 +302,8 @@ class MessagesNotifier extends _$MessagesNotifier {
} }
state = AsyncValue.data([...currentMessages, ...newMessages]); state = AsyncValue.data([...currentMessages, ...newMessages]);
} catch (err) { } catch (err, stackTrace) {
developer.log('Error loading more messages', name: 'MessagesNotifier', error: err, stackTrace: stackTrace);
showErrorAlert(err); showErrorAlert(err);
_currentPage--; _currentPage--;
} }
@@ -306,11 +317,12 @@ class MessagesNotifier extends _$MessagesNotifier {
SnChatMessage? replyingTo, SnChatMessage? replyingTo,
Function(String, Map<int, double>)? onProgress, Function(String, Map<int, double>)? onProgress,
}) async { }) async {
final nonce = const Uuid().v4();
developer.log('Sending message with nonce $nonce', name: 'MessagesNotifier');
final baseUrl = ref.read(serverUrlProvider); final baseUrl = ref.read(serverUrlProvider);
final token = await getToken(ref.watch(tokenProvider)); final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Access token is null'); if (token == null) throw ArgumentError('Access token is null');
final nonce = const Uuid().v4();
final mockMessage = SnChatMessage( final mockMessage = SnChatMessage(
id: 'pending_$nonce', id: 'pending_$nonce',
chatRoomId: _roomId, chatRoomId: _roomId,
@@ -337,14 +349,12 @@ class MessagesNotifier extends _$MessagesNotifier {
try { try {
var cloudAttachments = List.empty(growable: true); var cloudAttachments = List.empty(growable: true);
for (var idx = 0; idx < attachments.length; idx++) { for (var idx = 0; idx < attachments.length; idx++) {
final cloudFile = final cloudFile = await putMediaToCloud(
await putMediaToCloud(
fileData: attachments[idx], fileData: attachments[idx],
atk: token, atk: token,
baseUrl: baseUrl, baseUrl: baseUrl,
filename: attachments[idx].data.name ?? 'Post media', filename: attachments[idx].data.name ?? 'Post media',
mimetype: mimetype: attachments[idx].data.mimeType ??
attachments[idx].data.mimeType ??
switch (attachments[idx].type) { switch (attachments[idx].type) {
UniversalFileType.image => 'image/unknown', UniversalFileType.image => 'image/unknown',
UniversalFileType.video => 'video/unknown', UniversalFileType.video => 'video/unknown',
@@ -390,34 +400,35 @@ class MessagesNotifier extends _$MessagesNotifier {
await _database.deleteMessage(localMessage.id); await _database.deleteMessage(localMessage.id);
await _database.saveMessage(_database.messageToCompanion(updatedMessage)); await _database.saveMessage(_database.messageToCompanion(updatedMessage));
final newMessages = final newMessages = (state.value ?? []).map((m) {
(state.value ?? []).map((m) {
if (m.id == localMessage.id) { if (m.id == localMessage.id) {
return updatedMessage; return updatedMessage;
} }
return m; return m;
}).toList(); }).toList();
state = AsyncValue.data(newMessages); state = AsyncValue.data(newMessages);
} catch (err) { developer.log('Message with nonce $nonce sent successfully', name: 'MessagesNotifier');
} catch (e, stackTrace) {
developer.log('Failed to send message with nonce $nonce', name: 'MessagesNotifier', error: e, stackTrace: stackTrace);
localMessage.status = MessageStatus.failed; localMessage.status = MessageStatus.failed;
_pendingMessages[localMessage.id] = localMessage; _pendingMessages[localMessage.id] = localMessage;
await _database.updateMessageStatus( await _database.updateMessageStatus(
localMessage.id, localMessage.id,
MessageStatus.failed, MessageStatus.failed,
); );
final newMessages = final newMessages = (state.value ?? []).map((m) {
(state.value ?? []).map((m) {
if (m.id == localMessage.id) { if (m.id == localMessage.id) {
return m..status = MessageStatus.failed; return m..status = MessageStatus.failed;
} }
return m; return m;
}).toList(); }).toList();
state = AsyncValue.data(newMessages); state = AsyncValue.data(newMessages);
showErrorAlert(err); showErrorAlert(e);
} }
} }
Future<void> retryMessage(String pendingMessageId) async { Future<void> retryMessage(String pendingMessageId) async {
developer.log('Retrying message $pendingMessageId', name: 'MessagesNotifier');
final message = await fetchMessageById(pendingMessageId); final message = await fetchMessageById(pendingMessageId);
if (message == null) { if (message == null) {
throw Exception('Message not found'); throw Exception('Message not found');
@@ -452,35 +463,35 @@ class MessagesNotifier extends _$MessagesNotifier {
await _database.deleteMessage(pendingMessageId); await _database.deleteMessage(pendingMessageId);
await _database.saveMessage(_database.messageToCompanion(updatedMessage)); await _database.saveMessage(_database.messageToCompanion(updatedMessage));
final newMessages = final newMessages = (state.value ?? []).map((m) {
(state.value ?? []).map((m) {
if (m.id == pendingMessageId) { if (m.id == pendingMessageId) {
return updatedMessage; return updatedMessage;
} }
return m; return m;
}).toList(); }).toList();
state = AsyncValue.data(newMessages); state = AsyncValue.data(newMessages);
} catch (err) { } catch (e, stackTrace) {
developer.log('Failed to retry message $pendingMessageId', name: 'MessagesNotifier', error: e, stackTrace: stackTrace);
message.status = MessageStatus.failed; message.status = MessageStatus.failed;
_pendingMessages[pendingMessageId] = message; _pendingMessages[pendingMessageId] = message;
await _database.updateMessageStatus( await _database.updateMessageStatus(
pendingMessageId, pendingMessageId,
MessageStatus.failed, MessageStatus.failed,
); );
final newMessages = final newMessages = (state.value ?? []).map((m) {
(state.value ?? []).map((m) {
if (m.id == pendingMessageId) { if (m.id == pendingMessageId) {
return m..status = MessageStatus.failed; return m..status = MessageStatus.failed;
} }
return m; return m;
}).toList(); }).toList();
state = AsyncValue.data(newMessages); state = AsyncValue.data(newMessages);
showErrorAlert(err); showErrorAlert(e);
} }
} }
Future<void> receiveMessage(SnChatMessage remoteMessage) async { Future<void> receiveMessage(SnChatMessage remoteMessage) async {
if (remoteMessage.chatRoomId != _roomId) return; if (remoteMessage.chatRoomId != _roomId) return;
developer.log('Received new message ${remoteMessage.id}', name: 'MessagesNotifier');
final localMessage = LocalChatMessage.fromRemoteMessage( final localMessage = LocalChatMessage.fromRemoteMessage(
remoteMessage, remoteMessage,
@@ -513,6 +524,7 @@ class MessagesNotifier extends _$MessagesNotifier {
Future<void> receiveMessageUpdate(SnChatMessage remoteMessage) async { Future<void> receiveMessageUpdate(SnChatMessage remoteMessage) async {
if (remoteMessage.chatRoomId != _roomId) return; if (remoteMessage.chatRoomId != _roomId) return;
developer.log('Received message update ${remoteMessage.id}', name: 'MessagesNotifier');
final updatedMessage = LocalChatMessage.fromRemoteMessage( final updatedMessage = LocalChatMessage.fromRemoteMessage(
remoteMessage, remoteMessage,
@@ -521,7 +533,9 @@ class MessagesNotifier extends _$MessagesNotifier {
await _database.updateMessage(_database.messageToCompanion(updatedMessage)); await _database.updateMessage(_database.messageToCompanion(updatedMessage));
final currentMessages = state.value ?? []; final currentMessages = state.value ?? [];
final index = currentMessages.indexWhere((m) => m.id == updatedMessage.id); final index = currentMessages.indexWhere(
(m) => m.id == updatedMessage.id,
);
if (index >= 0) { if (index >= 0) {
final newList = [...currentMessages]; final newList = [...currentMessages];
@@ -531,6 +545,7 @@ class MessagesNotifier extends _$MessagesNotifier {
} }
Future<void> receiveMessageDeletion(String messageId) async { Future<void> receiveMessageDeletion(String messageId) async {
developer.log('Received message deletion $messageId', name: 'MessagesNotifier');
_pendingMessages.remove(messageId); _pendingMessages.remove(messageId);
await _database.deleteMessage(messageId); await _database.deleteMessage(messageId);
@@ -544,19 +559,22 @@ class MessagesNotifier extends _$MessagesNotifier {
} }
Future<void> deleteMessage(String messageId) async { Future<void> deleteMessage(String messageId) async {
developer.log('Deleting message $messageId', name: 'MessagesNotifier');
try { try {
await _apiClient.delete('/sphere/chat/$_roomId/messages/$messageId'); await _apiClient.delete('/sphere/chat/$_roomId/messages/$messageId');
await receiveMessageDeletion(messageId); await receiveMessageDeletion(messageId);
} catch (err) { } catch (err, stackTrace) {
developer.log('Error deleting message $messageId', name: 'MessagesNotifier', error: err, stackTrace: stackTrace);
showErrorAlert(err); showErrorAlert(err);
} }
} }
Future<LocalChatMessage?> fetchMessageById(String messageId) async { Future<LocalChatMessage?> fetchMessageById(String messageId) async {
developer.log('Fetching message by id $messageId', name: 'MessagesNotifier');
try { try {
final localMessage = final localMessage = await (_database.select(_database.chatMessages)
await (_database.select(_database.chatMessages) ..where((tbl) => tbl.id.equals(messageId)))
..where((tbl) => tbl.id.equals(messageId))).getSingleOrNull(); .getSingleOrNull();
if (localMessage != null) { if (localMessage != null) {
return _database.companionToMessage(localMessage); return _database.companionToMessage(localMessage);
} }
@@ -587,6 +605,7 @@ class ChatRoomScreen extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final chatRoom = ref.watch(chatroomProvider(id)); final chatRoom = ref.watch(chatroomProvider(id));
final chatIdentity = ref.watch(chatroomIdentityProvider(id)); final chatIdentity = ref.watch(chatroomIdentityProvider(id));
final isSyncing = ref.watch(isSyncingProvider);
if (chatIdentity.isLoading || chatRoom.isLoading) { if (chatIdentity.isLoading || chatRoom.isLoading) {
return AppScaffold( return AppScaffold(
@@ -598,8 +617,7 @@ class ChatRoomScreen extends HookConsumerWidget {
return AppScaffold( return AppScaffold(
appBar: AppBar(leading: const PageBackButton()), appBar: AppBar(leading: const PageBackButton()),
body: Center( body: Center(
child: child: ConstrainedBox(
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 280), constraints: const BoxConstraints(maxWidth: 280),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@@ -708,10 +726,8 @@ class ChatRoomScreen extends HookConsumerWidget {
if (typingStatuses.value.isNotEmpty) { if (typingStatuses.value.isNotEmpty) {
// Remove typing statuses older than 5 seconds // Remove typing statuses older than 5 seconds
final now = DateTime.now(); final now = DateTime.now();
typingStatuses.value = typingStatuses.value = typingStatuses.value.where((member) {
typingStatuses.value.where((member) { final lastTyped = member.lastTyped ??
final lastTyped =
member.lastTyped ??
DateTime.now().subtract(const Duration(milliseconds: 1350)); DateTime.now().subtract(const Duration(milliseconds: 1350));
return now.difference(lastTyped).inSeconds < 5; return now.difference(lastTyped).inSeconds < 5;
}).toList(); }).toList();
@@ -885,9 +901,7 @@ class ChatRoomScreen extends HookConsumerWidget {
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
toolbarHeight: compactHeader ? null : 64, toolbarHeight: compactHeader ? null : 64,
title: chatRoom.when( title: chatRoom.when(
data: data: (room) => compactHeader
(room) =>
compactHeader
? Row( ? Row(
spacing: 8, spacing: 8,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@@ -895,18 +909,11 @@ class ChatRoomScreen extends HookConsumerWidget {
SizedBox( SizedBox(
height: 26, height: 26,
width: 26, width: 26,
child: child: (room!.type == 1 && room.picture?.id == null)
(room!.type == 1 && room.picture?.id == null)
? SplitAvatarWidget( ? SplitAvatarWidget(
filesId: filesId: room.members!
room.members!
.map( .map(
(e) => (e) => e.account.profile.picture?.id,
e
.account
.profile
.picture
?.id,
) )
.toList(), .toList(),
) )
@@ -924,9 +931,7 @@ class ChatRoomScreen extends HookConsumerWidget {
), ),
Text( Text(
(room.type == 1 && room.name == null) (room.type == 1 && room.name == null)
? room.members! ? room.members!.map((e) => e.account.nick).join(', ')
.map((e) => e.account.nick)
.join(', ')
: room.name!, : room.name!,
).fontSize(19), ).fontSize(19),
], ],
@@ -939,18 +944,11 @@ class ChatRoomScreen extends HookConsumerWidget {
SizedBox( SizedBox(
height: 26, height: 26,
width: 26, width: 26,
child: child: (room!.type == 1 && room.picture?.id == null)
(room!.type == 1 && room.picture?.id == null)
? SplitAvatarWidget( ? SplitAvatarWidget(
filesId: filesId: room.members!
room.members!
.map( .map(
(e) => (e) => e.account.profile.picture?.id,
e
.account
.profile
.picture
?.id,
) )
.toList(), .toList(),
) )
@@ -968,16 +966,13 @@ class ChatRoomScreen extends HookConsumerWidget {
), ),
Text( Text(
(room.type == 1 && room.name == null) (room.type == 1 && room.name == null)
? room.members! ? room.members!.map((e) => e.account.nick).join(', ')
.map((e) => e.account.nick)
.join(', ')
: room.name!, : room.name!,
).fontSize(15), ).fontSize(15),
], ],
), ),
loading: () => const Text('Loading...'), loading: () => const Text('Loading...'),
error: error: (err, _) => ResponseErrorWidget(
(err, _) => ResponseErrorWidget(
error: err, error: err,
onRetry: () => messagesNotifier.loadInitial(), onRetry: () => messagesNotifier.loadInitial(),
), ),
@@ -992,6 +987,12 @@ class ChatRoomScreen extends HookConsumerWidget {
), ),
const Gap(8), const Gap(8),
], ],
bottom: isSyncing
? const PreferredSize(
preferredSize: Size.fromHeight(4.0),
child: LinearProgressIndicator(),
)
: null,
), ),
body: Stack( body: Stack(
children: [ children: [
@@ -999,16 +1000,13 @@ class ChatRoomScreen extends HookConsumerWidget {
children: [ children: [
Expanded( Expanded(
child: messages.when( child: messages.when(
data: data: (messageList) => messageList.isEmpty
(messageList) =>
messageList.isEmpty
? Center(child: Text('No messages yet'.tr())) ? Center(child: Text('No messages yet'.tr()))
: SuperListView.builder( : SuperListView.builder(
listController: listController, listController: listController,
padding: EdgeInsets.symmetric(vertical: 16), padding: EdgeInsets.symmetric(vertical: 16),
controller: scrollController, controller: scrollController,
reverse: reverse: true, // Show newest messages at the bottom
true, // Show newest messages at the bottom
itemCount: messageList.length, itemCount: messageList.length,
findChildIndexCallback: (key) { findChildIndexCallback: (key) {
final valueKey = key as ValueKey; final valueKey = key as ValueKey;
@@ -1019,14 +1017,11 @@ class ChatRoomScreen extends HookConsumerWidget {
}, },
itemBuilder: (context, index) { itemBuilder: (context, index) {
final message = messageList[index]; final message = messageList[index];
final nextMessage = final nextMessage = index < messageList.length - 1
index < messageList.length - 1
? messageList[index + 1] ? messageList[index + 1]
: null; : null;
final isLastInGroup = final isLastInGroup = nextMessage == null ||
nextMessage == null || nextMessage.senderId != message.senderId ||
nextMessage.senderId !=
message.senderId ||
nextMessage.createdAt nextMessage.createdAt
.difference(message.createdAt) .difference(message.createdAt)
.inMinutes .inMinutes
@@ -1035,12 +1030,10 @@ class ChatRoomScreen extends HookConsumerWidget {
return chatIdentity.when( return chatIdentity.when(
skipError: true, skipError: true,
data: data: (identity) => MessageItem(
(identity) => MessageItem(
key: ValueKey(message.id), key: ValueKey(message.id),
message: message, message: message,
isCurrentUser: isCurrentUser: identity?.id == message.senderId,
identity?.id == message.senderId,
onAction: (action) { onAction: (action) {
switch (action) { switch (action) {
case MessageItemAction.delete: case MessageItemAction.delete:
@@ -1051,17 +1044,11 @@ class ChatRoomScreen extends HookConsumerWidget {
messageEditingTo.value = messageEditingTo.value =
message.toRemoteMessage(); message.toRemoteMessage();
messageController.text = messageController.text =
messageEditingTo messageEditingTo.value?.content ?? '';
.value attachments.value = messageEditingTo
?.content ?? .value!.attachments
'';
attachments.value =
messageEditingTo
.value!
.attachments
.map( .map(
(e) => (e) => UniversalFile.fromAttachment(
UniversalFile.fromAttachment(
e, e,
), ),
) )
@@ -1075,8 +1062,7 @@ class ChatRoomScreen extends HookConsumerWidget {
} }
}, },
onJump: (messageId) { onJump: (messageId) {
final messageIndex = messageList final messageIndex = messageList.indexWhere(
.indexWhere(
(m) => m.id == messageId, (m) => m.id == messageId,
); );
listController.jumpToItem( listController.jumpToItem(
@@ -1086,13 +1072,10 @@ class ChatRoomScreen extends HookConsumerWidget {
alignment: 0.5, alignment: 0.5,
); );
}, },
progress: progress: attachmentProgress.value[message.id],
attachmentProgress.value[message
.id],
showAvatar: isLastInGroup, showAvatar: isLastInGroup,
), ),
loading: loading: () => MessageItem(
() => MessageItem(
message: message, message: message,
isCurrentUser: false, isCurrentUser: false,
onAction: null, onAction: null,
@@ -1104,18 +1087,15 @@ class ChatRoomScreen extends HookConsumerWidget {
); );
}, },
), ),
loading: loading: () => const Center(child: CircularProgressIndicator()),
() => const Center(child: CircularProgressIndicator()), error: (error, _) => ResponseErrorWidget(
error:
(error, _) => ResponseErrorWidget(
error: error, error: error,
onRetry: () => messagesNotifier.loadInitial(), onRetry: () => messagesNotifier.loadInitial(),
), ),
), ),
), ),
chatRoom.when( chatRoom.when(
data: data: (room) => Column(
(room) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
AnimatedSwitcher( AnimatedSwitcher(
@@ -1146,8 +1126,7 @@ class ChatRoomScreen extends HookConsumerWidget {
), ),
); );
}, },
child: child: typingStatuses.value.isNotEmpty
typingStatuses.value.isNotEmpty
? Container( ? Container(
key: const ValueKey('typing-indicator'), key: const ValueKey('typing-indicator'),
width: double.infinity, width: double.infinity,
@@ -1170,14 +1149,12 @@ class ChatRoomScreen extends HookConsumerWidget {
typingStatuses.value typingStatuses.value
.map( .map(
(x) => (x) =>
x.nick ?? x.nick ?? x.account.nick,
x.account.nick,
) )
.join(', '), .join(', '),
], ],
), ),
style: style: Theme.of(
Theme.of(
context, context,
).textTheme.bodySmall, ).textTheme.bodySmall,
), ),
@@ -1446,14 +1423,11 @@ class _ChatInput extends HookConsumerWidget {
// Insert placeholder at current cursor position // Insert placeholder at current cursor position
final text = messageController.text; final text = messageController.text;
final selection = messageController.selection; final selection = messageController.selection;
final start = final start = selection.start >= 0
selection.start >= 0
? selection.start ? selection.start
: text.length; : text.length;
final end = final end =
selection.end >= 0 selection.end >= 0 ? selection.end : text.length;
? selection.end
: text.length;
final newText = text.replaceRange( final newText = text.replaceRange(
start, start,
end, end,
@@ -1471,8 +1445,7 @@ class _ChatInput extends HookConsumerWidget {
), ),
PopupMenuButton( PopupMenuButton(
icon: const Icon(Symbols.photo_library), icon: const Icon(Symbols.photo_library),
itemBuilder: itemBuilder: (context) => [
(context) => [
PopupMenuItem( PopupMenuItem(
onTap: () => onPickFile(true), onTap: () => onPickFile(true),
child: Row( child: Row(
@@ -1543,8 +1516,8 @@ class _ChatInput extends HookConsumerWidget {
), ),
), ),
maxLines: null, maxLines: null,
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
), ),