👽 Support the latest API

This commit is contained in:
LittleSheep 2025-05-24 01:34:20 +08:00
parent 942b62fbff
commit fc2520b8f8
16 changed files with 426 additions and 86 deletions

View File

@ -263,5 +263,10 @@
"notifications": "Notifications", "notifications": "Notifications",
"posts": "Posts", "posts": "Posts",
"settingsBackgroundImage": "Background Image", "settingsBackgroundImage": "Background Image",
"settingsBackgroundImageClear": "Clear Background Image" "settingsBackgroundImageClear": "Clear Background Image",
"messageNone": "No content to display",
"unreadMessages": {
"one": "{} unread message",
"other": "{} unread messages"
}
} }

View File

@ -456,12 +456,4 @@ class MessageRepository {
rethrow; rethrow;
} }
} }
Future<void> markMessageAsRead(String messageId) async {
try {
await _database.markMessageAsRead(messageId);
} catch (e) {
showErrorAlert(e);
}
}
} }

View File

@ -27,7 +27,10 @@ import 'package:flutter_native_splash/flutter_native_splash.dart';
void main() async { void main() async {
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
}
await EasyLocalization.ensureInitialized(); await EasyLocalization.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
@ -51,7 +54,9 @@ void main() async {
} }
} }
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
FlutterNativeSplash.remove(); FlutterNativeSplash.remove();
}
runApp( runApp(
ProviderScope( ProviderScope(

View File

@ -95,6 +95,17 @@ abstract class SnChatMember with _$SnChatMember {
_$SnChatMemberFromJson(json); _$SnChatMemberFromJson(json);
} }
@freezed
abstract class SnChatSummary with _$SnChatSummary {
const factory SnChatSummary({
required int unreadCount,
required SnChatMessage lastMessage,
}) = _SnChatSummary;
factory SnChatSummary.fromJson(Map<String, dynamic> json) =>
_$SnChatSummaryFromJson(json);
}
class MessageChangeAction { class MessageChangeAction {
static const String create = "create"; static const String create = "create";
static const String update = "update"; static const String update = "update";

View File

@ -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<SnChatSummary> get copyWith => _$SnChatSummaryCopyWithImpl<SnChatSummary>(this as SnChatSummary, _$identity);
/// Serializes this SnChatSummary to a JSON map.
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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 /// @nodoc
mixin _$MessageChange { mixin _$MessageChange {

View File

@ -188,6 +188,20 @@ Map<String, dynamic> _$SnChatMemberToJson(_SnChatMember instance) =>
'is_bot': instance.isBot, 'is_bot': instance.isBot,
}; };
_SnChatSummary _$SnChatSummaryFromJson(Map<String, dynamic> json) =>
_SnChatSummary(
unreadCount: (json['unread_count'] as num).toInt(),
lastMessage: SnChatMessage.fromJson(
json['last_message'] as Map<String, dynamic>,
),
);
Map<String, dynamic> _$SnChatSummaryToJson(_SnChatSummary instance) =>
<String, dynamic>{
'unread_count': instance.unreadCount,
'last_message': instance.lastMessage.toJson(),
};
_MessageChange _$MessageChangeFromJson(Map<String, dynamic> json) => _MessageChange _$MessageChangeFromJson(Map<String, dynamic> json) =>
_MessageChange( _MessageChange(
messageId: json['message_id'] as String, messageId: json['message_id'] as String,

View File

@ -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<Map<String, SnChatSummary>> build() async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/chat/summary');
final Map<String, dynamic> data = resp.data;
return data.map(
(key, value) => MapEntry(key, SnChatSummary.fromJson(value)),
);
}
Future<void> 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,
),
});
}
});
}
}

View File

@ -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<String, SnChatSummary>
>.internal(
ChatSummary.new,
name: r'chatSummaryProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$chatSummaryHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$ChatSummary = AutoDisposeAsyncNotifier<Map<String, SnChatSummary>>;
// 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

View File

@ -61,8 +61,8 @@ class AccountProfileRoute extends _i26.PageRouteInfo<AccountProfileRouteArgs> {
builder: (data) { builder: (data) {
final pathParams = data.inheritedPathParams; final pathParams = data.inheritedPathParams;
final args = data.argsAs<AccountProfileRouteArgs>( final args = data.argsAs<AccountProfileRouteArgs>(
orElse: orElse: () =>
() => AccountProfileRouteArgs(name: pathParams.getString('name')), AccountProfileRouteArgs(name: pathParams.getString('name')),
); );
return _i1.AccountProfileScreen(key: args.key, name: args.name); return _i1.AccountProfileScreen(key: args.key, name: args.name);
}, },
@ -508,8 +508,7 @@ class EditStickerPacksRoute
builder: (data) { builder: (data) {
final pathParams = data.inheritedPathParams; final pathParams = data.inheritedPathParams;
final args = data.argsAs<EditStickerPacksRouteArgs>( final args = data.argsAs<EditStickerPacksRouteArgs>(
orElse: orElse: () => EditStickerPacksRouteArgs(
() => EditStickerPacksRouteArgs(
pubName: pathParams.getString('name'), pubName: pathParams.getString('name'),
packId: pathParams.optString('packId'), packId: pathParams.optString('packId'),
), ),
@ -564,8 +563,7 @@ class EditStickersRoute extends _i26.PageRouteInfo<EditStickersRouteArgs> {
builder: (data) { builder: (data) {
final pathParams = data.inheritedPathParams; final pathParams = data.inheritedPathParams;
final args = data.argsAs<EditStickersRouteArgs>( final args = data.argsAs<EditStickersRouteArgs>(
orElse: orElse: () => EditStickersRouteArgs(
() => EditStickersRouteArgs(
packId: pathParams.getString('packId'), packId: pathParams.getString('packId'),
id: pathParams.optString('id'), id: pathParams.optString('id'),
), ),
@ -716,8 +714,7 @@ class NewStickerPacksRoute
builder: (data) { builder: (data) {
final pathParams = data.inheritedPathParams; final pathParams = data.inheritedPathParams;
final args = data.argsAs<NewStickerPacksRouteArgs>( final args = data.argsAs<NewStickerPacksRouteArgs>(
orElse: orElse: () =>
() =>
NewStickerPacksRouteArgs(pubName: pathParams.getString('name')), NewStickerPacksRouteArgs(pubName: pathParams.getString('name')),
); );
return _i11.NewStickerPacksScreen(key: args.key, pubName: args.pubName); return _i11.NewStickerPacksScreen(key: args.key, pubName: args.pubName);
@ -759,8 +756,8 @@ class NewStickersRoute extends _i26.PageRouteInfo<NewStickersRouteArgs> {
builder: (data) { builder: (data) {
final pathParams = data.inheritedPathParams; final pathParams = data.inheritedPathParams;
final args = data.argsAs<NewStickersRouteArgs>( final args = data.argsAs<NewStickersRouteArgs>(
orElse: orElse: () =>
() => NewStickersRouteArgs(packId: pathParams.getString('packId')), NewStickersRouteArgs(packId: pathParams.getString('packId')),
); );
return _i12.NewStickersScreen(key: args.key, packId: args.packId); return _i12.NewStickersScreen(key: args.key, packId: args.packId);
}, },
@ -942,8 +939,8 @@ class PublisherProfileRoute
builder: (data) { builder: (data) {
final pathParams = data.inheritedPathParams; final pathParams = data.inheritedPathParams;
final args = data.argsAs<PublisherProfileRouteArgs>( final args = data.argsAs<PublisherProfileRouteArgs>(
orElse: orElse: () =>
() => PublisherProfileRouteArgs(name: pathParams.getString('name')), PublisherProfileRouteArgs(name: pathParams.getString('name')),
); );
return _i19.PublisherProfileScreen(key: args.key, name: args.name); return _i19.PublisherProfileScreen(key: args.key, name: args.name);
}, },
@ -1075,8 +1072,7 @@ class StickerPackDetailRoute
builder: (data) { builder: (data) {
final pathParams = data.inheritedPathParams; final pathParams = data.inheritedPathParams;
final args = data.argsAs<StickerPackDetailRouteArgs>( final args = data.argsAs<StickerPackDetailRouteArgs>(
orElse: orElse: () => StickerPackDetailRouteArgs(
() => StickerPackDetailRouteArgs(
pubName: pathParams.getString('name'), pubName: pathParams.getString('name'),
id: pathParams.getString('packId'), id: pathParams.getString('packId'),
), ),

View File

@ -130,13 +130,13 @@ class AccountProfileScreen extends HookConsumerWidget {
SliverToBoxAdapter( SliverToBoxAdapter(
child: const Divider(height: 1).padding(bottom: 24), child: const Divider(height: 1).padding(bottom: 24),
), ),
if (data.profile.bio?.isNotEmpty ?? false) if (data.profile.bio.isNotEmpty)
SliverToBoxAdapter( SliverToBoxAdapter(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Text('bio').tr().bold(), Text('bio').tr().bold(),
Text(data.profile.bio!), Text(data.profile.bio),
], ],
).padding(horizontal: 24), ).padding(horizontal: 24),
), ),

View File

@ -96,7 +96,8 @@ class RelationshipListTile extends StatelessWidget {
relationship.status == 0 && relationship.relatedId == currentUserId; relationship.status == 0 && relationship.relatedId == currentUserId;
final isWaiting = final isWaiting =
relationship.status == 0 && relationship.accountId == currentUserId; relationship.status == 0 && relationship.accountId == currentUserId;
final isEstablished = relationship.status == 1 || relationship.status == 2; final isEstablished =
relationship.status >= 100 || relationship.status <= -100;
return ListTile( return ListTile(
contentPadding: const EdgeInsets.only(left: 16, right: 12), contentPadding: const EdgeInsets.only(left: 16, right: 12),
@ -105,13 +106,13 @@ class RelationshipListTile extends StatelessWidget {
spacing: 6, spacing: 6,
children: [ children: [
Flexible(child: Text(account.nick)), Flexible(child: Text(account.nick)),
if (relationship.status == 1) // Friend if (relationship.status >= 100) // Friend
Badge( Badge(
label: Text('relationshipStatusFriend').tr(), label: Text('relationshipStatusFriend').tr(),
backgroundColor: Theme.of(context).colorScheme.primary, backgroundColor: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary, textColor: Theme.of(context).colorScheme.onPrimary,
) )
else if (relationship.status == 2) // Blocked else if (relationship.status <= -100) // Blocked
Badge( Badge(
label: Text('relationshipStatusBlocked').tr(), label: Text('relationshipStatusBlocked').tr(),
backgroundColor: Theme.of(context).colorScheme.error, backgroundColor: Theme.of(context).colorScheme.error,
@ -171,7 +172,7 @@ class RelationshipListTile extends StatelessWidget {
icon: const Icon(Symbols.more_vert), icon: const Icon(Symbols.more_vert),
itemBuilder: itemBuilder:
(context) => [ (context) => [
if (relationship.status == 1) // If friend if (relationship.status >= 100) // If friend
PopupMenuItem( PopupMenuItem(
child: ListTile( child: ListTile(
leading: const Icon(Symbols.block), leading: const Icon(Symbols.block),
@ -179,9 +180,12 @@ class RelationshipListTile extends StatelessWidget {
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
), ),
onTap: onTap:
() => onUpdateStatus?.call(relationship, 2), () => onUpdateStatus?.call(
relationship,
-100,
),
) )
else if (relationship.status == 2) // If blocked else if (relationship.status <= -100) // If blocked
PopupMenuItem( PopupMenuItem(
child: ListTile( child: ListTile(
leading: const Icon(Symbols.person_add), leading: const Icon(Symbols.person_add),
@ -189,7 +193,8 @@ class RelationshipListTile extends StatelessWidget {
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
), ),
onTap: onTap:
() => onUpdateStatus?.call(relationship, 1), () =>
onUpdateStatus?.call(relationship, 100),
), ),
], ],
), ),

View File

@ -110,6 +110,7 @@ class TabsNavigationWidget extends HookConsumerWidget {
Gap(MediaQuery.of(context).padding.top + 8), Gap(MediaQuery.of(context).padding.top + 8),
Expanded( Expanded(
child: NavigationRail( child: NavigationRail(
minExtendedWidth: 200,
extended: useExpandableLayout, extended: useExpandableLayout,
selectedIndex: activeIndex, selectedIndex: activeIndex,
onDestinationSelected: (index) { onDestinationSelected: (index) {

View File

@ -11,6 +11,7 @@ import 'package:image_picker/image_picker.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/models/realm.dart'; import 'package:island/models/realm.dart';
import 'package:island/pods/chat_summary.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/route.gr.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/realms/selection_dropdown.dart';
import 'package:island/widgets/response.dart'; import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:relative_time/relative_time.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
part 'chat.g.dart'; part 'chat.g.dart';
class ChatRoomListTile extends StatelessWidget { class ChatRoomListTile extends HookConsumerWidget {
final SnChatRoom room; final SnChatRoom room;
final bool isDirect; final bool isDirect;
final Widget? subtitle; final Widget? subtitle;
@ -46,7 +48,88 @@ class ChatRoomListTile extends StatelessWidget {
}); });
@override @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( return ListTile(
leading: leading:
(isDirect && room.pictureId == null) (isDirect && room.pictureId == null)
@ -62,19 +145,20 @@ class ChatRoomListTile extends StatelessWidget {
title: Text( title: Text(
(isDirect && room.name == null) (isDirect && room.name == null)
? room.members!.map((e) => e.account.nick).join(', ') ? room.members!.map((e) => e.account.nick).join(', ')
: room.name!, : room.name ?? '',
), ),
subtitle: subtitle: buildSubtitle(),
subtitle != null trailing: buildTrailing(),
? subtitle! onTap: () async {
: (isDirect && room.description == null) // Clear unread count if there are unread messages
? Text( final summary = await ref.read(chatSummaryProvider.future);
room.members!.map((e) => '@${e.account.name}').join(', '), if ((summary[room.id]?.unreadCount ?? 0) > 0) {
maxLines: 1, await ref
) .read(chatSummaryProvider.notifier)
: Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1), .clearUnreadCount(room.id);
trailing: trailing, }
onTap: onTap, onTap?.call();
},
); );
} }
} }
@ -100,9 +184,9 @@ class ChatShellScreen extends HookConsumerWidget {
if (isWide) { if (isWide) {
return Row( return Row(
children: [ children: [
SizedBox(width: 320, child: ChatListScreen(isAside: true)), Flexible(flex: 2, child: ChatListScreen(isAside: true)),
VerticalDivider(width: 1), VerticalDivider(width: 1),
Expanded(child: AutoRouter()), Flexible(flex: 4, child: AutoRouter()),
], ],
); );
} }

View File

@ -311,27 +311,14 @@ class ChatRoomScreen extends HookConsumerWidget {
final attachmentProgress = useState<Map<String, Map<int, double>>>({}); final attachmentProgress = useState<Map<String, Map<int, double>>>({});
// Function to send read receipt // Function to send read receipt
void sendReadReceipt(String messageId) async { void sendReadReceipt() 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;
// Send websocket packet // Send websocket packet
final wsState = ref.read(websocketStateProvider.notifier); final wsState = ref.read(websocketStateProvider.notifier);
wsState.sendMessage( wsState.sendMessage(
jsonEncode( jsonEncode(
WebSocketPacket( WebSocketPacket(type: 'messages.read', data: {'chat_room_id': id}),
type: 'messages.read',
data: {'chat_room_id': id, 'message_id': messageId},
),
), ),
); );
// Mark as read in local database
await repository.markMessageAsRead(messageId);
} }
// Add scroll listener for pagination // Add scroll listener for pagination
@ -357,7 +344,7 @@ class ChatRoomScreen extends HookConsumerWidget {
case 'messages.new': case 'messages.new':
messagesNotifier.receiveMessage(message); messagesNotifier.receiveMessage(message);
// Send read receipt for new message // Send read receipt for new message
sendReadReceipt(message.id); sendReadReceipt();
case 'messages.update': case 'messages.update':
messagesNotifier.receiveMessageUpdate(message); messagesNotifier.receiveMessageUpdate(message);
case 'messages.delete': case 'messages.delete':
@ -365,6 +352,7 @@ class ChatRoomScreen extends HookConsumerWidget {
} }
} }
sendReadReceipt();
final subscription = ws.dataStream.listen(onMessage); final subscription = ws.dataStream.listen(onMessage);
return () => subscription.cancel(); return () => subscription.cancel();
}, [ws, chatRoom]); }, [ws, chatRoom]);
@ -553,8 +541,6 @@ class ChatRoomScreen extends HookConsumerWidget {
nextMessage == null || nextMessage == null ||
nextMessage.senderId != message.senderId; nextMessage.senderId != message.senderId;
sendReadReceipt(message.id);
return chatIdentity.when( return chatIdentity.when(
skipError: true, skipError: true,
data: data:

View File

@ -285,7 +285,7 @@ class EditPublisherScreen extends HookConsumerWidget {
final user = ref.watch(userInfoProvider); final user = ref.watch(userInfoProvider);
nameController.text = user.value!.name; nameController.text = user.value!.name;
nickController.text = user.value!.nick; nickController.text = user.value!.nick;
bioController.text = user.value!.profile.bio ?? ''; bioController.text = user.value!.profile.bio;
picture.value = user.value!.profile.pictureId; picture.value = user.value!.profile.pictureId;
background.value = user.value!.profile.backgroundId; background.value = user.value!.profile.backgroundId;
} else { } else {

View File

@ -120,13 +120,9 @@ class RealmListScreen extends HookConsumerWidget {
), ),
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: error:
(e, _) => GestureDetector( (e, _) => ResponseErrorWidget(
child: Center( error: e,
child: Text('Error: $e', textAlign: TextAlign.center), onRetry: () => ref.invalidate(realmsJoinedProvider),
),
onTap: () {
ref.invalidate(realmsJoinedProvider);
},
), ),
), ),
onRefresh: () => ref.refresh(realmsJoinedProvider.future), onRefresh: () => ref.refresh(realmsJoinedProvider.future),