👽 Changes to API path since sphere no longer handle messages

This commit is contained in:
2026-01-01 23:59:21 +08:00
parent 0a4e797eec
commit 4d92dec45c
20 changed files with 644 additions and 712 deletions

View File

@@ -245,7 +245,7 @@ class CallNotifier extends _$CallNotifier {
final apiClient = ref.read(apiClientProvider);
final ongoingCall = await ref.read(ongoingCallProvider(roomId).future);
final response = await apiClient.get(
'/sphere/chat/realtime/$roomId/join',
'/messager/chat/realtime/$roomId/join',
);
if (response.statusCode == 200 && response.data != null) {
final data = response.data;

View File

@@ -15,7 +15,7 @@ class ChatOnlineCountNotifier extends _$ChatOnlineCountNotifier {
// Fetch initial online count
final response = await apiClient.get(
'/sphere/chat/$chatroomId/members/online',
'/messager/chat/$chatroomId/members/online',
);
final initialCount = response.data as int;

View File

@@ -105,7 +105,7 @@ class ChatRoomJoinedNotifier extends _$ChatRoomJoinedNotifier {
Future(() async {
try {
final client = ref.read(apiClientProvider);
final resp = await client.get('/sphere/chat');
final resp = await client.get('/messager/chat');
final remoteRooms = resp.data
.map((e) => SnChatRoom.fromJson(e))
.cast<SnChatRoom>()
@@ -122,7 +122,7 @@ class ChatRoomJoinedNotifier extends _$ChatRoomJoinedNotifier {
// Fallback to API
final client = ref.watch(apiClientProvider);
final resp = await client.get('/sphere/chat');
final resp = await client.get('/messager/chat');
final rooms = resp.data
.map((e) => SnChatRoom.fromJson(e))
.cast<SnChatRoom>()
@@ -306,7 +306,7 @@ class ChatRoomNotifier extends _$ChatRoomNotifier {
Future(() async {
try {
final client = ref.read(apiClientProvider);
final resp = await client.get('/sphere/chat/$identifier');
final resp = await client.get('/messager/chat/$identifier');
final remoteRoom = SnChatRoom.fromJson(resp.data);
// Update state with fresh data directly without saving to DB
// DB will be updated by ChatRoomJoinedNotifier's full sync
@@ -321,7 +321,7 @@ class ChatRoomNotifier extends _$ChatRoomNotifier {
// Fallback to API
try {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/sphere/chat/$identifier');
final resp = await client.get('/messager/chat/$identifier');
final room = SnChatRoom.fromJson(resp.data);
await db.saveChatRooms([room]);
return room;
@@ -375,7 +375,7 @@ class ChatRoomIdentityNotifier extends _$ChatRoomIdentityNotifier {
try {
final client = ref.read(apiClientProvider);
final resp = await client.get(
'/sphere/chat/$identifier/members/me',
'/messager/chat/$identifier/members/me',
);
final remoteMember = SnChatMember.fromJson(resp.data);
await db.saveMember(remoteMember);
@@ -396,7 +396,7 @@ class ChatRoomIdentityNotifier extends _$ChatRoomIdentityNotifier {
// Fallback to API
try {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/sphere/chat/$identifier/members/me');
final resp = await client.get('/messager/chat/$identifier/members/me');
final member = SnChatMember.fromJson(resp.data);
await db.saveMember(member);
return member;
@@ -444,7 +444,7 @@ class ChatRoomIdentityNotifier extends _$ChatRoomIdentityNotifier {
@riverpod
Future<List<SnChatMember>> chatroomInvites(Ref ref) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/sphere/chat/invites');
final resp = await client.get('/messager/chat/invites');
return resp.data
.map((e) => SnChatMember.fromJson(e))
.cast<SnChatMember>()

View File

@@ -61,7 +61,7 @@ class ChatSubscribeNotifier extends _$ChatSubscribeNotifier {
WebSocketPacket(
type: 'messages.subscribe',
data: {'chat_room_id': roomId},
endpoint: 'sphere',
endpoint: 'messager',
),
),
);
@@ -98,7 +98,7 @@ class ChatSubscribeNotifier extends _$ChatSubscribeNotifier {
WebSocketPacket(
type: 'messages.subscribe',
data: {'chat_room_id': roomId},
endpoint: 'sphere',
endpoint: 'messager',
),
),
);
@@ -116,7 +116,7 @@ class ChatSubscribeNotifier extends _$ChatSubscribeNotifier {
WebSocketPacket(
type: 'messages.unsubscribe',
data: {'chat_room_id': roomId},
endpoint: 'sphere',
endpoint: 'messager',
),
),
);
@@ -128,7 +128,7 @@ class ChatSubscribeNotifier extends _$ChatSubscribeNotifier {
WebSocketPacket(
type: 'messages.subscribe',
data: {'chat_room_id': roomId},
endpoint: 'sphere',
endpoint: 'messager',
),
),
);
@@ -143,7 +143,7 @@ class ChatSubscribeNotifier extends _$ChatSubscribeNotifier {
WebSocketPacket(
type: 'messages.unsubscribe',
data: {'chat_room_id': roomId},
endpoint: 'sphere',
endpoint: 'messager',
),
),
);
@@ -207,7 +207,7 @@ class ChatSubscribeNotifier extends _$ChatSubscribeNotifier {
WebSocketPacket(
type: 'messages.read',
data: {'chat_room_id': roomId},
endpoint: 'sphere',
endpoint: 'messager',
),
),
);
@@ -224,7 +224,7 @@ class ChatSubscribeNotifier extends _$ChatSubscribeNotifier {
WebSocketPacket(
type: 'messages.typing',
data: {'chat_room_id': roomId},
endpoint: 'sphere',
endpoint: 'messager',
),
),
);

View File

@@ -24,7 +24,7 @@ class ChatUnreadCountNotifier extends _$ChatUnreadCountNotifier {
try {
final client = ref.read(apiClientProvider);
final response = await client.get('/sphere/chat/unread');
final response = await client.get('/messager/chat/unread');
return (response.data as num).toInt();
} catch (_) {
return 0;
@@ -65,7 +65,7 @@ class ChatSummary extends _$ChatSummary {
@override
Future<Map<String, SnChatSummary>> build() async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/sphere/chat/summary');
final resp = await client.get('/messager/chat/summary');
final Map<String, dynamic> data = resp.data;
final summaries = data.map(

View File

@@ -281,7 +281,7 @@ class MessagesNotifier extends _$MessagesNotifier {
talker.log('Fetching messages from API, offset $offset, take $take');
if (_totalCount == null) {
final response = await _apiClient.get(
'/sphere/chat/$roomId/messages',
'/messager/chat/$roomId/messages',
queryParameters: {'offset': 0, 'take': 1},
);
_totalCount = int.parse(response.headers['x-total']?.firstOrNull ?? '0');
@@ -293,7 +293,7 @@ class MessagesNotifier extends _$MessagesNotifier {
}
final response = await _apiClient.get(
'/sphere/chat/$roomId/messages',
'/messager/chat/$roomId/messages',
queryParameters: {'offset': offset, 'take': take},
);
@@ -373,7 +373,7 @@ class MessagesNotifier extends _$MessagesNotifier {
do {
final resp = await _apiClient.post(
'/sphere/chat/${_room.id}/sync',
'/messager/chat/${_room.id}/sync',
data: {'last_sync_timestamp': lastSyncTimestamp},
);
@@ -603,8 +603,8 @@ class MessagesNotifier extends _$MessagesNotifier {
final response = await _apiClient.request(
editingTo == null
? '/sphere/chat/$roomId/messages'
: '/sphere/chat/$roomId/messages/${editingTo.id}',
? '/messager/chat/$roomId/messages'
: '/messager/chat/$roomId/messages/${editingTo.id}',
data: {
'content': content,
'attachments_id': cloudAttachments.map((e) => e.id).toList(),
@@ -688,7 +688,7 @@ class MessagesNotifier extends _$MessagesNotifier {
try {
var remoteMessage = message.toRemoteMessage();
final response = await _apiClient.post(
'/sphere/chat/${message.roomId}/messages',
'/messager/chat/${message.roomId}/messages',
data: {
'content': remoteMessage.content,
'attachments_id': remoteMessage.attachments,
@@ -925,7 +925,7 @@ class MessagesNotifier extends _$MessagesNotifier {
}
try {
await _apiClient.delete('/sphere/chat/$roomId/messages/$messageId');
await _apiClient.delete('/messager/chat/$roomId/messages/$messageId');
await receiveMessageDeletion(messageId);
} catch (err, stackTrace) {
talker.log(
@@ -1033,7 +1033,7 @@ class MessagesNotifier extends _$MessagesNotifier {
}
final response = await _apiClient.get(
'/sphere/chat/$roomId/messages/$messageId',
'/messager/chat/$roomId/messages/$messageId',
);
final remoteMessage = SnChatMessage.fromJson(response.data);
final message = LocalChatMessage.fromRemoteMessage(

View File

@@ -678,7 +678,7 @@ Future<SnChatRoom?> accountDirectChat(Ref ref, String uname) async {
final account = await ref.watch(accountProvider(uname).future);
final apiClient = ref.watch(apiClientProvider);
try {
final resp = await apiClient.get("/sphere/chat/direct/${account.id}");
final resp = await apiClient.get("/messager/chat/direct/${account.id}");
return SnChatRoom.fromJson(resp.data);
} catch (err) {
if (err is DioException && err.response?.statusCode == 404) {
@@ -807,7 +807,7 @@ class AccountProfileScreen extends HookConsumerWidget {
try {
final client = ref.watch(apiClientProvider);
final resp = await client.post(
'/sphere/chat/direct',
'/messager/chat/direct',
data: {'related_user_id': account.value!.id},
);
final chat = SnChatRoom.fromJson(resp.data);

View File

@@ -505,7 +505,7 @@ class ChatListScreen extends HookConsumerWidget {
final client = ref.read(apiClientProvider);
try {
await client.post(
'/sphere/chat/direct',
'/messager/chat/direct',
data: {'related_user_id': result.id},
);
eventBus.fire(const ChatRoomsRefreshEvent());
@@ -622,7 +622,9 @@ class _ChatInvitesSheet extends HookConsumerWidget {
Future<void> acceptInvite(SnChatMember invite) async {
try {
final client = ref.read(apiClientProvider);
await client.post('/sphere/chat/invites/${invite.chatRoom!.id}/accept');
await client.post(
'/messager/chat/invites/${invite.chatRoom!.id}/accept',
);
ref.invalidate(chatroomInvitesProvider);
ref.invalidate(chatRoomJoinedProvider);
} catch (err) {
@@ -634,7 +636,7 @@ class _ChatInvitesSheet extends HookConsumerWidget {
try {
final client = ref.read(apiClientProvider);
await client.post(
'/sphere/chat/invites/${invite.chatRoom!.id}/decline',
'/messager/chat/invites/${invite.chatRoom!.id}/decline',
);
ref.invalidate(chatroomInvitesProvider);
} catch (err) {

View File

@@ -97,14 +97,10 @@ class EditChatScreen extends HookConsumerWidget {
submitting.value = true;
try {
final cloudFile =
await FileUploader.createCloudFile(
ref: ref,
fileData: UniversalFile(
data: result,
type: UniversalFileType.image,
),
).future;
final cloudFile = await FileUploader.createCloudFile(
ref: ref,
fileData: UniversalFile(data: result, type: UniversalFileType.image),
).future;
if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...');
}
@@ -129,7 +125,7 @@ class EditChatScreen extends HookConsumerWidget {
try {
final client = ref.watch(apiClientProvider);
final resp = await client.request(
id == null ? '/sphere/chat' : '/sphere/chat/$id',
id == null ? '/messager/chat' : '/messager/chat/$id',
data: {
'name': nameController.text,
'description': descriptionController.text,
@@ -166,13 +162,12 @@ class EditChatScreen extends HookConsumerWidget {
GestureDetector(
child: Container(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child:
background.value != null
? CloudFileWidget(
item: background.value!,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
child: background.value != null
? CloudFileWidget(
item: background.value!,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
),
onTap: () {
setPicture('background');
@@ -208,8 +203,8 @@ class EditChatScreen extends HookConsumerWidget {
borderRadius: BorderRadius.circular(12),
),
),
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
TextFormField(
@@ -223,8 +218,8 @@ class EditChatScreen extends HookConsumerWidget {
),
minLines: 3,
maxLines: null,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
DropdownButtonFormField<SnRealm>(
@@ -241,22 +236,20 @@ class EditChatScreen extends HookConsumerWidget {
child: Text('none'.tr()),
),
...joinedRealms.maybeWhen(
data:
(realms) => realms.map(
(realm) => DropdownMenuItem(
value: realm,
child: Text(realm.name),
),
),
data: (realms) => realms.map(
(realm) => DropdownMenuItem(
value: realm,
child: Text(realm.name),
),
),
orElse: () => [],
),
],
onChanged:
joinedRealms.isLoading
? null
: (SnRealm? value) {
currentRealm.value = value;
},
onChanged: joinedRealms.isLoading
? null
: (SnRealm? value) {
currentRealm.value = value;
},
),
const SizedBox(height: 16),
Card(

View File

@@ -65,8 +65,9 @@ class PublicRoomPreview extends HookConsumerWidget {
extentEstimation: (_, _) => 40,
itemBuilder: (context, index) {
final message = messageList[index];
final nextMessage =
index < messageList.length - 1 ? messageList[index + 1] : null;
final nextMessage = index < messageList.length - 1
? messageList[index + 1]
: null;
final isLastInGroup =
nextMessage == null ||
nextMessage.senderId != message.senderId ||
@@ -97,25 +98,23 @@ class PublicRoomPreview extends HookConsumerWidget {
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),
),
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)
@@ -132,25 +131,23 @@ class PublicRoomPreview extends HookConsumerWidget {
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),
),
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)
@@ -181,17 +178,14 @@ class PublicRoomPreview extends HookConsumerWidget {
children: [
Expanded(
child: messages.when(
data:
(messageList) =>
messageList.isEmpty
? Center(child: Text('No messages yet'.tr()))
: chatMessageListWidget(messageList),
data: (messageList) => messageList.isEmpty
? Center(child: Text('No messages yet'.tr()))
: chatMessageListWidget(messageList),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, _) => ResponseErrorWidget(
error: error,
onRetry: () => messagesNotifier.loadInitial(),
),
error: (error, _) => ResponseErrorWidget(
error: error,
onRetry: () => messagesNotifier.loadInitial(),
),
),
),
// Join button at the bottom for public rooms
@@ -202,7 +196,7 @@ class PublicRoomPreview extends HookConsumerWidget {
try {
showLoadingModal(context);
final apiClient = ref.read(apiClientProvider);
await apiClient.post('/sphere/chat/${room.id}/members/me');
await apiClient.post('/messager/chat/${room.id}/members/me');
ref.invalidate(chatRoomIdentityProvider(id));
} catch (err) {
showErrorAlert(err);

View File

@@ -71,67 +71,64 @@ class ChatRoomScreen extends HookConsumerWidget {
return AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: Center(
child:
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 280),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
room.isCommunity == true
? Symbols.person_add
: Symbols.person_remove,
size: 36,
fill: 1,
).padding(bottom: 4),
Text('chatNotJoined').tr(),
if (room.isCommunity != true)
Text(
'chatUnableJoin',
textAlign: TextAlign.center,
).tr().bold()
else
FilledButton.tonalIcon(
onPressed: () async {
try {
showLoadingModal(context);
final apiClient = ref.read(apiClientProvider);
await apiClient.post(
'/sphere/chat/${room.id}/members/me',
);
ref.invalidate(chatRoomIdentityProvider(id));
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) {
hideLoadingModal(context);
}
}
},
label: Text('chatJoin').tr(),
icon: const Icon(Icons.add),
).padding(top: 8),
],
),
).center(),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 280),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
room.isCommunity == true
? Symbols.person_add
: Symbols.person_remove,
size: 36,
fill: 1,
).padding(bottom: 4),
Text('chatNotJoined').tr(),
if (room.isCommunity != true)
Text(
'chatUnableJoin',
textAlign: TextAlign.center,
).tr().bold()
else
FilledButton.tonalIcon(
onPressed: () async {
try {
showLoadingModal(context);
final apiClient = ref.read(apiClientProvider);
await apiClient.post(
'/messager/chat/${room.id}/members/me',
);
ref.invalidate(chatRoomIdentityProvider(id));
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) {
hideLoadingModal(context);
}
}
},
label: Text('chatJoin').tr(),
icon: const Icon(Icons.add),
).padding(top: 8),
],
),
).center(),
),
);
}
},
loading:
() => AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: CircularProgressIndicator().center(),
),
error:
(error, _) => AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: ResponseErrorWidget(
error: error,
onRetry: () => ref.refresh(chatRoomProvider(id)),
),
),
loading: () => AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: CircularProgressIndicator().center(),
),
error: (error, _) => AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: ResponseErrorWidget(
error: error,
onRetry: () => ref.refresh(chatRoomProvider(id)),
),
),
);
}
@@ -186,10 +183,9 @@ class ChatRoomScreen extends HookConsumerWidget {
case MessageItemAction.edit:
messageEditingTo.value = message.toRemoteMessage();
messageController.text = messageEditingTo.value?.content ?? '';
attachments.value =
messageEditingTo.value!.attachments
.map((e) => UniversalFile.fromAttachment(e))
.toList();
attachments.value = messageEditingTo.value!.attachments
.map((e) => UniversalFile.fromAttachment(e))
.toList();
case MessageItemAction.forward:
messageForwardingTo.value = message.toRemoteMessage();
case MessageItemAction.reply:
@@ -424,38 +420,37 @@ class ChatRoomScreen extends HookConsumerWidget {
label: Text('${(onlineCount.value ?? 0)}'),
textStyle: GoogleFonts.robotoMono(fontSize: 10),
textColor: Colors.white,
backgroundColor:
(onlineCount.value ?? 0) > 1 ? Colors.green : Colors.grey,
backgroundColor: (onlineCount.value ?? 0) > 1
? Colors.green
: Colors.grey,
offset: Offset(6, 14),
child: SizedBox(
height: 26,
width: 26,
child:
(room!.type == 1 && room.picture?.id == null)
? SplitAvatarWidget(
filesId:
getValidMembers(
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),
),
child: (room!.type == 1 && room.picture?.id == null)
? SplitAvatarWidget(
filesId: getValidMembers(
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)
? getValidMembers(
room.members!,
).map((e) => e.account.nick).join(', ')
room.members!,
).map((e) => e.account.nick).join(', ')
: room.name!,
).fontSize(15),
],
@@ -470,39 +465,38 @@ class ChatRoomScreen extends HookConsumerWidget {
isLabelVisible: (onlineCount.value ?? 0) > 1,
label: Text('${(onlineCount.value ?? 0)}'),
textStyle: GoogleFonts.robotoMono(fontSize: 10),
backgroundColor:
(onlineCount.value ?? 0) > 1 ? Colors.green : Colors.grey,
backgroundColor: (onlineCount.value ?? 0) > 1
? Colors.green
: Colors.grey,
textColor: Colors.white,
offset: Offset(6, 14),
child: SizedBox(
height: 28,
width: 28,
child:
(room!.type == 1 && room.picture?.id == null)
? SplitAvatarWidget(
filesId:
getValidMembers(
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),
),
child: (room!.type == 1 && room.picture?.id == null)
? SplitAvatarWidget(
filesId: getValidMembers(
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)
? getValidMembers(
room.members!,
).map((e) => e.account.nick).join(', ')
room.members!,
).map((e) => e.account.nick).join(', ')
: room.name!,
).fontSize(19),
],
@@ -531,11 +525,9 @@ class ChatRoomScreen extends HookConsumerWidget {
index: index,
scrollController: scrollController,
alignment: 0.5,
duration:
(estimatedDistance) => Duration(
milliseconds:
(estimatedDistance * 0.5).clamp(200, 800).toInt(),
),
duration: (estimatedDistance) => Duration(
milliseconds: (estimatedDistance * 0.5).clamp(200, 800).toInt(),
),
curve: (estimatedDistance) => Curves.easeOutCubic,
);
@@ -605,12 +597,11 @@ class ChatRoomScreen extends HookConsumerWidget {
final config = await showModalBottomSheet<AttachmentUploadConfig>(
context: context,
isScrollControlled: true,
builder:
(context) => AttachmentUploaderSheet(
ref: ref,
attachments: attachments.value,
index: index,
),
builder: (context) => AttachmentUploaderSheet(
ref: ref,
attachments: attachments.value,
index: index,
),
);
if (config == null) return;
@@ -621,22 +612,20 @@ class ChatRoomScreen extends HookConsumerWidget {
'chat-upload': {index: 0},
};
final cloudFile =
await FileUploader.createCloudFile(
ref: ref,
fileData: attachment,
poolId: config.poolId,
mode:
attachment.type == UniversalFileType.file
? FileUploadMode.generic
: FileUploadMode.mediaSafe,
onProgress: (progress, _) {
attachmentProgress.value = {
...attachmentProgress.value,
'chat-upload': {index: progress ?? 0.0},
};
},
).future;
final cloudFile = await FileUploader.createCloudFile(
ref: ref,
fileData: attachment,
poolId: config.poolId,
mode: attachment.type == UniversalFileType.file
? FileUploadMode.generic
: FileUploadMode.mediaSafe,
onProgress: (progress, _) {
attachmentProgress.value = {
...attachmentProgress.value,
'chat-upload': {index: progress ?? 0.0},
};
},
).future;
if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...');
@@ -690,10 +679,9 @@ class ChatRoomScreen extends HookConsumerWidget {
extentEstimation: (_, _) => 40,
itemBuilder: (context, index) {
final message = messageList[index];
final nextMessage =
index < messageList.length - 1
? messageList[index + 1]
: null;
final nextMessage = index < messageList.length - 1
? messageList[index + 1]
: null;
final isLastInGroup =
nextMessage == null ||
nextMessage.senderId != message.senderId ||
@@ -718,15 +706,14 @@ class ChatRoomScreen extends HookConsumerWidget {
toggleSelectionMode: toggleSelectionMode,
toggleMessageSelection: toggleMessageSelection,
onMessageAction: onMessageAction,
onJump:
(messageId) => scrollToMessage(
messageId: messageId,
messageList: messageList,
messagesNotifier: messagesNotifier,
listController: listController,
scrollController: scrollController,
ref: ref,
),
onJump: (messageId) => scrollToMessage(
messageId: messageId,
messageList: messageList,
messagesNotifier: messagesNotifier,
listController: listController,
scrollController: scrollController,
ref: ref,
),
attachmentProgress: attachmentProgress.value,
disableAnimation: settings.disableAnimation,
roomOpenTime: roomOpenTime,
@@ -744,17 +731,14 @@ class ChatRoomScreen extends HookConsumerWidget {
automaticallyImplyLeading: false,
toolbarHeight: compactHeader ? null : 74,
title: chatRoom.when(
data:
(room) =>
compactHeader
? compactHeaderWidget(room)
: comfortHeaderWidget(room),
data: (room) => compactHeader
? compactHeaderWidget(room)
: comfortHeaderWidget(room),
loading: () => const Text('Loading...'),
error:
(err, _) => ResponseErrorWidget(
error: err,
onRetry: () => messagesNotifier.loadInitial(),
),
error: (err, _) => ResponseErrorWidget(
error: err,
onRetry: () => messagesNotifier.loadInitial(),
),
),
actions: [
chatRoom.when(
@@ -787,13 +771,11 @@ class ChatRoomScreen extends HookConsumerWidget {
index: index,
scrollController: scrollController,
alignment: 0.5,
duration:
(estimatedDistance) => Duration(
milliseconds:
(estimatedDistance * 0.5)
.clamp(200, 800)
.toInt(),
),
duration: (estimatedDistance) => Duration(
milliseconds: (estimatedDistance * 0.5)
.clamp(200, 800)
.toInt(),
),
curve: (estimatedDistance) => Curves.easeOutCubic,
);
} catch (e) {
@@ -819,38 +801,35 @@ class ChatRoomScreen extends HookConsumerWidget {
duration: const Duration(milliseconds: 300),
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInCubic,
transitionBuilder: (
Widget child,
Animation<double> animation,
) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.05),
end: Offset.zero,
).animate(animation),
child: FadeTransition(opacity: animation, child: child),
);
},
transitionBuilder:
(Widget child, Animation<double> animation) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.05),
end: Offset.zero,
).animate(animation),
child: FadeTransition(
opacity: animation,
child: child,
),
);
},
child: messages.when(
data:
(messageList) =>
messageList.isEmpty
? Center(
key: const ValueKey('empty-messages'),
child: Text('No messages yet'.tr()),
)
: chatMessageListWidget(messageList),
loading:
() => const Center(
key: ValueKey('loading-messages'),
child: CircularProgressIndicator(),
),
error:
(error, _) => ResponseErrorWidget(
key: const ValueKey('error-messages'),
error: error,
onRetry: () => messagesNotifier.loadInitial(),
),
data: (messageList) => messageList.isEmpty
? Center(
key: const ValueKey('empty-messages'),
child: Text('No messages yet'.tr()),
)
: chatMessageListWidget(messageList),
loading: () => const Center(
key: ValueKey('loading-messages'),
child: CircularProgressIndicator(),
),
error: (error, _) => ResponseErrorWidget(
key: const ValueKey('error-messages'),
error: error,
onRetry: () => messagesNotifier.loadInitial(),
),
),
),
),
@@ -862,10 +841,8 @@ class ChatRoomScreen extends HookConsumerWidget {
right: 0,
top: 0,
child: chatRoom.when(
data:
(data) => CallOverlayBar(
room: data!,
).padding(horizontal: 8, top: 12),
data: (data) =>
CallOverlayBar(room: data!).padding(horizontal: 8, top: 12),
error: (_, _) => const SizedBox.shrink(),
loading: () => const SizedBox.shrink(),
),
@@ -903,35 +880,34 @@ class ChatRoomScreen extends HookConsumerWidget {
if (!isSelectionMode.value)
AnimatedBuilder(
animation: bottomGradientNotifier.value,
builder:
(context, child) => Positioned(
left: 0,
right: 0,
bottom: 0,
child: Opacity(
opacity: bottomGradientNotifier.value.value,
child: Container(
height: math.min(
MediaQuery.of(context).size.height * 0.1,
128,
),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Theme.of(
context,
).colorScheme.surfaceContainer.withOpacity(0.8),
Theme.of(
context,
).colorScheme.surfaceContainer.withOpacity(0.0),
],
),
),
builder: (context, child) => Positioned(
left: 0,
right: 0,
bottom: 0,
child: Opacity(
opacity: bottomGradientNotifier.value.value,
child: Container(
height: math.min(
MediaQuery.of(context).size.height * 0.1,
128,
),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Theme.of(
context,
).colorScheme.surfaceContainer.withOpacity(0.8),
Theme.of(
context,
).colorScheme.surfaceContainer.withOpacity(0.0),
],
),
),
),
),
),
),
// Chat Input positioned above gradient (higher z-index)
if (!isSelectionMode.value)
@@ -940,74 +916,73 @@ class ChatRoomScreen extends HookConsumerWidget {
right: 0,
bottom: 0, // At the very bottom, above gradient
child: chatRoom.when(
data:
(room) => Column(
key: inputKey,
mainAxisSize: MainAxisSize.min,
children: [
ChatInput(
messageController: messageController,
chatRoom: room!,
onSend: sendMessage,
onClear: () {
if (messageEditingTo.value != null) {
attachments.value.clear();
messageController.clear();
}
messageEditingTo.value = null;
messageReplyingTo.value = null;
messageForwardingTo.value = null;
selectedPoll.value = null;
selectedFund.value = null;
},
messageEditingTo: messageEditingTo.value,
messageReplyingTo: messageReplyingTo.value,
messageForwardingTo: messageForwardingTo.value,
selectedPoll: selectedPoll.value,
onPollSelected: (poll) => selectedPoll.value = poll,
selectedFund: selectedFund.value,
onFundSelected: (fund) => selectedFund.value = fund,
onPickFile: (bool isPhoto) {
if (isPhoto) {
pickPhotoMedia();
} else {
pickVideoMedia();
}
},
onPickAudio: pickAudioMedia,
onPickGeneralFile: pickGeneralFile,
onLinkAttachment: linkAttachment,
attachments: attachments.value,
onUploadAttachment: uploadAttachment,
onDeleteAttachment: (index) async {
final attachment = attachments.value[index];
if (attachment.isOnCloud && !attachment.isLink) {
final client = ref.watch(apiClientProvider);
await client.delete(
'/drive/files/${attachment.data.id}',
);
}
final clone = List.of(attachments.value);
clone.removeAt(index);
attachments.value = clone;
},
onMoveAttachment: (idx, delta) {
if (idx + delta < 0 ||
idx + delta >= attachments.value.length) {
return;
}
final clone = List.of(attachments.value);
clone.insert(idx + delta, clone.removeAt(idx));
attachments.value = clone;
},
onAttachmentsChanged: (newAttachments) {
attachments.value = newAttachments;
},
attachmentProgress: attachmentProgress.value,
),
Gap(MediaQuery.of(context).padding.bottom),
],
data: (room) => Column(
key: inputKey,
mainAxisSize: MainAxisSize.min,
children: [
ChatInput(
messageController: messageController,
chatRoom: room!,
onSend: sendMessage,
onClear: () {
if (messageEditingTo.value != null) {
attachments.value.clear();
messageController.clear();
}
messageEditingTo.value = null;
messageReplyingTo.value = null;
messageForwardingTo.value = null;
selectedPoll.value = null;
selectedFund.value = null;
},
messageEditingTo: messageEditingTo.value,
messageReplyingTo: messageReplyingTo.value,
messageForwardingTo: messageForwardingTo.value,
selectedPoll: selectedPoll.value,
onPollSelected: (poll) => selectedPoll.value = poll,
selectedFund: selectedFund.value,
onFundSelected: (fund) => selectedFund.value = fund,
onPickFile: (bool isPhoto) {
if (isPhoto) {
pickPhotoMedia();
} else {
pickVideoMedia();
}
},
onPickAudio: pickAudioMedia,
onPickGeneralFile: pickGeneralFile,
onLinkAttachment: linkAttachment,
attachments: attachments.value,
onUploadAttachment: uploadAttachment,
onDeleteAttachment: (index) async {
final attachment = attachments.value[index];
if (attachment.isOnCloud && !attachment.isLink) {
final client = ref.watch(apiClientProvider);
await client.delete(
'/drive/files/${attachment.data.id}',
);
}
final clone = List.of(attachments.value);
clone.removeAt(index);
attachments.value = clone;
},
onMoveAttachment: (idx, delta) {
if (idx + delta < 0 ||
idx + delta >= attachments.value.length) {
return;
}
final clone = List.of(attachments.value);
clone.insert(idx + delta, clone.removeAt(idx));
attachments.value = clone;
},
onAttachmentsChanged: (newAttachments) {
attachments.value = newAttachments;
},
attachmentProgress: attachmentProgress.value,
),
Gap(MediaQuery.of(context).padding.bottom),
],
),
error: (_, _) => const SizedBox.shrink(),
loading: () => const SizedBox.shrink(),
),

View File

@@ -67,7 +67,7 @@ class ChatDetailScreen extends HookConsumerWidget {
try {
final client = ref.watch(apiClientProvider);
await client.patch(
'/sphere/chat/$id/members/me/notify',
'/messager/chat/$id/members/me/notify',
data: {'notify_level': level},
);
ref.invalidate(chatRoomIdentityProvider(id));
@@ -85,7 +85,7 @@ class ChatDetailScreen extends HookConsumerWidget {
try {
final client = ref.watch(apiClientProvider);
await client.patch(
'/sphere/chat/$id/members/me/notify',
'/messager/chat/$id/members/me/notify',
data: {'break_until': until.toUtc().toIso8601String()},
);
ref.invalidate(chatRoomIdentityProvider(id));
@@ -529,7 +529,7 @@ class _ChatRoomActionMenu extends HookConsumerWidget {
).then((confirm) async {
if (confirm) {
final client = ref.watch(apiClientProvider);
await client.delete('/sphere/chat/$id');
await client.delete('/messager/chat/$id');
ref.invalidate(chatRoomJoinedProvider);
if (context.mounted) {
context.pop();
@@ -560,7 +560,7 @@ class _ChatRoomActionMenu extends HookConsumerWidget {
).then((confirm) async {
if (confirm) {
final client = ref.watch(apiClientProvider);
await client.delete('/sphere/chat/$id/members/me');
await client.delete('/messager/chat/$id/members/me');
ref.invalidate(chatRoomJoinedProvider);
if (context.mounted) {
context.pop();
@@ -600,7 +600,7 @@ class ChatMemberListNotifier
Future<List<SnChatMember>> fetch() async {
final apiClient = ref.watch(apiClientProvider);
final response = await apiClient.get(
'/sphere/chat/$arg/members',
'/messager/chat/$arg/members',
queryParameters: {
'offset': fetchedCount.toString(),
'take': pageSize,
@@ -645,7 +645,7 @@ class _ChatMemberListSheet extends HookConsumerWidget {
try {
final apiClient = ref.watch(apiClientProvider);
await apiClient.post(
'/sphere/chat/invites/$roomId',
'/messager/chat/invites/$roomId',
data: {'related_user_id': result.id, 'role': 0},
);
memberNotifier.refresh();
@@ -735,7 +735,7 @@ class _ChatMemberListSheet extends HookConsumerWidget {
try {
final apiClient = ref.watch(apiClientProvider);
await apiClient.delete(
'/sphere/chat/$roomId/members/${member.accountId}',
'/messager/chat/$roomId/members/${member.accountId}',
);
// Refresh both providers
memberNotifier.refresh();

View File

@@ -18,7 +18,7 @@ class AutocompleteService {
String content,
) async {
final response = await _client.post(
'/sphere/chat/$roomId/autocomplete',
'/messager/chat/$roomId/autocomplete',
data: {'content': content},
);

View File

@@ -16,7 +16,7 @@ Future<SnRealtimeCall?> ongoingCall(Ref ref, String roomId) async {
if (roomId.isEmpty) return null;
try {
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get('/sphere/chat/realtime/$roomId');
final resp = await apiClient.get('/messager/chat/realtime/$roomId');
return SnRealtimeCall.fromJson(resp.data);
} catch (e) {
if (e is DioException && e.response?.statusCode == 404) {
@@ -42,7 +42,7 @@ class AudioCallButton extends HookConsumerWidget {
Future<void> handleJoin() async {
isLoading.value = true;
try {
await apiClient.post('/sphere/chat/realtime/${room.id}');
await apiClient.post('/messager/chat/realtime/${room.id}');
ref.invalidate(ongoingCallProvider(room.id));
// Just join the room, the overlay will handle the UI
await callNotifier.joinRoom(room);
@@ -56,7 +56,7 @@ class AudioCallButton extends HookConsumerWidget {
Future<void> handleEnd() async {
isLoading.value = true;
try {
await apiClient.delete('/sphere/chat/realtime/${room.id}');
await apiClient.delete('/messager/chat/realtime/${room.id}');
callNotifier.dispose(); // Clean up call resources
} catch (e) {
showErrorAlert(e);

View File

@@ -117,7 +117,7 @@ class CallControlsBar extends HookConsumerWidget {
try {
showLoadingModal(context);
await apiClient.delete(
'/sphere/chat/realtime/${callNotifier.roomId}',
'/messager/chat/realtime/${callNotifier.roomId}',
);
callNotifier.dispose();
if (context.mounted && popOnLeaves) {

View File

@@ -66,8 +66,9 @@ class PublicRoomPreview extends HookConsumerWidget {
extentEstimation: (_, _) => 40,
itemBuilder: (context, index) {
final message = messageList[index];
final nextMessage =
index < messageList.length - 1 ? messageList[index + 1] : null;
final nextMessage = index < messageList.length - 1
? messageList[index + 1]
: null;
final isLastInGroup =
nextMessage == null ||
nextMessage.senderId != message.senderId ||
@@ -98,25 +99,23 @@ class PublicRoomPreview extends HookConsumerWidget {
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),
),
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)
@@ -133,25 +132,23 @@ class PublicRoomPreview extends HookConsumerWidget {
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),
),
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)
@@ -182,17 +179,14 @@ class PublicRoomPreview extends HookConsumerWidget {
children: [
Expanded(
child: messages.when(
data:
(messageList) =>
messageList.isEmpty
? Center(child: Text('No messages yet'.tr()))
: chatMessageListWidget(messageList),
data: (messageList) => messageList.isEmpty
? Center(child: Text('No messages yet'.tr()))
: chatMessageListWidget(messageList),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, _) => ResponseErrorWidget(
error: error,
onRetry: () => messagesNotifier.loadInitial(),
),
error: (error, _) => ResponseErrorWidget(
error: error,
onRetry: () => messagesNotifier.loadInitial(),
),
),
),
// Join button at the bottom for public rooms
@@ -203,7 +197,7 @@ class PublicRoomPreview extends HookConsumerWidget {
try {
showLoadingModal(context);
final apiClient = ref.read(apiClientProvider);
await apiClient.post('/sphere/chat/${room.id}/members/me');
await apiClient.post('/messager/chat/${room.id}/members/me');
ref.invalidate(chatRoomIdentityProvider(id));
} catch (err) {
showErrorAlert(err);

View File

@@ -218,20 +218,19 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
case ShareContentType.file:
// Upload files to cloud storage
if (widget.content.files?.isNotEmpty == true) {
final universalFiles =
widget.content.files!.map((file) {
UniversalFileType fileType;
if (file.mimeType?.startsWith('image/') == true) {
fileType = UniversalFileType.image;
} else if (file.mimeType?.startsWith('video/') == true) {
fileType = UniversalFileType.video;
} else if (file.mimeType?.startsWith('audio/') == true) {
fileType = UniversalFileType.audio;
} else {
fileType = UniversalFileType.file;
}
return UniversalFile(data: file, type: fileType);
}).toList();
final universalFiles = widget.content.files!.map((file) {
UniversalFileType fileType;
if (file.mimeType?.startsWith('image/') == true) {
fileType = UniversalFileType.image;
} else if (file.mimeType?.startsWith('video/') == true) {
fileType = UniversalFileType.video;
} else if (file.mimeType?.startsWith('audio/') == true) {
fileType = UniversalFileType.audio;
} else {
fileType = UniversalFileType.file;
}
return UniversalFile(data: file, type: fileType);
}).toList();
// Initialize progress tracking
final messageId = DateTime.now().millisecondsSinceEpoch.toString();
@@ -243,19 +242,17 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
// Upload each file
for (var idx = 0; idx < universalFiles.length; idx++) {
final file = universalFiles[idx];
final cloudFile =
await FileUploader.createCloudFile(
ref: ref,
fileData: file,
onProgress: (progress, _) {
if (mounted) {
setState(() {
_fileUploadProgress[messageId]?[idx] =
progress ?? 0.0;
});
}
},
).future;
final cloudFile = await FileUploader.createCloudFile(
ref: ref,
fileData: file,
onProgress: (progress, _) {
if (mounted) {
setState(() {
_fileUploadProgress[messageId]?[idx] = progress ?? 0.0;
});
}
},
).future;
if (cloudFile == null) {
throw Exception('Failed to upload file: ${file.data.name}');
@@ -272,7 +269,7 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
// Send message to chat room
await apiClient.post(
'/sphere/chat/${chatRoom.id}/messages',
'/messager/chat/${chatRoom.id}/messages',
data: {'content': content, 'attachments_id': attachmentIds, 'meta': {}},
);
@@ -359,20 +356,19 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
setState(() => _isLoading = true);
try {
final universalFiles =
widget.content.files!.map((file) {
UniversalFileType fileType;
if (file.mimeType?.startsWith('image/') == true) {
fileType = UniversalFileType.image;
} else if (file.mimeType?.startsWith('video/') == true) {
fileType = UniversalFileType.video;
} else if (file.mimeType?.startsWith('audio/') == true) {
fileType = UniversalFileType.audio;
} else {
fileType = UniversalFileType.file;
}
return UniversalFile(data: file, type: fileType);
}).toList();
final universalFiles = widget.content.files!.map((file) {
UniversalFileType fileType;
if (file.mimeType?.startsWith('image/') == true) {
fileType = UniversalFileType.image;
} else if (file.mimeType?.startsWith('video/') == true) {
fileType = UniversalFileType.video;
} else if (file.mimeType?.startsWith('audio/') == true) {
fileType = UniversalFileType.audio;
} else {
fileType = UniversalFileType.file;
}
return UniversalFile(data: file, type: fileType);
}).toList();
// Initialize progress tracking
final messageId = DateTime.now().millisecondsSinceEpoch.toString();
@@ -383,18 +379,17 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
// Upload each file
for (var idx = 0; idx < universalFiles.length; idx++) {
final file = universalFiles[idx];
final cloudFile =
await FileUploader.createCloudFile(
ref: ref,
fileData: file,
onProgress: (progress, _) {
if (mounted) {
setState(() {
_fileUploadProgress[messageId]?[idx] = progress ?? 0.0;
});
}
},
).future;
final cloudFile = await FileUploader.createCloudFile(
ref: ref,
fileData: file,
onProgress: (progress, _) {
if (mounted) {
setState(() {
_fileUploadProgress[messageId]?[idx] = progress ?? 0.0;
});
}
},
).future;
if (cloudFile == null) {
throw Exception('Failed to upload file: ${file.data.name}');
@@ -481,8 +476,9 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color:
Theme.of(context).colorScheme.surfaceContainerHighest,
color: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Column(
@@ -490,12 +486,12 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
children: [
Text(
'contentToShare'.tr(),
style: Theme.of(
context,
).textTheme.labelMedium?.copyWith(
color:
Theme.of(context).colorScheme.onSurfaceVariant,
),
style: Theme.of(context).textTheme.labelMedium
?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
_ContentPreview(content: widget.content),
@@ -510,12 +506,12 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
children: [
Text(
'quickActions'.tr(),
style: Theme.of(
context,
).textTheme.titleSmall?.copyWith(
color:
Theme.of(context).colorScheme.onSurfaceVariant,
),
style: Theme.of(context).textTheme.titleSmall
?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 12),
SizedBox(
@@ -568,12 +564,12 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
children: [
Text(
'sendToChat'.tr(),
style: Theme.of(
context,
).textTheme.titleSmall?.copyWith(
color:
Theme.of(context).colorScheme.onSurfaceVariant,
),
style: Theme.of(context).textTheme.titleSmall
?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 12),
@@ -592,10 +588,8 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
vertical: 12,
),
),
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
maxLines: 3,
minLines: 1,
enabled: !_isLoading,
@@ -603,8 +597,9 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
),
_ChatRoomsList(
onChatSelected:
_isLoading ? null : _shareToSpecificChat,
onChatSelected: _isLoading
? null
: _shareToSpecificChat,
),
],
),
@@ -627,11 +622,9 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
if (_fileUploadProgress.isNotEmpty)
..._fileUploadProgress.entries.map((entry) {
final progress = entry.value;
final averageProgress =
progress.isEmpty
? 0.0
: progress.reduce((a, b) => a + b) /
progress.length;
final averageProgress = progress.isEmpty
? 0.0
: progress.reduce((a, b) => a + b) / progress.length;
return Column(
children: [
Text(
@@ -699,39 +692,38 @@ class _ChatRoomsList extends ConsumerWidget {
final room = rooms[index];
return _ChatRoomOption(
room: room,
onTap:
onChatSelected != null ? () => onChatSelected!(room) : null,
onTap: onChatSelected != null
? () => onChatSelected!(room)
: null,
);
},
),
);
},
loading:
() => SizedBox(
height: 80,
child: Center(
child: CircularProgressIndicator(
strokeWidth: 2,
color: Theme.of(context).colorScheme.primary,
),
),
),
error:
(error, stack) => Container(
height: 80,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.errorContainer,
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
'failedToLoadChats'.tr(),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onErrorContainer,
),
),
loading: () => SizedBox(
height: 80,
child: Center(
child: CircularProgressIndicator(
strokeWidth: 2,
color: Theme.of(context).colorScheme.primary,
),
),
),
error: (error, stack) => Container(
height: 80,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.errorContainer,
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
'failedToLoadChats'.tr(),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onErrorContainer,
),
),
),
),
);
}
}
@@ -746,10 +738,9 @@ class _ChatRoomOption extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final userInfo = ref.watch(userInfoProvider);
final validMembers =
(room.members ?? [])
.where((m) => m.accountId != userInfo.value?.id)
.toList();
final validMembers = (room.members ?? [])
.where((m) => m.accountId != userInfo.value?.id)
.toList();
final isDirect = room.type == 1; // Assuming type 1 is direct chat
final displayName =
@@ -764,12 +755,11 @@ class _ChatRoomOption extends HookConsumerWidget {
width: 72,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color:
onTap != null
? Theme.of(context).colorScheme.surfaceContainerHighest
: Theme.of(
context,
).colorScheme.surfaceContainerHighest.withOpacity(0.5),
color: onTap != null
? Theme.of(context).colorScheme.surfaceContainerHighest
: Theme.of(
context,
).colorScheme.surfaceContainerHighest.withOpacity(0.5),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
@@ -786,36 +776,30 @@ class _ChatRoomOption extends HookConsumerWidget {
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
),
child:
(isDirect && room.picture?.id == null)
? SplitAvatarWidget(
filesId:
validMembers
.map((e) => e.account.profile.picture?.id)
.toList(),
radius: 16,
)
: room.picture?.id == null
? CircleAvatar(
radius: 16,
child: Text(room.name![0].toUpperCase()),
)
: ProfilePictureWidget(
fileId: room.picture?.id,
radius: 16,
),
child: (isDirect && room.picture?.id == null)
? SplitAvatarWidget(
filesId: validMembers
.map((e) => e.account.profile.picture?.id)
.toList(),
radius: 16,
)
: room.picture?.id == null
? CircleAvatar(
radius: 16,
child: Text(room.name![0].toUpperCase()),
)
: ProfilePictureWidget(fileId: room.picture?.id, radius: 16),
),
const SizedBox(height: 4),
// Chat room name
Text(
displayName,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color:
onTap != null
? Theme.of(context).colorScheme.onSurface
: Theme.of(
context,
).colorScheme.onSurfaceVariant.withOpacity(0.5),
color: onTap != null
? Theme.of(context).colorScheme.onSurface
: Theme.of(
context,
).colorScheme.onSurfaceVariant.withOpacity(0.5),
),
textAlign: TextAlign.center,
maxLines: 1,
@@ -847,12 +831,11 @@ class _CompactShareOption extends StatelessWidget {
width: 72,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color:
onTap != null
? Theme.of(context).colorScheme.surfaceContainerHighest
: Theme.of(
context,
).colorScheme.surfaceContainerHighest.withOpacity(0.5),
color: onTap != null
? Theme.of(context).colorScheme.surfaceContainerHighest
: Theme.of(
context,
).colorScheme.surfaceContainerHighest.withOpacity(0.5),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
@@ -864,23 +847,21 @@ class _CompactShareOption extends StatelessWidget {
Icon(
icon,
size: 24,
color:
onTap != null
? Theme.of(context).colorScheme.primary
: Theme.of(
context,
).colorScheme.onSurfaceVariant.withOpacity(0.5),
color: onTap != null
? Theme.of(context).colorScheme.primary
: Theme.of(
context,
).colorScheme.onSurfaceVariant.withOpacity(0.5),
),
const SizedBox(height: 4),
Text(
title,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color:
onTap != null
? Theme.of(context).colorScheme.onSurface
: Theme.of(
context,
).colorScheme.onSurfaceVariant.withOpacity(0.5),
color: onTap != null
? Theme.of(context).colorScheme.onSurface
: Theme.of(
context,
).colorScheme.onSurfaceVariant.withOpacity(0.5),
),
textAlign: TextAlign.center,
maxLines: 1,
@@ -937,26 +918,25 @@ class _LinkPreview extends ConsumerWidget {
final linkPreviewAsync = ref.watch(linkPreviewProvider(link));
return linkPreviewAsync.when(
loading:
() => Container(
constraints: const BoxConstraints(maxHeight: kPreviewMaxHeight),
child: Row(
children: [
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
const SizedBox(width: 8),
Text(
'Loading link preview...',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
loading: () => Container(
constraints: const BoxConstraints(maxHeight: kPreviewMaxHeight),
child: Row(
children: [
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
const SizedBox(width: 8),
Text(
'Loading link preview...',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
error: (error, stackTrace) => _buildFallbackPreview(context),
data: (embed) {
if (embed == null) {
@@ -979,27 +959,27 @@ class _LinkPreview extends ConsumerWidget {
margin: const EdgeInsets.only(right: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color:
Theme.of(context).colorScheme.surfaceContainerHighest,
color: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child:
embed.imageUrl != null
? Image.network(
embed.imageUrl!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _buildFaviconFallback(
context,
embed.faviconUrl ?? '',
);
},
)
: _buildFaviconFallback(
context,
embed.faviconUrl ?? '',
),
child: embed.imageUrl != null
? Image.network(
embed.imageUrl!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _buildFaviconFallback(
context,
embed.faviconUrl ?? '',
);
},
)
: _buildFaviconFallback(
context,
embed.faviconUrl ?? '',
),
),
),
// Content
@@ -1033,12 +1013,12 @@ class _LinkPreview extends ConsumerWidget {
padding: const EdgeInsets.only(top: 4),
child: Text(
embed.description!,
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(
color:
Theme.of(context).colorScheme.onSurfaceVariant,
),
style: Theme.of(context).textTheme.bodySmall
?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
@@ -1231,17 +1211,15 @@ class _FilePreview extends StatelessWidget {
return Container(
width: 40,
height: 40,
color:
Theme.of(
context,
).colorScheme.surfaceVariant,
color: Theme.of(
context,
).colorScheme.surfaceVariant,
child: Icon(
Symbols.broken_image,
size: 20,
color:
Theme.of(
context,
).colorScheme.onSurfaceVariant,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
);
},
@@ -1252,17 +1230,17 @@ class _FilePreview extends StatelessWidget {
width: 40,
height: 40,
decoration: BoxDecoration(
color:
Theme.of(context).colorScheme.primaryContainer,
color: Theme.of(
context,
).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(4),
),
child: Icon(
_getFileIcon(file.name),
size: 20,
color:
Theme.of(
context,
).colorScheme.onPrimaryContainer,
color: Theme.of(
context,
).colorScheme.onPrimaryContainer,
),
),
const SizedBox(width: 12),
@@ -1281,22 +1259,19 @@ class _FilePreview extends StatelessWidget {
builder: (context, snapshot) {
if (snapshot.hasData) {
final size = snapshot.data!;
final sizeStr =
size < 1024
? '${size}B'
: size < 1024 * 1024
? '${(size / 1024).toStringAsFixed(1)}KB'
: '${(size / (1024 * 1024)).toStringAsFixed(1)}MB';
final sizeStr = size < 1024
? '${size}B'
: size < 1024 * 1024
? '${(size / 1024).toStringAsFixed(1)}KB'
: '${(size / (1024 * 1024)).toStringAsFixed(1)}MB';
return Text(
sizeStr,
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(
color:
Theme.of(
style: Theme.of(context).textTheme.bodySmall
?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
);
}
return const SizedBox.shrink();
@@ -1329,13 +1304,12 @@ void showShareSheet({
context: context,
isScrollControlled: true,
useRootNavigator: true,
builder:
(context) => ShareSheet(
content: content,
title: title,
toSystem: toSystem,
onClose: onClose,
),
builder: (context) => ShareSheet(
content: content,
title: title,
toSystem: toSystem,
onClose: onClose,
),
);
}