💄 Optimize chat room
This commit is contained in:
@@ -837,5 +837,6 @@
|
|||||||
"pollAddOption": "Add option",
|
"pollAddOption": "Add option",
|
||||||
"pollOptionLabel": "Option label",
|
"pollOptionLabel": "Option label",
|
||||||
"pollLongTextAnswerPreview": "Long text answer (preview)",
|
"pollLongTextAnswerPreview": "Long text answer (preview)",
|
||||||
"pollShortTextAnswerPreview": "Short text answer (preview)"
|
"pollShortTextAnswerPreview": "Short text answer (preview)",
|
||||||
|
"messageJumpNotLoaded": "The referenced message was not loaded, unable to jump to it."
|
||||||
}
|
}
|
@@ -812,5 +812,6 @@
|
|||||||
"filesListAdditional": {
|
"filesListAdditional": {
|
||||||
"one": "+{} 个文件被折叠",
|
"one": "+{} 个文件被折叠",
|
||||||
"other": "+{} 个文件被折叠"
|
"other": "+{} 个文件被折叠"
|
||||||
}
|
},
|
||||||
|
"messageJumpNotLoaded": "引用的消息没有被加载,无法跳转。"
|
||||||
}
|
}
|
||||||
|
@@ -1,44 +1,45 @@
|
|||||||
import 'dart:async';
|
import "dart:async";
|
||||||
import 'dart:convert';
|
import "dart:convert";
|
||||||
import 'dart:developer' as developer;
|
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";
|
||||||
import 'package:flutter/foundation.dart';
|
import "package:flutter/foundation.dart";
|
||||||
import 'package:flutter/material.dart';
|
import "package:flutter/material.dart";
|
||||||
import 'package:go_router/go_router.dart';
|
import "package:go_router/go_router.dart";
|
||||||
import 'package:flutter/services.dart';
|
import "package:flutter/services.dart";
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
import 'package:gap/gap.dart';
|
import "package:gap/gap.dart";
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import 'package:image_picker/image_picker.dart';
|
import "package:image_picker/image_picker.dart";
|
||||||
import 'package:island/database/drift_db.dart';
|
import "package:island/database/drift_db.dart";
|
||||||
import 'package:island/database/message.dart';
|
import "package:island/database/message.dart";
|
||||||
import 'package:island/models/chat.dart';
|
import "package:island/models/chat.dart";
|
||||||
import 'package:island/models/file.dart';
|
import "package:island/models/file.dart";
|
||||||
import 'package:island/pods/config.dart';
|
import "package:island/pods/config.dart";
|
||||||
import 'package:island/pods/database.dart';
|
import "package:island/pods/database.dart";
|
||||||
import 'package:island/pods/network.dart';
|
import "package:island/pods/network.dart";
|
||||||
import 'package:island/pods/websocket.dart';
|
import "package:island/pods/websocket.dart";
|
||||||
import 'package:island/services/file.dart';
|
import "package:island/services/file.dart";
|
||||||
import 'package:island/services/responsive.dart';
|
import "package:island/services/responsive.dart";
|
||||||
import 'package:island/widgets/alert.dart';
|
import "package:island/widgets/alert.dart";
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import "package:island/widgets/app_scaffold.dart";
|
||||||
import 'package:island/widgets/chat/call_overlay.dart';
|
import "package:island/widgets/chat/call_overlay.dart";
|
||||||
import 'package:island/widgets/chat/message_item.dart';
|
import "package:island/widgets/chat/message_item.dart";
|
||||||
import 'package:island/widgets/content/attachment_preview.dart';
|
import "package:island/widgets/content/attachment_preview.dart";
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import "package:island/widgets/content/cloud_files.dart";
|
||||||
import 'package:island/widgets/response.dart';
|
import "package:island/widgets/response.dart";
|
||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
import "package:material_symbols_icons/material_symbols_icons.dart";
|
||||||
import 'package:pasteboard/pasteboard.dart';
|
import "package:pasteboard/pasteboard.dart";
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import "package:styled_widget/styled_widget.dart";
|
||||||
import 'package:super_sliver_list/super_sliver_list.dart';
|
import "package:super_sliver_list/super_sliver_list.dart";
|
||||||
import 'package:uuid/uuid.dart';
|
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import "package:uuid/uuid.dart";
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import "package:material_symbols_icons/symbols.dart";
|
||||||
import 'chat.dart';
|
import "package:riverpod_annotation/riverpod_annotation.dart";
|
||||||
import 'package:island/widgets/chat/call_button.dart';
|
import "chat.dart";
|
||||||
import 'package:island/widgets/stickers/picker.dart';
|
import "package:island/widgets/chat/call_button.dart";
|
||||||
|
import "package:island/widgets/stickers/picker.dart";
|
||||||
|
|
||||||
part 'room.g.dart';
|
part 'room.g.dart';
|
||||||
|
|
||||||
@@ -86,6 +87,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
int _currentPage = 0;
|
int _currentPage = 0;
|
||||||
static const int _pageSize = 20;
|
static const int _pageSize = 20;
|
||||||
bool _hasMore = true;
|
bool _hasMore = true;
|
||||||
|
bool _isSyncing = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<List<LocalChatMessage>> build(String roomId) async {
|
FutureOr<List<LocalChatMessage>> build(String roomId) async {
|
||||||
@@ -100,11 +102,17 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
_room = room;
|
_room = room;
|
||||||
_identity = identity;
|
_identity = identity;
|
||||||
|
|
||||||
developer.log('MessagesNotifier built for room $roomId', name: 'MessagesNotifier');
|
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');
|
developer.log(
|
||||||
|
'App resumed, syncing messages',
|
||||||
|
name: 'MessagesNotifier',
|
||||||
|
);
|
||||||
syncMessages();
|
syncMessages();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -116,7 +124,10 @@ 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');
|
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,
|
||||||
@@ -127,7 +138,9 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
|
|
||||||
if (offset == 0) {
|
if (offset == 0) {
|
||||||
final pendingForRoom =
|
final pendingForRoom =
|
||||||
_pendingMessages.values.where((msg) => msg.roomId == _roomId).toList();
|
_pendingMessages.values
|
||||||
|
.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));
|
||||||
@@ -149,7 +162,10 @@ 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');
|
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',
|
||||||
@@ -170,7 +186,8 @@ 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 = data.map((json) {
|
final messages =
|
||||||
|
data.map((json) {
|
||||||
final remoteMessage = SnChatMessage.fromJson(json);
|
final remoteMessage = SnChatMessage.fromJson(json);
|
||||||
return LocalChatMessage.fromRemoteMessage(
|
return LocalChatMessage.fromRemoteMessage(
|
||||||
remoteMessage,
|
remoteMessage,
|
||||||
@@ -191,6 +208,15 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> syncMessages() async {
|
Future<void> syncMessages() async {
|
||||||
|
if (_isSyncing) {
|
||||||
|
developer.log(
|
||||||
|
'Sync already in progress, skipping.',
|
||||||
|
name: 'MessagesNotifier',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_isSyncing = true;
|
||||||
|
|
||||||
developer.log('Starting message sync', name: 'MessagesNotifier');
|
developer.log('Starting message sync', name: 'MessagesNotifier');
|
||||||
ref.read(isSyncingProvider.notifier).state = true;
|
ref.read(isSyncingProvider.notifier).state = true;
|
||||||
try {
|
try {
|
||||||
@@ -200,11 +226,19 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
limit: 1,
|
limit: 1,
|
||||||
);
|
);
|
||||||
final lastMessage =
|
final lastMessage =
|
||||||
dbMessages.isEmpty ? null : _database.companionToMessage(dbMessages.first);
|
dbMessages.isEmpty
|
||||||
|
? null
|
||||||
|
: _database.companionToMessage(dbMessages.first);
|
||||||
|
|
||||||
if (lastMessage == null) {
|
if (lastMessage == null) {
|
||||||
developer.log('No local messages, fetching from network', name: 'MessagesNotifier');
|
developer.log(
|
||||||
final newMessages = await _fetchAndCacheMessages(offset: 0, take: _pageSize);
|
'No local messages, fetching from network',
|
||||||
|
name: 'MessagesNotifier',
|
||||||
|
);
|
||||||
|
final newMessages = await _fetchAndCacheMessages(
|
||||||
|
offset: 0,
|
||||||
|
take: _pageSize,
|
||||||
|
);
|
||||||
state = AsyncValue.data(newMessages);
|
state = AsyncValue.data(newMessages);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -218,7 +252,10 @@ 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');
|
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:
|
||||||
@@ -233,11 +270,17 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err, stackTrace) {
|
} catch (err, stackTrace) {
|
||||||
developer.log('Error syncing messages', name: 'MessagesNotifier', error: err, stackTrace: stackTrace);
|
developer.log(
|
||||||
|
'Error syncing messages',
|
||||||
|
name: 'MessagesNotifier',
|
||||||
|
error: err,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
showErrorAlert(err);
|
showErrorAlert(err);
|
||||||
} finally {
|
} finally {
|
||||||
developer.log('Finished message sync', name: 'MessagesNotifier');
|
developer.log('Finished message sync', name: 'MessagesNotifier');
|
||||||
ref.read(isSyncingProvider.notifier).state = false;
|
ref.read(isSyncingProvider.notifier).state = false;
|
||||||
|
_isSyncing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,7 +322,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
Future<List<LocalChatMessage>> loadInitial() async {
|
Future<List<LocalChatMessage>> loadInitial() async {
|
||||||
developer.log('Loading initial messages', name: 'MessagesNotifier');
|
developer.log('Loading initial messages', name: 'MessagesNotifier');
|
||||||
syncMessages();
|
syncMessages();
|
||||||
final messages = await _getCachedMessages(offset: 0, take: _pageSize);
|
final messages = await _getCachedMessages(offset: 0, take: 100);
|
||||||
_currentPage = 0;
|
_currentPage = 0;
|
||||||
_hasMore = messages.length == _pageSize;
|
_hasMore = messages.length == _pageSize;
|
||||||
return messages;
|
return messages;
|
||||||
@@ -303,7 +346,12 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
|
|
||||||
state = AsyncValue.data([...currentMessages, ...newMessages]);
|
state = AsyncValue.data([...currentMessages, ...newMessages]);
|
||||||
} catch (err, stackTrace) {
|
} catch (err, stackTrace) {
|
||||||
developer.log('Error loading more messages', name: 'MessagesNotifier', error: err, stackTrace: stackTrace);
|
developer.log(
|
||||||
|
'Error loading more messages',
|
||||||
|
name: 'MessagesNotifier',
|
||||||
|
error: err,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
showErrorAlert(err);
|
showErrorAlert(err);
|
||||||
_currentPage--;
|
_currentPage--;
|
||||||
}
|
}
|
||||||
@@ -318,7 +366,10 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
Function(String, Map<int, double>)? onProgress,
|
Function(String, Map<int, double>)? onProgress,
|
||||||
}) async {
|
}) async {
|
||||||
final nonce = const Uuid().v4();
|
final nonce = const Uuid().v4();
|
||||||
developer.log('Sending message with nonce $nonce', name: 'MessagesNotifier');
|
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');
|
||||||
@@ -349,12 +400,14 @@ 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 = await putMediaToCloud(
|
final cloudFile =
|
||||||
|
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: attachments[idx].data.mimeType ??
|
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',
|
||||||
@@ -400,23 +453,33 @@ 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 = (state.value ?? []).map((m) {
|
final newMessages =
|
||||||
|
(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);
|
||||||
developer.log('Message with nonce $nonce sent successfully', name: 'MessagesNotifier');
|
developer.log(
|
||||||
|
'Message with nonce $nonce sent successfully',
|
||||||
|
name: 'MessagesNotifier',
|
||||||
|
);
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
developer.log('Failed to send message with nonce $nonce', name: 'MessagesNotifier', error: e, stackTrace: 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 = (state.value ?? []).map((m) {
|
final newMessages =
|
||||||
|
(state.value ?? []).map((m) {
|
||||||
if (m.id == localMessage.id) {
|
if (m.id == localMessage.id) {
|
||||||
return m..status = MessageStatus.failed;
|
return m..status = MessageStatus.failed;
|
||||||
}
|
}
|
||||||
@@ -428,7 +491,10 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> retryMessage(String pendingMessageId) async {
|
Future<void> retryMessage(String pendingMessageId) async {
|
||||||
developer.log('Retrying message $pendingMessageId', name: 'MessagesNotifier');
|
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');
|
||||||
@@ -463,7 +529,8 @@ 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 = (state.value ?? []).map((m) {
|
final newMessages =
|
||||||
|
(state.value ?? []).map((m) {
|
||||||
if (m.id == pendingMessageId) {
|
if (m.id == pendingMessageId) {
|
||||||
return updatedMessage;
|
return updatedMessage;
|
||||||
}
|
}
|
||||||
@@ -471,14 +538,20 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
}).toList();
|
}).toList();
|
||||||
state = AsyncValue.data(newMessages);
|
state = AsyncValue.data(newMessages);
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
developer.log('Failed to retry message $pendingMessageId', name: 'MessagesNotifier', error: e, stackTrace: 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 = (state.value ?? []).map((m) {
|
final newMessages =
|
||||||
|
(state.value ?? []).map((m) {
|
||||||
if (m.id == pendingMessageId) {
|
if (m.id == pendingMessageId) {
|
||||||
return m..status = MessageStatus.failed;
|
return m..status = MessageStatus.failed;
|
||||||
}
|
}
|
||||||
@@ -491,7 +564,10 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
|
|
||||||
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');
|
developer.log(
|
||||||
|
'Received new message ${remoteMessage.id}',
|
||||||
|
name: 'MessagesNotifier',
|
||||||
|
);
|
||||||
|
|
||||||
final localMessage = LocalChatMessage.fromRemoteMessage(
|
final localMessage = LocalChatMessage.fromRemoteMessage(
|
||||||
remoteMessage,
|
remoteMessage,
|
||||||
@@ -524,7 +600,10 @@ 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');
|
developer.log(
|
||||||
|
'Received message update ${remoteMessage.id}',
|
||||||
|
name: 'MessagesNotifier',
|
||||||
|
);
|
||||||
|
|
||||||
final updatedMessage = LocalChatMessage.fromRemoteMessage(
|
final updatedMessage = LocalChatMessage.fromRemoteMessage(
|
||||||
remoteMessage,
|
remoteMessage,
|
||||||
@@ -533,9 +612,7 @@ 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(
|
final index = currentMessages.indexWhere((m) => m.id == updatedMessage.id);
|
||||||
(m) => m.id == updatedMessage.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
final newList = [...currentMessages];
|
final newList = [...currentMessages];
|
||||||
@@ -545,7 +622,10 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> receiveMessageDeletion(String messageId) async {
|
Future<void> receiveMessageDeletion(String messageId) async {
|
||||||
developer.log('Received message deletion $messageId', name: 'MessagesNotifier');
|
developer.log(
|
||||||
|
'Received message deletion $messageId',
|
||||||
|
name: 'MessagesNotifier',
|
||||||
|
);
|
||||||
_pendingMessages.remove(messageId);
|
_pendingMessages.remove(messageId);
|
||||||
await _database.deleteMessage(messageId);
|
await _database.deleteMessage(messageId);
|
||||||
|
|
||||||
@@ -564,17 +644,25 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
await _apiClient.delete('/sphere/chat/$_roomId/messages/$messageId');
|
await _apiClient.delete('/sphere/chat/$_roomId/messages/$messageId');
|
||||||
await receiveMessageDeletion(messageId);
|
await receiveMessageDeletion(messageId);
|
||||||
} catch (err, stackTrace) {
|
} catch (err, stackTrace) {
|
||||||
developer.log('Error deleting message $messageId', name: 'MessagesNotifier', error: err, stackTrace: 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');
|
developer.log(
|
||||||
|
'Fetching message by id $messageId',
|
||||||
|
name: 'MessagesNotifier',
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
final localMessage = await (_database.select(_database.chatMessages)
|
final localMessage =
|
||||||
..where((tbl) => tbl.id.equals(messageId)))
|
await (_database.select(_database.chatMessages)
|
||||||
.getSingleOrNull();
|
..where((tbl) => tbl.id.equals(messageId))).getSingleOrNull();
|
||||||
if (localMessage != null) {
|
if (localMessage != null) {
|
||||||
return _database.companionToMessage(localMessage);
|
return _database.companionToMessage(localMessage);
|
||||||
}
|
}
|
||||||
@@ -617,7 +705,8 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(leading: const PageBackButton()),
|
appBar: AppBar(leading: const PageBackButton()),
|
||||||
body: Center(
|
body: Center(
|
||||||
child: ConstrainedBox(
|
child:
|
||||||
|
ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 280),
|
constraints: const BoxConstraints(maxWidth: 280),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
@@ -726,8 +815,10 @@ 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.where((member) {
|
typingStatuses.value =
|
||||||
final lastTyped = member.lastTyped ??
|
typingStatuses.value.where((member) {
|
||||||
|
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();
|
||||||
@@ -895,48 +986,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
final listController = useMemoized(() => ListController(), []);
|
final listController = useMemoized(() => ListController(), []);
|
||||||
|
|
||||||
return AppScaffold(
|
Widget comfortHeaderWidget(SnChatRoom? room) => Column(
|
||||||
appBar: AppBar(
|
|
||||||
leading: !compactHeader ? const Center(child: PageBackButton()) : null,
|
|
||||||
automaticallyImplyLeading: false,
|
|
||||||
toolbarHeight: compactHeader ? null : 64,
|
|
||||||
title: chatRoom.when(
|
|
||||||
data: (room) => compactHeader
|
|
||||||
? Row(
|
|
||||||
spacing: 8,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
height: 26,
|
|
||||||
width: 26,
|
|
||||||
child: (room!.type == 1 && room.picture?.id == null)
|
|
||||||
? SplitAvatarWidget(
|
|
||||||
filesId: room.members!
|
|
||||||
.map(
|
|
||||||
(e) => e.account.profile.picture?.id,
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
)
|
|
||||||
: room.picture?.id != null
|
|
||||||
? ProfilePictureWidget(
|
|
||||||
fileId: room.picture?.id,
|
|
||||||
fallbackIcon: Symbols.chat,
|
|
||||||
)
|
|
||||||
: CircleAvatar(
|
|
||||||
child: Text(
|
|
||||||
room.name![0].toUpperCase(),
|
|
||||||
style: const TextStyle(fontSize: 12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
(room.type == 1 && room.name == null)
|
|
||||||
? room.members!.map((e) => e.account.nick).join(', ')
|
|
||||||
: room.name!,
|
|
||||||
).fontSize(19),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: Column(
|
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
@@ -944,12 +994,12 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
height: 26,
|
height: 26,
|
||||||
width: 26,
|
width: 26,
|
||||||
child: (room!.type == 1 && room.picture?.id == null)
|
child:
|
||||||
|
(room!.type == 1 && room.picture?.id == null)
|
||||||
? SplitAvatarWidget(
|
? SplitAvatarWidget(
|
||||||
filesId: room.members!
|
filesId:
|
||||||
.map(
|
room.members!
|
||||||
(e) => e.account.profile.picture?.id,
|
.map((e) => e.account.profile.picture?.id)
|
||||||
)
|
|
||||||
.toList(),
|
.toList(),
|
||||||
)
|
)
|
||||||
: room.picture?.id != null
|
: room.picture?.id != null
|
||||||
@@ -970,9 +1020,142 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
: room.name!,
|
: room.name!,
|
||||||
).fontSize(15),
|
).fontSize(15),
|
||||||
],
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget compactHeaderWidget(SnChatRoom? room) => Row(
|
||||||
|
spacing: 8,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 26,
|
||||||
|
width: 26,
|
||||||
|
child:
|
||||||
|
(room!.type == 1 && room.picture?.id == null)
|
||||||
|
? SplitAvatarWidget(
|
||||||
|
filesId:
|
||||||
|
room.members!
|
||||||
|
.map((e) => e.account.profile.picture?.id)
|
||||||
|
.toList(),
|
||||||
|
)
|
||||||
|
: room.picture?.id != null
|
||||||
|
? ProfilePictureWidget(
|
||||||
|
fileId: room.picture?.id,
|
||||||
|
fallbackIcon: Symbols.chat,
|
||||||
|
)
|
||||||
|
: CircleAvatar(
|
||||||
|
child: Text(
|
||||||
|
room.name![0].toUpperCase(),
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
(room.type == 1 && room.name == null)
|
||||||
|
? room.members!.map((e) => e.account.nick).join(', ')
|
||||||
|
: room.name!,
|
||||||
|
).fontSize(19),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget chatMessageListWidget(List<LocalChatMessage> messageList) =>
|
||||||
|
SuperListView.builder(
|
||||||
|
listController: listController,
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 16),
|
||||||
|
controller: scrollController,
|
||||||
|
reverse: true, // Show newest messages at the bottom
|
||||||
|
itemCount: messageList.length,
|
||||||
|
findChildIndexCallback: (key) {
|
||||||
|
final valueKey = key as ValueKey;
|
||||||
|
final messageId = valueKey.value as String;
|
||||||
|
return messageList.indexWhere((m) => m.id == messageId);
|
||||||
|
},
|
||||||
|
extentEstimation: (_, _) => 40,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final message = messageList[index];
|
||||||
|
final nextMessage =
|
||||||
|
index < messageList.length - 1 ? messageList[index + 1] : null;
|
||||||
|
final isLastInGroup =
|
||||||
|
nextMessage == null ||
|
||||||
|
nextMessage.senderId != message.senderId ||
|
||||||
|
nextMessage.createdAt
|
||||||
|
.difference(message.createdAt)
|
||||||
|
.inMinutes
|
||||||
|
.abs() >
|
||||||
|
3;
|
||||||
|
|
||||||
|
return chatIdentity.when(
|
||||||
|
skipError: true,
|
||||||
|
data:
|
||||||
|
(identity) => MessageItem(
|
||||||
|
key: ValueKey(message.id),
|
||||||
|
message: message,
|
||||||
|
isCurrentUser: identity?.id == message.senderId,
|
||||||
|
onAction: (action) {
|
||||||
|
switch (action) {
|
||||||
|
case MessageItemAction.delete:
|
||||||
|
messagesNotifier.deleteMessage(message.id);
|
||||||
|
case MessageItemAction.edit:
|
||||||
|
messageEditingTo.value = message.toRemoteMessage();
|
||||||
|
messageController.text =
|
||||||
|
messageEditingTo.value?.content ?? '';
|
||||||
|
attachments.value =
|
||||||
|
messageEditingTo.value!.attachments
|
||||||
|
.map((e) => UniversalFile.fromAttachment(e))
|
||||||
|
.toList();
|
||||||
|
case MessageItemAction.forward:
|
||||||
|
messageForwardingTo.value = message.toRemoteMessage();
|
||||||
|
case MessageItemAction.reply:
|
||||||
|
messageReplyingTo.value = message.toRemoteMessage();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onJump: (messageId) {
|
||||||
|
final messageIndex = messageList.indexWhere(
|
||||||
|
(m) => m.id == messageId,
|
||||||
|
);
|
||||||
|
if (messageIndex == -1) {
|
||||||
|
showSnackBar('messageJumpNotLoaded'.tr());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
listController.animateToItem(
|
||||||
|
index: messageIndex,
|
||||||
|
scrollController: scrollController,
|
||||||
|
alignment: 0.5,
|
||||||
|
duration:
|
||||||
|
(estimatedDistance) => Duration(milliseconds: 250),
|
||||||
|
curve: (estimatedDistance) => Curves.easeInOut,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
progress: attachmentProgress.value[message.id],
|
||||||
|
showAvatar: isLastInGroup,
|
||||||
|
),
|
||||||
|
loading:
|
||||||
|
() => MessageItem(
|
||||||
|
message: message,
|
||||||
|
isCurrentUser: false,
|
||||||
|
onAction: null,
|
||||||
|
progress: null,
|
||||||
|
showAvatar: false,
|
||||||
|
onJump: (_) {},
|
||||||
|
),
|
||||||
|
error: (_, _) => const SizedBox.shrink(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return AppScaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: !compactHeader ? const Center(child: PageBackButton()) : null,
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
toolbarHeight: compactHeader ? null : 64,
|
||||||
|
title: chatRoom.when(
|
||||||
|
data:
|
||||||
|
(room) =>
|
||||||
|
compactHeader
|
||||||
|
? compactHeaderWidget(room)
|
||||||
|
: comfortHeaderWidget(room),
|
||||||
loading: () => const Text('Loading...'),
|
loading: () => const Text('Loading...'),
|
||||||
error: (err, _) => ResponseErrorWidget(
|
error:
|
||||||
|
(err, _) => ResponseErrorWidget(
|
||||||
error: err,
|
error: err,
|
||||||
onRetry: () => messagesNotifier.loadInitial(),
|
onRetry: () => messagesNotifier.loadInitial(),
|
||||||
),
|
),
|
||||||
@@ -987,10 +1170,13 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
],
|
],
|
||||||
bottom: isSyncing
|
bottom:
|
||||||
|
isSyncing
|
||||||
? const PreferredSize(
|
? const PreferredSize(
|
||||||
preferredSize: Size.fromHeight(4.0),
|
preferredSize: Size.fromHeight(2),
|
||||||
child: LinearProgressIndicator(),
|
child: LinearProgressIndicator(
|
||||||
|
borderRadius: BorderRadius.zero,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@@ -1000,102 +1186,23 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: messages.when(
|
child: messages.when(
|
||||||
data: (messageList) => messageList.isEmpty
|
data:
|
||||||
|
(messageList) =>
|
||||||
|
messageList.isEmpty
|
||||||
? Center(child: Text('No messages yet'.tr()))
|
? Center(child: Text('No messages yet'.tr()))
|
||||||
: SuperListView.builder(
|
: chatMessageListWidget(messageList),
|
||||||
listController: listController,
|
loading:
|
||||||
padding: EdgeInsets.symmetric(vertical: 16),
|
() => const Center(child: CircularProgressIndicator()),
|
||||||
controller: scrollController,
|
error:
|
||||||
reverse: true, // Show newest messages at the bottom
|
(error, _) => ResponseErrorWidget(
|
||||||
itemCount: messageList.length,
|
|
||||||
findChildIndexCallback: (key) {
|
|
||||||
final valueKey = key as ValueKey;
|
|
||||||
final messageId = valueKey.value as String;
|
|
||||||
return messageList.indexWhere(
|
|
||||||
(m) => m.id == messageId,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final message = messageList[index];
|
|
||||||
final nextMessage = index < messageList.length - 1
|
|
||||||
? messageList[index + 1]
|
|
||||||
: null;
|
|
||||||
final isLastInGroup = nextMessage == null ||
|
|
||||||
nextMessage.senderId != message.senderId ||
|
|
||||||
nextMessage.createdAt
|
|
||||||
.difference(message.createdAt)
|
|
||||||
.inMinutes
|
|
||||||
.abs() >
|
|
||||||
3;
|
|
||||||
|
|
||||||
return chatIdentity.when(
|
|
||||||
skipError: true,
|
|
||||||
data: (identity) => MessageItem(
|
|
||||||
key: ValueKey(message.id),
|
|
||||||
message: message,
|
|
||||||
isCurrentUser: identity?.id == message.senderId,
|
|
||||||
onAction: (action) {
|
|
||||||
switch (action) {
|
|
||||||
case MessageItemAction.delete:
|
|
||||||
messagesNotifier.deleteMessage(
|
|
||||||
message.id,
|
|
||||||
);
|
|
||||||
case MessageItemAction.edit:
|
|
||||||
messageEditingTo.value =
|
|
||||||
message.toRemoteMessage();
|
|
||||||
messageController.text =
|
|
||||||
messageEditingTo.value?.content ?? '';
|
|
||||||
attachments.value = messageEditingTo
|
|
||||||
.value!.attachments
|
|
||||||
.map(
|
|
||||||
(e) => UniversalFile.fromAttachment(
|
|
||||||
e,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
case MessageItemAction.forward:
|
|
||||||
messageForwardingTo.value =
|
|
||||||
message.toRemoteMessage();
|
|
||||||
case MessageItemAction.reply:
|
|
||||||
messageReplyingTo.value =
|
|
||||||
message.toRemoteMessage();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onJump: (messageId) {
|
|
||||||
final messageIndex = messageList.indexWhere(
|
|
||||||
(m) => m.id == messageId,
|
|
||||||
);
|
|
||||||
listController.jumpToItem(
|
|
||||||
index: messageIndex,
|
|
||||||
scrollController:
|
|
||||||
scrollController,
|
|
||||||
alignment: 0.5,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
progress: attachmentProgress.value[message.id],
|
|
||||||
showAvatar: isLastInGroup,
|
|
||||||
),
|
|
||||||
loading: () => MessageItem(
|
|
||||||
message: message,
|
|
||||||
isCurrentUser: false,
|
|
||||||
onAction: null,
|
|
||||||
progress: null,
|
|
||||||
showAvatar: false,
|
|
||||||
onJump: (_) {},
|
|
||||||
),
|
|
||||||
error: (_, _) => const SizedBox.shrink(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
|
||||||
error: (error, _) => ResponseErrorWidget(
|
|
||||||
error: error,
|
error: error,
|
||||||
onRetry: () => messagesNotifier.loadInitial(),
|
onRetry: () => messagesNotifier.loadInitial(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
chatRoom.when(
|
chatRoom.when(
|
||||||
data: (room) => Column(
|
data:
|
||||||
|
(room) => Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
AnimatedSwitcher(
|
AnimatedSwitcher(
|
||||||
@@ -1126,7 +1233,8 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: typingStatuses.value.isNotEmpty
|
child:
|
||||||
|
typingStatuses.value.isNotEmpty
|
||||||
? Container(
|
? Container(
|
||||||
key: const ValueKey('typing-indicator'),
|
key: const ValueKey('typing-indicator'),
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@@ -1149,12 +1257,14 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
typingStatuses.value
|
typingStatuses.value
|
||||||
.map(
|
.map(
|
||||||
(x) =>
|
(x) =>
|
||||||
x.nick ?? x.account.nick,
|
x.nick ??
|
||||||
|
x.account.nick,
|
||||||
)
|
)
|
||||||
.join(', '),
|
.join(', '),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
style: Theme.of(
|
style:
|
||||||
|
Theme.of(
|
||||||
context,
|
context,
|
||||||
).textTheme.bodySmall,
|
).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
@@ -1423,11 +1533,14 @@ 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 = selection.start >= 0
|
final start =
|
||||||
|
selection.start >= 0
|
||||||
? selection.start
|
? selection.start
|
||||||
: text.length;
|
: text.length;
|
||||||
final end =
|
final end =
|
||||||
selection.end >= 0 ? selection.end : text.length;
|
selection.end >= 0
|
||||||
|
? selection.end
|
||||||
|
: text.length;
|
||||||
final newText = text.replaceRange(
|
final newText = text.replaceRange(
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
@@ -1445,7 +1558,8 @@ class _ChatInput extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
icon: const Icon(Symbols.photo_library),
|
icon: const Icon(Symbols.photo_library),
|
||||||
itemBuilder: (context) => [
|
itemBuilder:
|
||||||
|
(context) => [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
onTap: () => onPickFile(true),
|
onTap: () => onPickFile(true),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -1516,8 +1630,8 @@ class _ChatInput extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
onTapOutside: (_) =>
|
onTapOutside:
|
||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
28
pubspec.lock
28
pubspec.lock
@@ -1233,50 +1233,50 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: image_picker
|
name: image_picker
|
||||||
sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a"
|
sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.2.0"
|
||||||
image_picker_android:
|
image_picker_android:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: image_picker_android
|
name: image_picker_android
|
||||||
sha256: b08e9a04d0f8d91f4a6e767a745b9871bfbc585410205c311d0492de20a7ccd6
|
sha256: e83b2b05141469c5e19d77e1dfa11096b6b1567d09065b2265d7c6904560050c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.12+25"
|
version: "0.8.13"
|
||||||
image_picker_for_web:
|
image_picker_for_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_for_web
|
name: image_picker_for_web
|
||||||
sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83"
|
sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.6"
|
version: "3.1.0"
|
||||||
image_picker_ios:
|
image_picker_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_ios
|
name: image_picker_ios
|
||||||
sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100"
|
sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.12+2"
|
version: "0.8.13"
|
||||||
image_picker_linux:
|
image_picker_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_linux
|
name: image_picker_linux
|
||||||
sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9"
|
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.1+2"
|
version: "0.2.2"
|
||||||
image_picker_macos:
|
image_picker_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_macos
|
name: image_picker_macos
|
||||||
sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1"
|
sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.1+2"
|
version: "0.2.2"
|
||||||
image_picker_platform_interface:
|
image_picker_platform_interface:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1289,10 +1289,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_windows
|
name: image_picker_windows
|
||||||
sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
|
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.1+1"
|
version: "0.2.2"
|
||||||
intl:
|
intl:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@@ -72,11 +72,11 @@ dependencies:
|
|||||||
tus_client_dart:
|
tus_client_dart:
|
||||||
git: https://github.com/LittleSheep2Code/tus_client.git
|
git: https://github.com/LittleSheep2Code/tus_client.git
|
||||||
cross_file: ^0.3.4+2
|
cross_file: ^0.3.4+2
|
||||||
image_picker: ^1.1.2
|
image_picker: ^1.2.0
|
||||||
file_picker: ^10.3.1
|
file_picker: ^10.3.1
|
||||||
riverpod_annotation: ^2.6.1
|
riverpod_annotation: ^2.6.1
|
||||||
image_picker_platform_interface: ^2.11.0
|
image_picker_platform_interface: ^2.11.0
|
||||||
image_picker_android: ^0.8.12+25
|
image_picker_android: ^0.8.13
|
||||||
super_context_menu: ^0.9.1
|
super_context_menu: ^0.9.1
|
||||||
modal_bottom_sheet: ^3.0.0
|
modal_bottom_sheet: ^3.0.0
|
||||||
firebase_messaging: ^16.0.0
|
firebase_messaging: ^16.0.0
|
||||||
|
Reference in New Issue
Block a user