♻️ Use controller instead of state to manage history
This commit is contained in:
		
							
								
								
									
										48
									
								
								lib/controllers/chat_history_controller.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								lib/controllers/chat_history_controller.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:solian/models/channel.dart'; | ||||
| import 'package:solian/models/message.dart'; | ||||
| import 'package:solian/providers/message/helper.dart'; | ||||
| import 'package:solian/providers/message/history.dart'; | ||||
|  | ||||
| class ChatHistoryController { | ||||
|   late final MessageHistoryDb database; | ||||
|  | ||||
|   final RxList<LocalMessage> currentHistory = RxList.empty(growable: true); | ||||
|   final RxInt totalHistoryCount = 0.obs; | ||||
|  | ||||
|   initialize() async { | ||||
|     database = await createHistoryDb(); | ||||
|     currentHistory.clear(); | ||||
|   } | ||||
|  | ||||
|   Future<void> getMessages(Channel channel, String scope) async { | ||||
|     totalHistoryCount.value = await database.syncMessages(channel, scope: scope); | ||||
|     await syncHistory(channel); | ||||
|   } | ||||
|  | ||||
|   Future<void> syncHistory(Channel channel) async { | ||||
|     currentHistory.replaceRange(0, currentHistory.length, | ||||
|         await database.localMessages.findAllByChannel(channel.id)); | ||||
|   } | ||||
|  | ||||
|   receiveMessage(Message remote) async { | ||||
|     final entry = await database.receiveMessage(remote); | ||||
|     totalHistoryCount.value++; | ||||
|     currentHistory.add(entry); | ||||
|   } | ||||
|  | ||||
|   void replaceMessage(Message remote) async { | ||||
|     final entry = await database.replaceMessage(remote); | ||||
|     currentHistory.replaceRange( | ||||
|       0, | ||||
|       currentHistory.length, | ||||
|       currentHistory.map((x) => x.id == entry.id ? entry : x), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void burnMessage(int id) async { | ||||
|     await database.burnMessage(id); | ||||
|     totalHistoryCount.value--; | ||||
|     currentHistory.removeWhere((x) => x.id == id); | ||||
|   } | ||||
| } | ||||
| @@ -81,24 +81,24 @@ class AccountBadge { | ||||
|   }); | ||||
|  | ||||
|   factory AccountBadge.fromJson(Map<String, dynamic> json) => AccountBadge( | ||||
|         id: json["id"], | ||||
|         accountId: json["account_id"], | ||||
|         updatedAt: DateTime.parse(json["updated_at"]), | ||||
|         createdAt: DateTime.parse(json["created_at"]), | ||||
|         deletedAt: json["deleted_at"] != null | ||||
|             ? DateTime.parse(json["deleted_at"]) | ||||
|         id: json['id'], | ||||
|         accountId: json['account_id'], | ||||
|         updatedAt: DateTime.parse(json['updated_at']), | ||||
|         createdAt: DateTime.parse(json['created_at']), | ||||
|         deletedAt: json['deleted_at'] != null | ||||
|             ? DateTime.parse(json['deleted_at']) | ||||
|             : null, | ||||
|         metadata: json["metadata"], | ||||
|         type: json["type"], | ||||
|         metadata: json['metadata'], | ||||
|         type: json['type'], | ||||
|       ); | ||||
|  | ||||
|   Map<String, dynamic> toJson() => { | ||||
|         "id": id, | ||||
|         "account_id": accountId, | ||||
|         "created_at": createdAt.toIso8601String(), | ||||
|         "updated_at": updatedAt.toIso8601String(), | ||||
|         "deleted_at": deletedAt?.toIso8601String(), | ||||
|         "metadata": metadata, | ||||
|         "type": type, | ||||
|         'id': id, | ||||
|         'account_id': accountId, | ||||
|         'created_at': createdAt.toIso8601String(), | ||||
|         'updated_at': updatedAt.toIso8601String(), | ||||
|         'deleted_at': deletedAt?.toIso8601String(), | ||||
|         'metadata': metadata, | ||||
|         'type': type, | ||||
|       }; | ||||
| } | ||||
|   | ||||
| @@ -38,40 +38,40 @@ class Attachment { | ||||
|   }); | ||||
|  | ||||
|   factory Attachment.fromJson(Map<String, dynamic> json) => Attachment( | ||||
|     id: json["id"], | ||||
|     createdAt: DateTime.parse(json["created_at"]), | ||||
|     updatedAt: DateTime.parse(json["updated_at"]), | ||||
|     deletedAt: json["deleted_at"], | ||||
|     uuid: json["uuid"], | ||||
|     size: json["size"], | ||||
|     name: json["name"], | ||||
|     alt: json["alt"], | ||||
|     usage: json["usage"], | ||||
|     mimetype: json["mimetype"], | ||||
|     hash: json["hash"], | ||||
|     destination: json["destination"], | ||||
|     metadata: json["metadata"], | ||||
|     isMature: json["is_mature"], | ||||
|     account: Account.fromJson(json["account"]), | ||||
|     accountId: json["account_id"], | ||||
|     id: json['id'], | ||||
|     createdAt: DateTime.parse(json['created_at']), | ||||
|     updatedAt: DateTime.parse(json['updated_at']), | ||||
|     deletedAt: json['deleted_at'], | ||||
|     uuid: json['uuid'], | ||||
|     size: json['size'], | ||||
|     name: json['name'], | ||||
|     alt: json['alt'], | ||||
|     usage: json['usage'], | ||||
|     mimetype: json['mimetype'], | ||||
|     hash: json['hash'], | ||||
|     destination: json['destination'], | ||||
|     metadata: json['metadata'], | ||||
|     isMature: json['is_mature'], | ||||
|     account: Account.fromJson(json['account']), | ||||
|     accountId: json['account_id'], | ||||
|   ); | ||||
|  | ||||
|   Map<String, dynamic> toJson() => { | ||||
|     "id": id, | ||||
|     "created_at": createdAt.toIso8601String(), | ||||
|     "updated_at": updatedAt.toIso8601String(), | ||||
|     "deleted_at": deletedAt, | ||||
|     "uuid": uuid, | ||||
|     "size": size, | ||||
|     "name": name, | ||||
|     "alt": alt, | ||||
|     "usage": usage, | ||||
|     "mimetype": mimetype, | ||||
|     "hash": hash, | ||||
|     "destination": destination, | ||||
|     "metadata": metadata, | ||||
|     "is_mature": isMature, | ||||
|     "account": account.toJson(), | ||||
|     "account_id": accountId, | ||||
|     'id': id, | ||||
|     'created_at': createdAt.toIso8601String(), | ||||
|     'updated_at': updatedAt.toIso8601String(), | ||||
|     'deleted_at': deletedAt, | ||||
|     'uuid': uuid, | ||||
|     'size': size, | ||||
|     'name': name, | ||||
|     'alt': alt, | ||||
|     'usage': usage, | ||||
|     'mimetype': mimetype, | ||||
|     'hash': hash, | ||||
|     'destination': destination, | ||||
|     'metadata': metadata, | ||||
|     'is_mature': isMature, | ||||
|     'account': account.toJson(), | ||||
|     'account_id': accountId, | ||||
|   }; | ||||
| } | ||||
| @@ -44,8 +44,8 @@ class Message { | ||||
|         deletedAt: json['deleted_at'], | ||||
|         content: json['content'], | ||||
|         type: json['type'], | ||||
|         attachments: json["attachments"] != null | ||||
|             ? List<int>.from(json["attachments"]) | ||||
|         attachments: json['attachments'] != null | ||||
|             ? List<int>.from(json['attachments']) | ||||
|             : null, | ||||
|         channel: | ||||
|             json['channel'] != null ? Channel.fromJson(json['channel']) : null, | ||||
|   | ||||
| @@ -53,36 +53,36 @@ class Post { | ||||
|   }); | ||||
|  | ||||
|   factory Post.fromJson(Map<String, dynamic> json) => Post( | ||||
|         id: json["id"], | ||||
|         createdAt: DateTime.parse(json["created_at"]), | ||||
|         updatedAt: DateTime.parse(json["updated_at"]), | ||||
|         deletedAt: json["deleted_at"] != null | ||||
|         id: json['id'], | ||||
|         createdAt: DateTime.parse(json['created_at']), | ||||
|         updatedAt: DateTime.parse(json['updated_at']), | ||||
|         deletedAt: json['deleted_at'] != null | ||||
|             ? DateTime.parse(json['deleted_at']) | ||||
|             : null, | ||||
|         alias: json["alias"], | ||||
|         content: json["content"], | ||||
|         tags: json["tags"], | ||||
|         categories: json["categories"], | ||||
|         reactions: json["reactions"], | ||||
|         replies: json["replies"], | ||||
|         attachments: json["attachments"] != null | ||||
|             ? List<int>.from(json["attachments"]) | ||||
|         alias: json['alias'], | ||||
|         content: json['content'], | ||||
|         tags: json['tags'], | ||||
|         categories: json['categories'], | ||||
|         reactions: json['reactions'], | ||||
|         replies: json['replies'], | ||||
|         attachments: json['attachments'] != null | ||||
|             ? List<int>.from(json['attachments']) | ||||
|             : null, | ||||
|         replyId: json["reply_id"], | ||||
|         repostId: json["repost_id"], | ||||
|         realmId: json["realm_id"], | ||||
|         replyId: json['reply_id'], | ||||
|         repostId: json['repost_id'], | ||||
|         realmId: json['realm_id'], | ||||
|         replyTo: | ||||
|             json["reply_to"] != null ? Post.fromJson(json["reply_to"]) : null, | ||||
|             json['reply_to'] != null ? Post.fromJson(json['reply_to']) : null, | ||||
|         repostTo: | ||||
|             json["repost_to"] != null ? Post.fromJson(json["repost_to"]) : null, | ||||
|         realm: json["realm"] != null ? Realm.fromJson(json["realm"]) : null, | ||||
|         publishedAt: json["published_at"] != null ? DateTime.parse(json["published_at"]) : null, | ||||
|         authorId: json["author_id"], | ||||
|         author: Account.fromJson(json["author"]), | ||||
|         replyCount: json["reply_count"], | ||||
|         reactionCount: json["reaction_count"], | ||||
|         reactionList: json["reaction_list"] != null | ||||
|             ? json["reaction_list"] | ||||
|             json['repost_to'] != null ? Post.fromJson(json['repost_to']) : null, | ||||
|         realm: json['realm'] != null ? Realm.fromJson(json['realm']) : null, | ||||
|         publishedAt: json['published_at'] != null ? DateTime.parse(json['published_at']) : null, | ||||
|         authorId: json['author_id'], | ||||
|         author: Account.fromJson(json['author']), | ||||
|         replyCount: json['reply_count'], | ||||
|         reactionCount: json['reaction_count'], | ||||
|         reactionList: json['reaction_list'] != null | ||||
|             ? json['reaction_list'] | ||||
|                 .map((key, value) => MapEntry( | ||||
|                     key, | ||||
|                     int.tryParse(value.toString()) ?? | ||||
| @@ -92,28 +92,28 @@ class Post { | ||||
|       ); | ||||
|  | ||||
|   Map<String, dynamic> toJson() => { | ||||
|         "id": id, | ||||
|         "created_at": createdAt.toIso8601String(), | ||||
|         "updated_at": updatedAt.toIso8601String(), | ||||
|         "deleted_at": deletedAt, | ||||
|         "alias": alias, | ||||
|         "content": content, | ||||
|         "tags": tags, | ||||
|         "categories": categories, | ||||
|         "reactions": reactions, | ||||
|         "replies": replies, | ||||
|         "attachments": attachments, | ||||
|         "reply_id": replyId, | ||||
|         "repost_id": repostId, | ||||
|         "realm_id": realmId, | ||||
|         "reply_to": replyTo?.toJson(), | ||||
|         "repost_to": repostTo?.toJson(), | ||||
|         "realm": realm?.toJson(), | ||||
|         "published_at": publishedAt?.toIso8601String(), | ||||
|         "author_id": authorId, | ||||
|         "author": author.toJson(), | ||||
|         "reply_count": replyCount, | ||||
|         "reaction_count": reactionCount, | ||||
|         "reaction_list": reactionList, | ||||
|         'id': id, | ||||
|         'created_at': createdAt.toIso8601String(), | ||||
|         'updated_at': updatedAt.toIso8601String(), | ||||
|         'deleted_at': deletedAt, | ||||
|         'alias': alias, | ||||
|         'content': content, | ||||
|         'tags': tags, | ||||
|         'categories': categories, | ||||
|         'reactions': reactions, | ||||
|         'replies': replies, | ||||
|         'attachments': attachments, | ||||
|         'reply_id': replyId, | ||||
|         'repost_id': repostId, | ||||
|         'realm_id': realmId, | ||||
|         'reply_to': replyTo?.toJson(), | ||||
|         'repost_to': repostTo?.toJson(), | ||||
|         'realm': realm?.toJson(), | ||||
|         'published_at': publishedAt?.toIso8601String(), | ||||
|         'author_id': authorId, | ||||
|         'author': author.toJson(), | ||||
|         'reply_count': replyCount, | ||||
|         'reaction_count': reactionCount, | ||||
|         'reaction_list': reactionList, | ||||
|       }; | ||||
| } | ||||
|   | ||||
| @@ -190,10 +190,10 @@ class AccountProvider extends GetxController { | ||||
|     } | ||||
|  | ||||
|     if (PlatformInfo.isIOS || PlatformInfo.isMacOS) { | ||||
|       provider = "apple"; | ||||
|       provider = 'apple'; | ||||
|       token = await FirebaseMessaging.instance.getAPNSToken(); | ||||
|     } else { | ||||
|       provider = "firebase"; | ||||
|       provider = 'firebase'; | ||||
|       token = await FirebaseMessaging.instance.getToken(); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -79,7 +79,7 @@ class AuthProvider extends GetConnect { | ||||
|  | ||||
|     if (credentials!.isExpired) { | ||||
|       await refreshCredentials(); | ||||
|       log("Refreshed credentials at ${DateTime.now()}"); | ||||
|       log('Refreshed credentials at ${DateTime.now()}'); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -13,19 +13,23 @@ Future<MessageHistoryDb> createHistoryDb() async { | ||||
|  | ||||
| extension MessageHistoryHelper on MessageHistoryDb { | ||||
|   receiveMessage(Message remote) async { | ||||
|     await localMessages.insert(LocalMessage( | ||||
|     final entry = LocalMessage( | ||||
|       remote.id, | ||||
|       remote, | ||||
|       remote.channelId, | ||||
|     )); | ||||
|     ); | ||||
|     await localMessages.insert(entry); | ||||
|     return entry; | ||||
|   } | ||||
|  | ||||
|   replaceMessage(Message remote) async { | ||||
|     await localMessages.update(LocalMessage( | ||||
|     final entry = LocalMessage( | ||||
|       remote.id, | ||||
|       remote, | ||||
|       remote.channelId, | ||||
|     )); | ||||
|     ); | ||||
|     await localMessages.update(entry); | ||||
|     return entry; | ||||
|   } | ||||
|  | ||||
|   burnMessage(int id) async { | ||||
| @@ -38,18 +42,22 @@ extension MessageHistoryHelper on MessageHistoryDb { | ||||
|     final data = await _getRemoteMessages( | ||||
|       channel, | ||||
|       scope, | ||||
|       remainBreath: 3, | ||||
|       remainBreath: 10, | ||||
|       offset: offset, | ||||
|       onBrake: (items) { | ||||
|         return items.any((x) => x.id == lastOne?.id); | ||||
|       }, | ||||
|     ); | ||||
|     await localMessages.insertBulk( | ||||
|       data.map((x) => LocalMessage(x.id, x, x.channelId)).toList(), | ||||
|     ); | ||||
|     if (data != null) { | ||||
|       await localMessages.insertBulk( | ||||
|         data.$1.map((x) => LocalMessage(x.id, x, x.channelId)).toList(), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return data?.$2 ?? 0; | ||||
|   } | ||||
|  | ||||
|   Future<List<Message>> _getRemoteMessages( | ||||
|   Future<(List<Message>, int)?> _getRemoteMessages( | ||||
|     Channel channel, | ||||
|     String scope, { | ||||
|     required int remainBreath, | ||||
| @@ -58,11 +66,11 @@ extension MessageHistoryHelper on MessageHistoryDb { | ||||
|     offset = 0, | ||||
|   }) async { | ||||
|     if (remainBreath <= 0) { | ||||
|       return List.empty(); | ||||
|       return null; | ||||
|     } | ||||
|  | ||||
|     final AuthProvider auth = Get.find(); | ||||
|     if (!await auth.isAuthorized) return List.empty(); | ||||
|     if (!await auth.isAuthorized) return null; | ||||
|  | ||||
|     final client = auth.configureClient('messaging'); | ||||
|  | ||||
| @@ -78,18 +86,20 @@ extension MessageHistoryHelper on MessageHistoryDb { | ||||
|         response.data?.map((e) => Message.fromJson(e)).toList() ?? List.empty(); | ||||
|  | ||||
|     if (onBrake != null && onBrake(result)) { | ||||
|       return result; | ||||
|       return (result, response.count); | ||||
|     } | ||||
|  | ||||
|     final expandResult = await _getRemoteMessages( | ||||
|       channel, | ||||
|       scope, | ||||
|       remainBreath: remainBreath - 1, | ||||
|       take: take, | ||||
|       offset: offset + result.length, | ||||
|     ); | ||||
|     final expandResult = (await _getRemoteMessages( | ||||
|           channel, | ||||
|           scope, | ||||
|           remainBreath: remainBreath - 1, | ||||
|           take: take, | ||||
|           offset: offset + result.length, | ||||
|         )) | ||||
|             ?.$1 ?? | ||||
|         List.empty(); | ||||
|  | ||||
|     return [...result, ...expandResult]; | ||||
|     return ([...result, ...expandResult], response.count); | ||||
|   } | ||||
|  | ||||
|   Future<List<LocalMessage>> listMessages(Channel channel) async { | ||||
|   | ||||
| @@ -27,9 +27,9 @@ class _CallScreenState extends State<CallScreen> { | ||||
|         DateTime.now().difference(provider.current.value!.createdAt); | ||||
|  | ||||
|     String twoDigits(int n) => n.toString().padLeft(2, '0'); | ||||
|     String formattedTime = "${twoDigits(duration.inHours)}:" | ||||
|         "${twoDigits(duration.inMinutes.remainder(60))}:" | ||||
|         "${twoDigits(duration.inSeconds.remainder(60))}"; | ||||
|     String formattedTime = '${twoDigits(duration.inHours)}:' | ||||
|         '${twoDigits(duration.inMinutes.remainder(60))}:' | ||||
|         '${twoDigits(duration.inSeconds.remainder(60))}'; | ||||
|  | ||||
|     return formattedTime; | ||||
|   } | ||||
| @@ -66,7 +66,7 @@ class _CallScreenState extends State<CallScreen> { | ||||
|                 text: 'call'.tr, | ||||
|                 style: Theme.of(context).textTheme.titleLarge, | ||||
|               ), | ||||
|               const TextSpan(text: "\n"), | ||||
|               const TextSpan(text: '\n'), | ||||
|               TextSpan( | ||||
|                 text: currentDuration, | ||||
|                 style: Theme.of(context).textTheme.bodySmall, | ||||
|   | ||||
| @@ -2,8 +2,8 @@ import 'dart:async'; | ||||
| import 'dart:ui'; | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_animate/flutter_animate.dart'; | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:solian/controllers/chat_history_controller.dart'; | ||||
| import 'package:solian/exts.dart'; | ||||
| import 'package:solian/models/call.dart'; | ||||
| import 'package:solian/models/channel.dart'; | ||||
| @@ -13,8 +13,6 @@ import 'package:solian/providers/auth.dart'; | ||||
| import 'package:solian/providers/chat.dart'; | ||||
| import 'package:solian/providers/content/call.dart'; | ||||
| import 'package:solian/providers/content/channel.dart'; | ||||
| import 'package:solian/providers/message/helper.dart'; | ||||
| import 'package:solian/providers/message/history.dart'; | ||||
| import 'package:solian/router.dart'; | ||||
| import 'package:solian/screens/channel/channel_detail.dart'; | ||||
| import 'package:solian/theme.dart'; | ||||
| @@ -41,10 +39,7 @@ class ChannelChatScreen extends StatefulWidget { | ||||
| } | ||||
|  | ||||
| class _ChannelChatScreenState extends State<ChannelChatScreen> { | ||||
|   final _chatScrollController = ScrollController(); | ||||
|  | ||||
|   bool _isBusy = false; | ||||
|   bool _isLoadingMore = false; | ||||
|   int? _accountId; | ||||
|  | ||||
|   String? _overrideAlias; | ||||
| @@ -54,9 +49,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> { | ||||
|   ChannelMember? _channelProfile; | ||||
|   StreamSubscription<NetworkPackage>? _subscription; | ||||
|  | ||||
|   int _nextHistorySyncOffset = 0; | ||||
|   MessageHistoryDb? _db; | ||||
|   List<LocalMessage> _currentHistory = List.empty(); | ||||
|   late final ChatHistoryController _chatController; | ||||
|  | ||||
|   getProfile() async { | ||||
|     final AuthProvider auth = Get.find(); | ||||
| @@ -111,22 +104,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> { | ||||
|     setState(() => _isBusy = false); | ||||
|   } | ||||
|  | ||||
|   Future<void> getMessages() async { | ||||
|     await _db!.syncMessages( | ||||
|       _channel!, | ||||
|       scope: widget.realm, | ||||
|       offset: _nextHistorySyncOffset, | ||||
|     ); | ||||
|     await syncHistory(); | ||||
|   } | ||||
|  | ||||
|   Future<void> syncHistory() async { | ||||
|     final data = await _db!.localMessages.findAllByChannel(_channel!.id); | ||||
|     setState(() { | ||||
|       _currentHistory = data; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void listenMessages() { | ||||
|     final ChatProvider provider = Get.find(); | ||||
|     _subscription = provider.stream.stream.listen((event) { | ||||
| @@ -134,19 +111,19 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> { | ||||
|         case 'messages.new': | ||||
|           final payload = Message.fromJson(event.payload!); | ||||
|           if (payload.channelId == _channel?.id) { | ||||
|             _db?.receiveMessage(payload); | ||||
|             _chatController.receiveMessage(payload); | ||||
|           } | ||||
|           break; | ||||
|         case 'messages.update': | ||||
|           final payload = Message.fromJson(event.payload!); | ||||
|           if (payload.channelId == _channel?.id) { | ||||
|             _db?.replaceMessage(payload); | ||||
|             _chatController.replaceMessage(payload); | ||||
|           } | ||||
|           break; | ||||
|         case 'messages.burnt': | ||||
|           final payload = Message.fromJson(event.payload!); | ||||
|           if (payload.channelId == _channel?.id) { | ||||
|             _db?.burnMessage(payload.id); | ||||
|             _chatController.burnMessage(payload.id); | ||||
|           } | ||||
|           break; | ||||
|         case 'calls.new': | ||||
| @@ -157,7 +134,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> { | ||||
|           _ongoingCall = null; | ||||
|           break; | ||||
|       } | ||||
|       syncHistory(); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
| @@ -211,18 +187,18 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> { | ||||
|     bool isMerged = false, hasMerged = false; | ||||
|     if (index > 0) { | ||||
|       hasMerged = checkMessageMergeable( | ||||
|         _currentHistory[index - 1].data, | ||||
|         _currentHistory[index].data, | ||||
|         _chatController.currentHistory[index - 1].data, | ||||
|         _chatController.currentHistory[index].data, | ||||
|       ); | ||||
|     } | ||||
|     if (index + 1 < _currentHistory.length) { | ||||
|     if (index + 1 < _chatController.currentHistory.length) { | ||||
|       isMerged = checkMessageMergeable( | ||||
|         _currentHistory[index].data, | ||||
|         _currentHistory[index + 1].data, | ||||
|         _chatController.currentHistory[index].data, | ||||
|         _chatController.currentHistory[index + 1].data, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     final item = _currentHistory[index].data; | ||||
|     final item = _chatController.currentHistory[index].data; | ||||
|  | ||||
|     return InkWell( | ||||
|       child: Container( | ||||
| @@ -253,28 +229,17 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> { | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     _chatScrollController.addListener(() async { | ||||
|       if (_chatScrollController.position.pixels == | ||||
|           _chatScrollController.position.maxScrollExtent) { | ||||
|         setState(() => _isLoadingMore = true); | ||||
|         _nextHistorySyncOffset = _currentHistory.length; | ||||
|         await getMessages(); | ||||
|         setState(() => _isLoadingMore = false); | ||||
|       } | ||||
|     _chatController = ChatHistoryController(); | ||||
|     _chatController.initialize(); | ||||
|  | ||||
|     getChannel().then((_) { | ||||
|       _chatController.getMessages(_channel!, widget.realm); | ||||
|     }); | ||||
|  | ||||
|     createHistoryDb().then((db) async { | ||||
|       _db = db; | ||||
|     getProfile(); | ||||
|     getOngoingCall(); | ||||
|  | ||||
|       await getChannel(); | ||||
|       await syncHistory(); | ||||
|  | ||||
|       getProfile(); | ||||
|       getOngoingCall(); | ||||
|       getMessages(); | ||||
|  | ||||
|       listenMessages(); | ||||
|     }); | ||||
|     listenMessages(); | ||||
|  | ||||
|     super.initState(); | ||||
|   } | ||||
| @@ -352,52 +317,66 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> { | ||||
|         children: [ | ||||
|           Column( | ||||
|             children: [ | ||||
|               if (_isLoadingMore) | ||||
|                 const LinearProgressIndicator() | ||||
|                     .paddingOnly(bottom: 4) | ||||
|                     .animate() | ||||
|                     .slideY(), | ||||
|               Expanded( | ||||
|                 child: ListView.builder( | ||||
|                   controller: _chatScrollController, | ||||
|                   itemCount: _currentHistory.length, | ||||
|                   clipBehavior: Clip.none, | ||||
|                 child: CustomScrollView( | ||||
|                   reverse: true, | ||||
|                   itemBuilder: buildHistory, | ||||
|                 ).paddingOnly(bottom: 56), | ||||
|                   slivers: [ | ||||
|                     Obx(() { | ||||
|                       return SliverList.builder( | ||||
|                         itemCount: _chatController.currentHistory.length, | ||||
|                         itemBuilder: buildHistory, | ||||
|                       ); | ||||
|                     }), | ||||
|                     Obx(() { | ||||
|                       final amount = _chatController.totalHistoryCount - | ||||
|                           _chatController.currentHistory.length; | ||||
|                       if (amount > 0) { | ||||
|                         return SliverToBoxAdapter( | ||||
|                           child: ListTile( | ||||
|                             tileColor: Theme.of(context) | ||||
|                                 .colorScheme | ||||
|                                 .surfaceContainerLow, | ||||
|                             leading: const Icon(Icons.sync_disabled), | ||||
|                             title: Text('messageUnsync'.tr), | ||||
|                             subtitle: Text('messageUnsyncCaption'.trParams({ | ||||
|                               'count': amount.string, | ||||
|                             })), | ||||
|                             onTap: () {}, | ||||
|                           ), | ||||
|                         ); | ||||
|                       } else { | ||||
|                         return const SliverToBoxAdapter(child: SizedBox()); | ||||
|                       } | ||||
|                     }), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|           Positioned( | ||||
|             bottom: 0, | ||||
|             left: 0, | ||||
|             right: 0, | ||||
|             child: ClipRect( | ||||
|               child: BackdropFilter( | ||||
|                 filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50), | ||||
|                 child: SafeArea( | ||||
|                   child: ChatMessageInput( | ||||
|                     edit: _messageToEditing, | ||||
|                     reply: _messageToReplying, | ||||
|                     realm: widget.realm, | ||||
|                     placeholder: placeholder, | ||||
|                     channel: _channel!, | ||||
|                     onSent: (Message item) { | ||||
|                       setState(() { | ||||
|                         _db?.receiveMessage(item); | ||||
|                         syncHistory(); | ||||
|                       }); | ||||
|                     }, | ||||
|                     onReset: () { | ||||
|                       setState(() { | ||||
|                         _messageToReplying = null; | ||||
|                         _messageToEditing = null; | ||||
|                       }); | ||||
|                     }, | ||||
|               ClipRect( | ||||
|                 child: BackdropFilter( | ||||
|                   filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50), | ||||
|                   child: SafeArea( | ||||
|                     child: ChatMessageInput( | ||||
|                       edit: _messageToEditing, | ||||
|                       reply: _messageToReplying, | ||||
|                       realm: widget.realm, | ||||
|                       placeholder: placeholder, | ||||
|                       channel: _channel!, | ||||
|                       onSent: (Message item) { | ||||
|                         setState(() { | ||||
|                           _chatController.receiveMessage(item); | ||||
|                         }); | ||||
|                       }, | ||||
|                       onReset: () { | ||||
|                         setState(() { | ||||
|                           _messageToReplying = null; | ||||
|                           _messageToEditing = null; | ||||
|                         }); | ||||
|                       }, | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|             ], | ||||
|           ), | ||||
|           if (_ongoingCall != null) | ||||
|             Positioned( | ||||
|   | ||||
| @@ -164,6 +164,8 @@ class SolianMessages extends Translations { | ||||
|           'channelNotifyLevelMentioned': 'Only mentioned', | ||||
|           'channelNotifyLevelNone': 'Ignore all', | ||||
|           'channelNotifyLevelApplied': 'Your notification settings has been applied.', | ||||
|           'messageUnsync': 'Messages Un-synced', | ||||
|           'messageUnsyncCaption': '@count message(s) still in un-synced.', | ||||
|           'messageDecoding': 'Decoding...', | ||||
|           'messageDecodeFailed': 'Unable to decode: @message', | ||||
|           'messageInputPlaceholder': 'Message @channel', | ||||
| @@ -364,6 +366,8 @@ class SolianMessages extends Translations { | ||||
|           'channelNotifyLevelMentioned': '仅提及', | ||||
|           'channelNotifyLevelNone': '忽略一切', | ||||
|           'channelNotifyLevelApplied': '你的通知设置已经应用。', | ||||
|           'messageUnsync': '消息未同步', | ||||
|           'messageUnsyncCaption': '还有 @count 条消息未同步', | ||||
|           'messageDecoding': '解码信息中…', | ||||
|           'messageDecodeFailed': '解码信息失败:@message', | ||||
|           'messageInputPlaceholder': '在 @channel 发信息', | ||||
|   | ||||
| @@ -67,7 +67,7 @@ class SliverFriendList extends StatelessWidget { | ||||
|   Widget build(BuildContext context) { | ||||
|     return SliverList.builder( | ||||
|       itemCount: items.length, | ||||
|       itemBuilder: (_, __) => buildItem(_, __), | ||||
|       itemBuilder: (context, idx) => buildItem(context, idx), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -65,7 +65,7 @@ class _ChatCallButtonState extends State<ChatCallButton> { | ||||
|         ? widget.realm?.alias | ||||
|         : 'global'; | ||||
|     final resp = await client | ||||
|         .delete('/api/channels/${scope}/${widget.channel.alias}/calls/ongoing'); | ||||
|         .delete('/api/channels/$scope/${widget.channel.alias}/calls/ongoing'); | ||||
|     if (resp.statusCode == 200) { | ||||
|       if (widget.onEnded != null) widget.onEnded!(); | ||||
|     } else { | ||||
|   | ||||
| @@ -98,7 +98,7 @@ class _PostItemState extends State<PostItem> { | ||||
|  | ||||
|     if (labels.isNotEmpty) { | ||||
|       return Text( | ||||
|         labels.join(" · "), | ||||
|         labels.join(' · '), | ||||
|         textAlign: TextAlign.left, | ||||
|         style: TextStyle( | ||||
|           fontSize: 12, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user