👽 Support the latest API
This commit is contained in:
parent
942b62fbff
commit
fc2520b8f8
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -456,12 +456,4 @@ class MessageRepository {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> markMessageAsRead(String messageId) async {
|
||||
try {
|
||||
await _database.markMessageAsRead(messageId);
|
||||
} catch (e) {
|
||||
showErrorAlert(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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<String, dynamic> json) =>
|
||||
_$SnChatSummaryFromJson(json);
|
||||
}
|
||||
|
||||
class MessageChangeAction {
|
||||
static const String create = "create";
|
||||
static const String update = "update";
|
||||
|
@ -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
|
||||
mixin _$MessageChange {
|
||||
|
||||
|
@ -188,6 +188,20 @@ Map<String, dynamic> _$SnChatMemberToJson(_SnChatMember instance) =>
|
||||
'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(
|
||||
messageId: json['message_id'] as String,
|
||||
|
64
lib/pods/chat_summary.dart
Normal file
64
lib/pods/chat_summary.dart
Normal 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,
|
||||
),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
27
lib/pods/chat_summary.g.dart
Normal file
27
lib/pods/chat_summary.g.dart
Normal 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
|
@ -61,8 +61,8 @@ class AccountProfileRoute extends _i26.PageRouteInfo<AccountProfileRouteArgs> {
|
||||
builder: (data) {
|
||||
final pathParams = data.inheritedPathParams;
|
||||
final args = data.argsAs<AccountProfileRouteArgs>(
|
||||
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<EditStickerPacksRouteArgs>(
|
||||
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<EditStickersRouteArgs> {
|
||||
builder: (data) {
|
||||
final pathParams = data.inheritedPathParams;
|
||||
final args = data.argsAs<EditStickersRouteArgs>(
|
||||
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<NewStickerPacksRouteArgs>(
|
||||
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<NewStickersRouteArgs> {
|
||||
builder: (data) {
|
||||
final pathParams = data.inheritedPathParams;
|
||||
final args = data.argsAs<NewStickersRouteArgs>(
|
||||
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<PublisherProfileRouteArgs>(
|
||||
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<StickerPackDetailRouteArgs>(
|
||||
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,
|
||||
|
@ -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),
|
||||
),
|
||||
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -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) {
|
||||
|
@ -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()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -311,27 +311,14 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
final attachmentProgress = useState<Map<String, Map<int, double>>>({});
|
||||
|
||||
// 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:
|
||||
|
@ -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 {
|
||||
|
@ -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),
|
||||
|
Loading…
x
Reference in New Issue
Block a user