From fc2520b8f87679affa0708dc61aeaa2833ce21f4 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 24 May 2025 01:34:20 +0800 Subject: [PATCH] :alien: Support the latest API --- assets/i18n/en-US.json | 7 +- lib/database/message_repository.dart | 8 -- lib/main.dart | 9 +- lib/models/chat.dart | 11 ++ lib/models/chat.freezed.dart | 154 ++++++++++++++++++++++++++ lib/models/chat.g.dart | 14 +++ lib/pods/chat_summary.dart | 64 +++++++++++ lib/pods/chat_summary.g.dart | 27 +++++ lib/route.gr.dart | 44 ++++---- lib/screens/account/profile.dart | 4 +- lib/screens/account/relationship.dart | 19 ++-- lib/screens/auth/tabs.dart | 1 + lib/screens/chat/chat.dart | 116 ++++++++++++++++--- lib/screens/chat/room.dart | 22 +--- lib/screens/creators/publishers.dart | 2 +- lib/screens/realm/realms.dart | 10 +- 16 files changed, 426 insertions(+), 86 deletions(-) create mode 100644 lib/pods/chat_summary.dart create mode 100644 lib/pods/chat_summary.g.dart diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 7f75494..d6bd22c 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -263,5 +263,10 @@ "notifications": "Notifications", "posts": "Posts", "settingsBackgroundImage": "Background Image", - "settingsBackgroundImageClear": "Clear Background Image" + "settingsBackgroundImageClear": "Clear Background Image", + "messageNone": "No content to display", + "unreadMessages": { + "one": "{} unread message", + "other": "{} unread messages" + } } diff --git a/lib/database/message_repository.dart b/lib/database/message_repository.dart index 1c7d363..37bdc26 100644 --- a/lib/database/message_repository.dart +++ b/lib/database/message_repository.dart @@ -456,12 +456,4 @@ class MessageRepository { rethrow; } } - - Future markMessageAsRead(String messageId) async { - try { - await _database.markMessageAsRead(messageId); - } catch (e) { - showErrorAlert(e); - } - } } diff --git a/lib/main.dart b/lib/main.dart index 5ae41ee..d4353d8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -27,7 +27,10 @@ import 'package:flutter_native_splash/flutter_native_splash.dart'; void main() async { final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); - FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); + if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { + FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); + } + await EasyLocalization.ensureInitialized(); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); @@ -51,7 +54,9 @@ void main() async { } } - FlutterNativeSplash.remove(); + if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { + FlutterNativeSplash.remove(); + } runApp( ProviderScope( diff --git a/lib/models/chat.dart b/lib/models/chat.dart index 9d54c41..3de5b99 100644 --- a/lib/models/chat.dart +++ b/lib/models/chat.dart @@ -95,6 +95,17 @@ abstract class SnChatMember with _$SnChatMember { _$SnChatMemberFromJson(json); } +@freezed +abstract class SnChatSummary with _$SnChatSummary { + const factory SnChatSummary({ + required int unreadCount, + required SnChatMessage lastMessage, + }) = _SnChatSummary; + + factory SnChatSummary.fromJson(Map json) => + _$SnChatSummaryFromJson(json); +} + class MessageChangeAction { static const String create = "create"; static const String update = "update"; diff --git a/lib/models/chat.freezed.dart b/lib/models/chat.freezed.dart index c46e2d6..ddfdf10 100644 --- a/lib/models/chat.freezed.dart +++ b/lib/models/chat.freezed.dart @@ -874,6 +874,160 @@ $SnAccountCopyWith<$Res> get account { } +/// @nodoc +mixin _$SnChatSummary { + + int get unreadCount; SnChatMessage get lastMessage; +/// Create a copy of SnChatSummary +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SnChatSummaryCopyWith get copyWith => _$SnChatSummaryCopyWithImpl(this as SnChatSummary, _$identity); + + /// Serializes this SnChatSummary to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatSummary&&(identical(other.unreadCount, unreadCount) || other.unreadCount == unreadCount)&&(identical(other.lastMessage, lastMessage) || other.lastMessage == lastMessage)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,unreadCount,lastMessage); + +@override +String toString() { + return 'SnChatSummary(unreadCount: $unreadCount, lastMessage: $lastMessage)'; +} + + +} + +/// @nodoc +abstract mixin class $SnChatSummaryCopyWith<$Res> { + factory $SnChatSummaryCopyWith(SnChatSummary value, $Res Function(SnChatSummary) _then) = _$SnChatSummaryCopyWithImpl; +@useResult +$Res call({ + int unreadCount, SnChatMessage lastMessage +}); + + +$SnChatMessageCopyWith<$Res> get lastMessage; + +} +/// @nodoc +class _$SnChatSummaryCopyWithImpl<$Res> + implements $SnChatSummaryCopyWith<$Res> { + _$SnChatSummaryCopyWithImpl(this._self, this._then); + + final SnChatSummary _self; + final $Res Function(SnChatSummary) _then; + +/// Create a copy of SnChatSummary +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? unreadCount = null,Object? lastMessage = null,}) { + return _then(_self.copyWith( +unreadCount: null == unreadCount ? _self.unreadCount : unreadCount // ignore: cast_nullable_to_non_nullable +as int,lastMessage: null == lastMessage ? _self.lastMessage : lastMessage // ignore: cast_nullable_to_non_nullable +as SnChatMessage, + )); +} +/// Create a copy of SnChatSummary +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnChatMessageCopyWith<$Res> get lastMessage { + + return $SnChatMessageCopyWith<$Res>(_self.lastMessage, (value) { + return _then(_self.copyWith(lastMessage: value)); + }); +} +} + + +/// @nodoc +@JsonSerializable() + +class _SnChatSummary implements SnChatSummary { + const _SnChatSummary({required this.unreadCount, required this.lastMessage}); + factory _SnChatSummary.fromJson(Map json) => _$SnChatSummaryFromJson(json); + +@override final int unreadCount; +@override final SnChatMessage lastMessage; + +/// Create a copy of SnChatSummary +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$SnChatSummaryCopyWith<_SnChatSummary> get copyWith => __$SnChatSummaryCopyWithImpl<_SnChatSummary>(this, _$identity); + +@override +Map toJson() { + return _$SnChatSummaryToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatSummary&&(identical(other.unreadCount, unreadCount) || other.unreadCount == unreadCount)&&(identical(other.lastMessage, lastMessage) || other.lastMessage == lastMessage)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,unreadCount,lastMessage); + +@override +String toString() { + return 'SnChatSummary(unreadCount: $unreadCount, lastMessage: $lastMessage)'; +} + + +} + +/// @nodoc +abstract mixin class _$SnChatSummaryCopyWith<$Res> implements $SnChatSummaryCopyWith<$Res> { + factory _$SnChatSummaryCopyWith(_SnChatSummary value, $Res Function(_SnChatSummary) _then) = __$SnChatSummaryCopyWithImpl; +@override @useResult +$Res call({ + int unreadCount, SnChatMessage lastMessage +}); + + +@override $SnChatMessageCopyWith<$Res> get lastMessage; + +} +/// @nodoc +class __$SnChatSummaryCopyWithImpl<$Res> + implements _$SnChatSummaryCopyWith<$Res> { + __$SnChatSummaryCopyWithImpl(this._self, this._then); + + final _SnChatSummary _self; + final $Res Function(_SnChatSummary) _then; + +/// Create a copy of SnChatSummary +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? unreadCount = null,Object? lastMessage = null,}) { + return _then(_SnChatSummary( +unreadCount: null == unreadCount ? _self.unreadCount : unreadCount // ignore: cast_nullable_to_non_nullable +as int,lastMessage: null == lastMessage ? _self.lastMessage : lastMessage // ignore: cast_nullable_to_non_nullable +as SnChatMessage, + )); +} + +/// Create a copy of SnChatSummary +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnChatMessageCopyWith<$Res> get lastMessage { + + return $SnChatMessageCopyWith<$Res>(_self.lastMessage, (value) { + return _then(_self.copyWith(lastMessage: value)); + }); +} +} + + /// @nodoc mixin _$MessageChange { diff --git a/lib/models/chat.g.dart b/lib/models/chat.g.dart index 08bd0a1..4498aa9 100644 --- a/lib/models/chat.g.dart +++ b/lib/models/chat.g.dart @@ -188,6 +188,20 @@ Map _$SnChatMemberToJson(_SnChatMember instance) => 'is_bot': instance.isBot, }; +_SnChatSummary _$SnChatSummaryFromJson(Map json) => + _SnChatSummary( + unreadCount: (json['unread_count'] as num).toInt(), + lastMessage: SnChatMessage.fromJson( + json['last_message'] as Map, + ), + ); + +Map _$SnChatSummaryToJson(_SnChatSummary instance) => + { + 'unread_count': instance.unreadCount, + 'last_message': instance.lastMessage.toJson(), + }; + _MessageChange _$MessageChangeFromJson(Map json) => _MessageChange( messageId: json['message_id'] as String, diff --git a/lib/pods/chat_summary.dart b/lib/pods/chat_summary.dart new file mode 100644 index 0000000..91296a4 --- /dev/null +++ b/lib/pods/chat_summary.dart @@ -0,0 +1,64 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:island/models/chat.dart'; +import 'package:island/pods/network.dart'; + +part 'chat_summary.g.dart'; + +@riverpod +class ChatSummary extends _$ChatSummary { + @override + Future> build() async { + final client = ref.watch(apiClientProvider); + final resp = await client.get('/chat/summary'); + + final Map data = resp.data; + return data.map( + (key, value) => MapEntry(key, SnChatSummary.fromJson(value)), + ); + } + + Future clearUnreadCount(String chatId) async { + state.whenData((summaries) { + final summary = summaries[chatId]; + if (summary != null) { + state = AsyncData({ + ...summaries, + chatId: SnChatSummary( + unreadCount: 0, + lastMessage: summary.lastMessage, + ), + }); + } + }); + } + + void updateLastMessage(String chatId, SnChatMessage message) { + state.whenData((summaries) { + final summary = summaries[chatId]; + if (summary != null) { + state = AsyncData({ + ...summaries, + chatId: SnChatSummary( + unreadCount: summary.unreadCount + 1, + lastMessage: message, + ), + }); + } + }); + } + + void incrementUnreadCount(String chatId) { + state.whenData((summaries) { + final summary = summaries[chatId]; + if (summary != null) { + state = AsyncData({ + ...summaries, + chatId: SnChatSummary( + unreadCount: summary.unreadCount + 1, + lastMessage: summary.lastMessage, + ), + }); + } + }); + } +} diff --git a/lib/pods/chat_summary.g.dart b/lib/pods/chat_summary.g.dart new file mode 100644 index 0000000..c582148 --- /dev/null +++ b/lib/pods/chat_summary.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'chat_summary.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$chatSummaryHash() => r'fa48d381f489f90055fb728f7e0fda6f8ef49d15'; + +/// See also [ChatSummary]. +@ProviderFor(ChatSummary) +final chatSummaryProvider = AutoDisposeAsyncNotifierProvider< + ChatSummary, + Map +>.internal( + ChatSummary.new, + name: r'chatSummaryProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$chatSummaryHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$ChatSummary = AutoDisposeAsyncNotifier>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/route.gr.dart b/lib/route.gr.dart index a50848b..8acedbc 100644 --- a/lib/route.gr.dart +++ b/lib/route.gr.dart @@ -61,8 +61,8 @@ class AccountProfileRoute extends _i26.PageRouteInfo { builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( - orElse: - () => AccountProfileRouteArgs(name: pathParams.getString('name')), + orElse: () => + AccountProfileRouteArgs(name: pathParams.getString('name')), ); return _i1.AccountProfileScreen(key: args.key, name: args.name); }, @@ -508,11 +508,10 @@ class EditStickerPacksRoute builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( - orElse: - () => EditStickerPacksRouteArgs( - pubName: pathParams.getString('name'), - packId: pathParams.optString('packId'), - ), + orElse: () => EditStickerPacksRouteArgs( + pubName: pathParams.getString('name'), + packId: pathParams.optString('packId'), + ), ); return _i11.EditStickerPacksScreen( key: args.key, @@ -564,11 +563,10 @@ class EditStickersRoute extends _i26.PageRouteInfo { builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( - orElse: - () => EditStickersRouteArgs( - packId: pathParams.getString('packId'), - id: pathParams.optString('id'), - ), + orElse: () => EditStickersRouteArgs( + packId: pathParams.getString('packId'), + id: pathParams.optString('id'), + ), ); return _i12.EditStickersScreen( key: args.key, @@ -716,9 +714,8 @@ class NewStickerPacksRoute builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( - orElse: - () => - NewStickerPacksRouteArgs(pubName: pathParams.getString('name')), + orElse: () => + NewStickerPacksRouteArgs(pubName: pathParams.getString('name')), ); return _i11.NewStickerPacksScreen(key: args.key, pubName: args.pubName); }, @@ -759,8 +756,8 @@ class NewStickersRoute extends _i26.PageRouteInfo { builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( - orElse: - () => NewStickersRouteArgs(packId: pathParams.getString('packId')), + orElse: () => + NewStickersRouteArgs(packId: pathParams.getString('packId')), ); return _i12.NewStickersScreen(key: args.key, packId: args.packId); }, @@ -942,8 +939,8 @@ class PublisherProfileRoute builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( - orElse: - () => PublisherProfileRouteArgs(name: pathParams.getString('name')), + orElse: () => + PublisherProfileRouteArgs(name: pathParams.getString('name')), ); return _i19.PublisherProfileScreen(key: args.key, name: args.name); }, @@ -1075,11 +1072,10 @@ class StickerPackDetailRoute builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( - orElse: - () => StickerPackDetailRouteArgs( - pubName: pathParams.getString('name'), - id: pathParams.getString('packId'), - ), + orElse: () => StickerPackDetailRouteArgs( + pubName: pathParams.getString('name'), + id: pathParams.getString('packId'), + ), ); return _i12.StickerPackDetailScreen( key: args.key, diff --git a/lib/screens/account/profile.dart b/lib/screens/account/profile.dart index 4b1626f..183bea5 100644 --- a/lib/screens/account/profile.dart +++ b/lib/screens/account/profile.dart @@ -130,13 +130,13 @@ class AccountProfileScreen extends HookConsumerWidget { SliverToBoxAdapter( child: const Divider(height: 1).padding(bottom: 24), ), - if (data.profile.bio?.isNotEmpty ?? false) + if (data.profile.bio.isNotEmpty) SliverToBoxAdapter( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text('bio').tr().bold(), - Text(data.profile.bio!), + Text(data.profile.bio), ], ).padding(horizontal: 24), ), diff --git a/lib/screens/account/relationship.dart b/lib/screens/account/relationship.dart index b09d7af..2d67e67 100644 --- a/lib/screens/account/relationship.dart +++ b/lib/screens/account/relationship.dart @@ -96,7 +96,8 @@ class RelationshipListTile extends StatelessWidget { relationship.status == 0 && relationship.relatedId == currentUserId; final isWaiting = relationship.status == 0 && relationship.accountId == currentUserId; - final isEstablished = relationship.status == 1 || relationship.status == 2; + final isEstablished = + relationship.status >= 100 || relationship.status <= -100; return ListTile( contentPadding: const EdgeInsets.only(left: 16, right: 12), @@ -105,13 +106,13 @@ class RelationshipListTile extends StatelessWidget { spacing: 6, children: [ Flexible(child: Text(account.nick)), - if (relationship.status == 1) // Friend + if (relationship.status >= 100) // Friend Badge( label: Text('relationshipStatusFriend').tr(), backgroundColor: Theme.of(context).colorScheme.primary, textColor: Theme.of(context).colorScheme.onPrimary, ) - else if (relationship.status == 2) // Blocked + else if (relationship.status <= -100) // Blocked Badge( label: Text('relationshipStatusBlocked').tr(), backgroundColor: Theme.of(context).colorScheme.error, @@ -171,7 +172,7 @@ class RelationshipListTile extends StatelessWidget { icon: const Icon(Symbols.more_vert), itemBuilder: (context) => [ - if (relationship.status == 1) // If friend + if (relationship.status >= 100) // If friend PopupMenuItem( child: ListTile( leading: const Icon(Symbols.block), @@ -179,9 +180,12 @@ class RelationshipListTile extends StatelessWidget { contentPadding: EdgeInsets.zero, ), onTap: - () => onUpdateStatus?.call(relationship, 2), + () => onUpdateStatus?.call( + relationship, + -100, + ), ) - else if (relationship.status == 2) // If blocked + else if (relationship.status <= -100) // If blocked PopupMenuItem( child: ListTile( leading: const Icon(Symbols.person_add), @@ -189,7 +193,8 @@ class RelationshipListTile extends StatelessWidget { contentPadding: EdgeInsets.zero, ), onTap: - () => onUpdateStatus?.call(relationship, 1), + () => + onUpdateStatus?.call(relationship, 100), ), ], ), diff --git a/lib/screens/auth/tabs.dart b/lib/screens/auth/tabs.dart index f1d61fc..5b2f6a7 100644 --- a/lib/screens/auth/tabs.dart +++ b/lib/screens/auth/tabs.dart @@ -110,6 +110,7 @@ class TabsNavigationWidget extends HookConsumerWidget { Gap(MediaQuery.of(context).padding.top + 8), Expanded( child: NavigationRail( + minExtendedWidth: 200, extended: useExpandableLayout, selectedIndex: activeIndex, onDestinationSelected: (index) { diff --git a/lib/screens/chat/chat.dart b/lib/screens/chat/chat.dart index 0fbf97d..5fd1c71 100644 --- a/lib/screens/chat/chat.dart +++ b/lib/screens/chat/chat.dart @@ -11,6 +11,7 @@ import 'package:image_picker/image_picker.dart'; import 'package:island/models/chat.dart'; import 'package:island/models/file.dart'; import 'package:island/models/realm.dart'; +import 'package:island/pods/chat_summary.dart'; import 'package:island/pods/config.dart'; import 'package:island/pods/network.dart'; import 'package:island/route.gr.dart'; @@ -24,12 +25,13 @@ import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/realms/selection_dropdown.dart'; import 'package:island/widgets/response.dart'; import 'package:material_symbols_icons/symbols.dart'; +import 'package:relative_time/relative_time.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:styled_widget/styled_widget.dart'; part 'chat.g.dart'; -class ChatRoomListTile extends StatelessWidget { +class ChatRoomListTile extends HookConsumerWidget { final SnChatRoom room; final bool isDirect; final Widget? subtitle; @@ -46,7 +48,88 @@ class ChatRoomListTile extends StatelessWidget { }); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final summary = ref + .watch(chatSummaryProvider) + .whenData((summaries) => summaries[room.id]); + + Widget buildSubtitle() { + if (subtitle != null) return subtitle!; + + return summary.when( + data: (data) { + if (data == null) { + return isDirect && room.description == null + ? Text( + room.members!.map((e) => '@${e.account.name}').join(', '), + maxLines: 1, + ) + : Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (data.unreadCount > 0) + Text( + 'unreadMessages'.plural(data.unreadCount), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + ), + Row( + children: [ + Text( + '${data.lastMessage.sender.account.name}: ', + style: Theme.of(context).textTheme.bodySmall, + ), + Expanded( + child: Text( + data.lastMessage.content ?? 'messageNone'.tr(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Text( + RelativeTime(context).format(data.lastMessage.createdAt), + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ], + ); + }, + loading: () => const SizedBox.shrink(), + error: + (_, __) => + isDirect && room.description == null + ? Text( + room.members!.map((e) => '@${e.account.name}').join(', '), + maxLines: 1, + ) + : Text( + room.description ?? 'descriptionNone'.tr(), + maxLines: 1, + ), + ); + } + + Widget buildTrailing() { + if (trailing != null) return trailing!; + + return summary.when( + data: (data) { + if (data == null || data.unreadCount == 0) { + return const SizedBox.shrink(); + } + + return Badge(label: Text(data.unreadCount.toString())); + }, + loading: () => const SizedBox.shrink(), + error: (_, __) => const SizedBox.shrink(), + ); + } + return ListTile( leading: (isDirect && room.pictureId == null) @@ -62,19 +145,20 @@ class ChatRoomListTile extends StatelessWidget { title: Text( (isDirect && room.name == null) ? room.members!.map((e) => e.account.nick).join(', ') - : room.name!, + : room.name ?? '', ), - subtitle: - subtitle != null - ? subtitle! - : (isDirect && room.description == null) - ? Text( - room.members!.map((e) => '@${e.account.name}').join(', '), - maxLines: 1, - ) - : Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1), - trailing: trailing, - onTap: onTap, + subtitle: buildSubtitle(), + trailing: buildTrailing(), + onTap: () async { + // Clear unread count if there are unread messages + final summary = await ref.read(chatSummaryProvider.future); + if ((summary[room.id]?.unreadCount ?? 0) > 0) { + await ref + .read(chatSummaryProvider.notifier) + .clearUnreadCount(room.id); + } + onTap?.call(); + }, ); } } @@ -100,9 +184,9 @@ class ChatShellScreen extends HookConsumerWidget { if (isWide) { return Row( children: [ - SizedBox(width: 320, child: ChatListScreen(isAside: true)), + Flexible(flex: 2, child: ChatListScreen(isAside: true)), VerticalDivider(width: 1), - Expanded(child: AutoRouter()), + Flexible(flex: 4, child: AutoRouter()), ], ); } diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index edeea09..91d7004 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -311,27 +311,14 @@ class ChatRoomScreen extends HookConsumerWidget { final attachmentProgress = useState>>({}); // Function to send read receipt - void sendReadReceipt(String messageId) async { - // Get message from repository to check read status - final repository = await ref.read(messageRepositoryProvider(id).future); - final message = await repository.getMessageById(messageId); - - // Skip if message is already marked as read - if (message?.isRead ?? false) return; - + void sendReadReceipt() async { // Send websocket packet final wsState = ref.read(websocketStateProvider.notifier); wsState.sendMessage( jsonEncode( - WebSocketPacket( - type: 'messages.read', - data: {'chat_room_id': id, 'message_id': messageId}, - ), + WebSocketPacket(type: 'messages.read', data: {'chat_room_id': id}), ), ); - - // Mark as read in local database - await repository.markMessageAsRead(messageId); } // Add scroll listener for pagination @@ -357,7 +344,7 @@ class ChatRoomScreen extends HookConsumerWidget { case 'messages.new': messagesNotifier.receiveMessage(message); // Send read receipt for new message - sendReadReceipt(message.id); + sendReadReceipt(); case 'messages.update': messagesNotifier.receiveMessageUpdate(message); case 'messages.delete': @@ -365,6 +352,7 @@ class ChatRoomScreen extends HookConsumerWidget { } } + sendReadReceipt(); final subscription = ws.dataStream.listen(onMessage); return () => subscription.cancel(); }, [ws, chatRoom]); @@ -553,8 +541,6 @@ class ChatRoomScreen extends HookConsumerWidget { nextMessage == null || nextMessage.senderId != message.senderId; - sendReadReceipt(message.id); - return chatIdentity.when( skipError: true, data: diff --git a/lib/screens/creators/publishers.dart b/lib/screens/creators/publishers.dart index 47a8a4f..874df08 100644 --- a/lib/screens/creators/publishers.dart +++ b/lib/screens/creators/publishers.dart @@ -285,7 +285,7 @@ class EditPublisherScreen extends HookConsumerWidget { final user = ref.watch(userInfoProvider); nameController.text = user.value!.name; nickController.text = user.value!.nick; - bioController.text = user.value!.profile.bio ?? ''; + bioController.text = user.value!.profile.bio; picture.value = user.value!.profile.pictureId; background.value = user.value!.profile.backgroundId; } else { diff --git a/lib/screens/realm/realms.dart b/lib/screens/realm/realms.dart index 7c65eb4..a1cc4dd 100644 --- a/lib/screens/realm/realms.dart +++ b/lib/screens/realm/realms.dart @@ -120,13 +120,9 @@ class RealmListScreen extends HookConsumerWidget { ), loading: () => const Center(child: CircularProgressIndicator()), error: - (e, _) => GestureDetector( - child: Center( - child: Text('Error: $e', textAlign: TextAlign.center), - ), - onTap: () { - ref.invalidate(realmsJoinedProvider); - }, + (e, _) => ResponseErrorWidget( + error: e, + onRetry: () => ref.invalidate(realmsJoinedProvider), ), ), onRefresh: () => ref.refresh(realmsJoinedProvider.future),