👽 Support the latest API
This commit is contained in:
parent
942b62fbff
commit
fc2520b8f8
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -456,12 +456,4 @@ class MessageRepository {
|
|||||||
rethrow;
|
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 {
|
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(
|
||||||
|
@ -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";
|
||||||
|
@ -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 {
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
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) {
|
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'),
|
||||||
),
|
),
|
||||||
|
@ -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),
|
||||||
),
|
),
|
||||||
|
@ -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),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -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) {
|
||||||
|
@ -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()),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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:
|
||||||
|
@ -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 {
|
||||||
|
@ -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),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user