From 34706531adb7b2b7aecc01419ae6d47b57e1404c Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 23 Jun 2024 12:29:07 +0800 Subject: [PATCH 1/6] :sparkles: Local message history db --- analysis_options.yaml | 4 +- lib/main.dart | 1 + lib/providers/message/helper.dart | 50 ++++ lib/providers/message/history.dart | 51 +++++ lib/providers/message/history.g.dart | 165 ++++++++++++++ pubspec.lock | 330 ++++++++++++++++++++++++++- pubspec.yaml | 68 +----- 7 files changed, 605 insertions(+), 64 deletions(-) create mode 100644 lib/providers/message/helper.dart create mode 100644 lib/providers/message/history.dart create mode 100644 lib/providers/message/history.g.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index 0d29021..f82bda6 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -21,8 +21,8 @@ linter: # `// ignore_for_file: name_of_lint` syntax on the line or in the file # producing the lint. rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + avoid_print: true # Uncomment to disable the `avoid_print` rule + prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options diff --git a/lib/main.dart b/lib/main.dart index b8ed3d4..e018e0d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -33,6 +33,7 @@ void main() async { await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); + if (PlatformInfo.isDesktop) { await Window.initialize(); await Window.setEffect( diff --git a/lib/providers/message/helper.dart b/lib/providers/message/helper.dart new file mode 100644 index 0000000..b911425 --- /dev/null +++ b/lib/providers/message/helper.dart @@ -0,0 +1,50 @@ +import 'package:get/get.dart'; +import 'package:solian/models/channel.dart'; +import 'package:solian/models/message.dart'; +import 'package:solian/models/pagination.dart'; +import 'package:solian/providers/auth.dart'; +import 'package:solian/providers/message/history.dart'; + +Future createHistoryDb() async { + return await $FloorMessageHistoryDb + .databaseBuilder('messaging_data.dart') + .build(); +} + +extension MessageHistoryHelper on MessageHistoryDb { + receiveMessage(Message remote) async { + await localMessages.insert(LocalMessage( + remote.id, + remote, + remote.channelId, + )); + } + + syncMessages(Channel channel, {String? scope}) async { + final AuthProvider auth = Get.find(); + if (!await auth.isAuthorized) return; + + final client = auth.configureClient('messaging'); + + final resp = await client + .get('/api/channels/$scope/${channel.alias}/messages?take=10&offset=0'); + + if (resp.statusCode != 200) { + throw Exception(resp.bodyString); + } + + // TODO Continue sync until the last message / the message exists / sync limitation + + final PaginationResult result = PaginationResult.fromJson(resp.body); + final parsed = result.data?.map((e) => Message.fromJson(e)).toList(); + if (parsed != null) { + await localMessages.insertBulk( + parsed.map((x) => LocalMessage(x.id, x, x.channelId)).toList(), + ); + } + } + + Future> listMessages(Channel channel) async { + return await localMessages.findAllByChannel(channel.id); + } +} diff --git a/lib/providers/message/history.dart b/lib/providers/message/history.dart new file mode 100644 index 0000000..9618706 --- /dev/null +++ b/lib/providers/message/history.dart @@ -0,0 +1,51 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:floor/floor.dart'; +import 'package:solian/models/message.dart'; +import 'package:sqflite/sqflite.dart' as sqflite; + +part 'history.g.dart'; + +@entity +class LocalMessage { + @primaryKey + final int id; + + final Message data; + final int channelId; + + LocalMessage(this.id, this.data, this.channelId); +} + +class RemoteMessageConverter extends TypeConverter { + @override + Message decode(String databaseValue) { + return Message.fromJson(jsonDecode(databaseValue)); + } + + @override + String encode(Message value) { + return jsonEncode(value.toJson()); + } +} + +@dao +abstract class LocalMessageDao { + @Query('SELECT * FROM LocalMessage WHERE channelId = :channelId') + Future> findAllByChannel(int channelId); + + @Insert(onConflict: OnConflictStrategy.replace) + Future insert(LocalMessage m); + + @Insert(onConflict: OnConflictStrategy.replace) + Future insertBulk(List m); + + @Query('DELETE * FROM LocalMessage') + Future wipeLocalMessages(); +} + +@TypeConverters([RemoteMessageConverter]) +@Database(version: 1, entities: [LocalMessage]) +abstract class MessageHistoryDb extends FloorDatabase { + LocalMessageDao get localMessages; +} diff --git a/lib/providers/message/history.g.dart b/lib/providers/message/history.g.dart new file mode 100644 index 0000000..9edbf25 --- /dev/null +++ b/lib/providers/message/history.g.dart @@ -0,0 +1,165 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'history.dart'; + +// ************************************************************************** +// FloorGenerator +// ************************************************************************** + +abstract class $MessageHistoryDbBuilderContract { + /// Adds migrations to the builder. + $MessageHistoryDbBuilderContract addMigrations(List migrations); + + /// Adds a database [Callback] to the builder. + $MessageHistoryDbBuilderContract addCallback(Callback callback); + + /// Creates the database and initializes it. + Future build(); +} + +// ignore: avoid_classes_with_only_static_members +class $FloorMessageHistoryDb { + /// Creates a database builder for a persistent database. + /// Once a database is built, you should keep a reference to it and re-use it. + static $MessageHistoryDbBuilderContract databaseBuilder(String name) => + _$MessageHistoryDbBuilder(name); + + /// Creates a database builder for an in memory database. + /// Information stored in an in memory database disappears when the process is killed. + /// Once a database is built, you should keep a reference to it and re-use it. + static $MessageHistoryDbBuilderContract inMemoryDatabaseBuilder() => + _$MessageHistoryDbBuilder(null); +} + +class _$MessageHistoryDbBuilder implements $MessageHistoryDbBuilderContract { + _$MessageHistoryDbBuilder(this.name); + + final String? name; + + final List _migrations = []; + + Callback? _callback; + + @override + $MessageHistoryDbBuilderContract addMigrations(List migrations) { + _migrations.addAll(migrations); + return this; + } + + @override + $MessageHistoryDbBuilderContract addCallback(Callback callback) { + _callback = callback; + return this; + } + + @override + Future build() async { + final path = name != null + ? await sqfliteDatabaseFactory.getDatabasePath(name!) + : ':memory:'; + final database = _$MessageHistoryDb(); + database.database = await database.open( + path, + _migrations, + _callback, + ); + return database; + } +} + +class _$MessageHistoryDb extends MessageHistoryDb { + _$MessageHistoryDb([StreamController? listener]) { + changeListener = listener ?? StreamController.broadcast(); + } + + LocalMessageDao? _localMessagesInstance; + + Future open( + String path, + List migrations, [ + Callback? callback, + ]) async { + final databaseOptions = sqflite.OpenDatabaseOptions( + version: 1, + onConfigure: (database) async { + await database.execute('PRAGMA foreign_keys = ON'); + await callback?.onConfigure?.call(database); + }, + onOpen: (database) async { + await callback?.onOpen?.call(database); + }, + onUpgrade: (database, startVersion, endVersion) async { + await MigrationAdapter.runMigrations( + database, startVersion, endVersion, migrations); + + await callback?.onUpgrade?.call(database, startVersion, endVersion); + }, + onCreate: (database, version) async { + await database.execute( + 'CREATE TABLE IF NOT EXISTS `LocalMessage` (`id` INTEGER NOT NULL, `data` TEXT NOT NULL, `channelId` INTEGER NOT NULL, PRIMARY KEY (`id`))'); + + await callback?.onCreate?.call(database, version); + }, + ); + return sqfliteDatabaseFactory.openDatabase(path, options: databaseOptions); + } + + @override + LocalMessageDao get localMessages { + return _localMessagesInstance ??= + _$LocalMessageDao(database, changeListener); + } +} + +class _$LocalMessageDao extends LocalMessageDao { + _$LocalMessageDao( + this.database, + this.changeListener, + ) : _queryAdapter = QueryAdapter(database), + _localMessageInsertionAdapter = InsertionAdapter( + database, + 'LocalMessage', + (LocalMessage item) => { + 'id': item.id, + 'data': _remoteMessageConverter.encode(item.data), + 'channelId': item.channelId + }); + + final sqflite.DatabaseExecutor database; + + final StreamController changeListener; + + final QueryAdapter _queryAdapter; + + final InsertionAdapter _localMessageInsertionAdapter; + + @override + Future> findAllByChannel(int channelId) async { + return _queryAdapter.queryList( + 'SELECT * FROM LocalMessage WHERE channelId = ?1', + mapper: (Map row) => LocalMessage( + row['id'] as int, + _remoteMessageConverter.decode(row['data'] as String), + row['channelId'] as int), + arguments: [channelId]); + } + + @override + Future wipeLocalMessages() async { + await _queryAdapter.queryNoReturn('DELETE * FROM LocalMessage'); + } + + @override + Future insert(LocalMessage m) async { + await _localMessageInsertionAdapter.insert(m, OnConflictStrategy.replace); + } + + @override + Future insertBulk(List m) async { + await _localMessageInsertionAdapter.insertList( + m, OnConflictStrategy.replace); + } +} + +// ignore_for_file: unused_element +final _remoteMessageConverter = RemoteMessageConverter(); diff --git a/pubspec.lock b/pubspec.lock index 4ce3074..b7cedf9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + url: "https://pub.dev" + source: hosted + version: "67.0.0" _flutterfire_internals: dependency: transitive description: @@ -9,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.37" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + url: "https://pub.dev" + source: hosted + version: "6.4.1" archive: dependency: transitive description: @@ -41,6 +57,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7" + url: "https://pub.dev" + source: hosted + version: "2.4.11" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe + url: "https://pub.dev" + source: hosted + version: "7.3.1" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + url: "https://pub.dev" + source: hosted + version: "8.9.2" cached_network_image: dependency: "direct main" description: @@ -81,6 +161,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" + source: hosted + version: "1.3.1" checked_yaml: dependency: transitive description: @@ -113,6 +201,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + url: "https://pub.dev" + source: hosted + version: "4.10.0" collection: dependency: transitive description: @@ -137,6 +233,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" cross_file: dependency: transitive description: @@ -169,6 +273,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + url: "https://pub.dev" + source: hosted + version: "2.3.6" dart_webrtc: dependency: transitive description: @@ -185,6 +297,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.10" + dev_build: + dependency: transitive + description: + name: dev_build + sha256: "4a4c8e3aaaaa9a7de4039dc711ee573de5612a35a4469d01ecf30e5ae74cbcfd" + url: "https://pub.dev" + source: hosted + version: "0.16.7+4" device_info_plus: dependency: "direct main" description: @@ -329,6 +449,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + floor: + dependency: "direct main" + description: + name: floor + sha256: c1b06023912b5b8e49deb6a9d867863c535ae1a232d991c3582bba3ee8687867 + url: "https://pub.dev" + source: hosted + version: "1.5.0" + floor_annotation: + dependency: transitive + description: + name: floor_annotation + sha256: a40949580a7ab0eee572686e2d3b1638fd6bd6a753e661d792ab4236b365b23b + url: "https://pub.dev" + source: hosted + version: "1.5.0" + floor_common: + dependency: transitive + description: + name: floor_common + sha256: "41c9914862f83a821815e1b1ffd47a1e1ae2130c35ff882ba2d000a67713ba64" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + floor_generator: + dependency: "direct dev" + description: + name: floor_generator + sha256: "1499b3ab878a807e6fbe6f140dc014124845cd1df3090a113aae5fa7577a1e77" + url: "https://pub.dev" + source: hosted + version: "1.5.0" flutter: dependency: "direct main" description: flutter @@ -504,6 +656,14 @@ packages: url: "https://pub.dev" source: hosted version: "10.7.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" get: dependency: "direct main" description: @@ -512,6 +672,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.6.6" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" go_router: dependency: "direct main" description: @@ -520,6 +688,14 @@ packages: url: "https://pub.dev" source: hosted version: "14.2.0" + graphs: + dependency: transitive + description: + name: graphs + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" + source: hosted + version: "2.3.1" html: dependency: transitive description: @@ -536,6 +712,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" http_parser: dependency: transitive description: @@ -632,6 +816,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" js: dependency: transitive description: @@ -680,6 +872,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + lists: + dependency: transitive + description: + name: lists + sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27" + url: "https://pub.dev" + source: hosted + version: "1.0.1" livekit_client: dependency: "direct main" description: @@ -776,6 +976,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" package_info_plus: dependency: "direct main" description: @@ -928,6 +1136,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + process_run: + dependency: transitive + description: + name: process_run + sha256: "8d9c6198b98fbbfb511edd42e7364e24d85c163e47398919871b952dc86a423e" + url: "https://pub.dev" + source: hosted + version: "0.14.2" protobuf: dependency: transitive description: @@ -952,6 +1176,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + url: "https://pub.dev" + source: hosted + version: "1.3.0" rxdart: dependency: transitive description: @@ -1040,6 +1272,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + url: "https://pub.dev" + source: hosted + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -1053,6 +1301,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.12" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" source_span: dependency: transitive description: @@ -1070,7 +1326,7 @@ packages: source: hosted version: "7.0.0" sqflite: - dependency: transitive + dependency: "direct main" description: name: sqflite sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d @@ -1085,6 +1341,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.5.4" + sqflite_common_ffi: + dependency: "direct dev" + description: + name: sqflite_common_ffi + sha256: "4d6137c29e930d6e4a8ff373989dd9de7bac12e3bc87bce950f6e844e8ad3bb5" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + sqflite_common_ffi_web: + dependency: "direct dev" + description: + name: sqflite_common_ffi_web + sha256: cfc9d1c61a3e06e5b2e96994a44b11125b4f451fee95b9fad8bd473b4613d592 + url: "https://pub.dev" + source: hosted + version: "0.4.3+1" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: b384f598b813b347c5a7e5ffad82cbaff1bec3d1561af267041e66f6f0899295 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + sqlparser: + dependency: transitive + description: + name: sqlparser + sha256: "7b20045d1ccfb7bc1df7e8f9fee5ae58673fce6ff62cefbb0e0fd7214e90e5a0" + url: "https://pub.dev" + source: hosted + version: "0.34.1" stack_trace: dependency: transitive description: @@ -1101,6 +1389,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" string_scanner: dependency: transitive description: @@ -1109,6 +1405,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + strings: + dependency: transitive + description: + name: strings + sha256: b33f40c4dd3e597bf6d9e7f4f4dc282dad0f19b07d9f320cb5c2183859cbccf5 + url: "https://pub.dev" + source: hosted + version: "3.1.1" synchronized: dependency: transitive description: @@ -1149,6 +1453,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.3" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" typed_data: dependency: transitive description: @@ -1157,6 +1469,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + unicode: + dependency: transitive + description: + name: unicode + sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1" + url: "https://pub.dev" + source: hosted + version: "0.3.1" url_launcher: dependency: "direct main" description: @@ -1309,6 +1629,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5e8ca1b..385e626 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,38 +1,16 @@ name: solian description: "The Solar Network App" -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: "none" # Remove this line if you wish to publish to pub.dev +publish_to: "none" -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. version: 1.1.0+1 environment: sdk: ">=3.3.4 <4.0.0" -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.6 go_router: ^14.1.1 get: ^4.6.6 @@ -70,59 +48,27 @@ dependencies: device_info_plus: ^10.1.0 shared_preferences: ^2.2.3 flutter_acrylic: ^1.1.4 + floor: ^1.5.0 + sqflite: ^2.3.3+1 dev_dependencies: flutter_test: sdk: flutter - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. flutter_lints: ^4.0.0 flutter_launcher_icons: ^0.13.1 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec + floor_generator: ^1.4.0 + build_runner: ^2.1.2 + sqflite_common_ffi: ^2.3.3 + sqflite_common_ffi_web: ^0.4.3+1 -# The following section is specific to Flutter packages. flutter: - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - # To add assets to your application, add an assets section, like this: assets: - assets/logo.png - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages - flutter_launcher_icons: android: "launcher_icon" ios: true From 52e5dd68606464fefbb4e4031e7910f29cd84ef7 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 23 Jun 2024 13:27:21 +0800 Subject: [PATCH 2/6] :recycle: Moved the chat page to use local db --- lib/models/message.dart | 3 +- lib/providers/message/helper.dart | 69 +++++++++++++++--- lib/providers/message/history.dart | 19 ++++- lib/providers/message/history.g.dart | 53 +++++++++++++- lib/screens/channel/channel_chat.dart | 101 ++++++++++---------------- 5 files changed, 167 insertions(+), 78 deletions(-) diff --git a/lib/models/message.dart b/lib/models/message.dart index d9ca54a..9164fe0 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -47,7 +47,8 @@ class Message { attachments: json["attachments"] != null ? List.from(json["attachments"]) : null, - channel: Channel.fromJson(json['channel']), + channel: + json['channel'] != null ? Channel.fromJson(json['channel']) : null, sender: Sender.fromJson(json['sender']), replyId: json['reply_id'], replyTo: json['reply_to'] != null diff --git a/lib/providers/message/helper.dart b/lib/providers/message/helper.dart index b911425..3b3e4dd 100644 --- a/lib/providers/message/helper.dart +++ b/lib/providers/message/helper.dart @@ -20,28 +20,75 @@ extension MessageHistoryHelper on MessageHistoryDb { )); } - syncMessages(Channel channel, {String? scope}) async { + replaceMessage(Message remote) async { + await localMessages.update(LocalMessage( + remote.id, + remote, + remote.channelId, + )); + } + + burnMessage(int id) async { + await localMessages.delete(id); + } + + syncMessages(Channel channel, {String scope = 'global'}) async { + final lastOne = await localMessages.findLastByChannel(channel.id); + + final data = await _getRemoteMessages( + channel, + scope, + remainBreath: 5, + onBrake: (items) { + return items.any((x) => x.id == lastOne?.id); + }, + ); + await localMessages.insertBulk( + data.map((x) => LocalMessage(x.id, x, x.channelId)).toList(), + ); + } + + Future> _getRemoteMessages( + Channel channel, + String scope, { + required int remainBreath, + bool Function(List items)? onBrake, + take = 10, + offset = 0, + }) async { + if (remainBreath <= 0) { + return List.empty(); + } + final AuthProvider auth = Get.find(); - if (!await auth.isAuthorized) return; + if (!await auth.isAuthorized) return List.empty(); final client = auth.configureClient('messaging'); - final resp = await client - .get('/api/channels/$scope/${channel.alias}/messages?take=10&offset=0'); + final resp = await client.get( + '/api/channels/$scope/${channel.alias}/messages?take=$take&offset=$offset'); if (resp.statusCode != 200) { throw Exception(resp.bodyString); } - // TODO Continue sync until the last message / the message exists / sync limitation + final PaginationResult response = PaginationResult.fromJson(resp.body); + final result = + response.data?.map((e) => Message.fromJson(e)).toList() ?? List.empty(); - final PaginationResult result = PaginationResult.fromJson(resp.body); - final parsed = result.data?.map((e) => Message.fromJson(e)).toList(); - if (parsed != null) { - await localMessages.insertBulk( - parsed.map((x) => LocalMessage(x.id, x, x.channelId)).toList(), - ); + if (onBrake != null && onBrake(result)) { + return result; } + + final expandResult = await _getRemoteMessages( + channel, + scope, + remainBreath: remainBreath - 1, + take: take, + offset: offset + result.length, + ); + + return [...result, ...expandResult]; } Future> listMessages(Channel channel) async { diff --git a/lib/providers/message/history.dart b/lib/providers/message/history.dart index 9618706..c28ebaa 100644 --- a/lib/providers/message/history.dart +++ b/lib/providers/message/history.dart @@ -31,16 +31,31 @@ class RemoteMessageConverter extends TypeConverter { @dao abstract class LocalMessageDao { - @Query('SELECT * FROM LocalMessage WHERE channelId = :channelId') + @Query('SELECT COUNT(id) FROM LocalMessage WHERE channelId = :channelId') + Future countByChannel(int channelId); + + @Query('SELECT * FROM LocalMessage WHERE channelId = :channelId ORDER BY id DESC') Future> findAllByChannel(int channelId); + @Query('SELECT * FROM LocalMessage WHERE channelId = :channelId ORDER BY id DESC LIMIT 1') + Future findLastByChannel(int channelId); + @Insert(onConflict: OnConflictStrategy.replace) Future insert(LocalMessage m); @Insert(onConflict: OnConflictStrategy.replace) Future insertBulk(List m); - @Query('DELETE * FROM LocalMessage') + @Update(onConflict: OnConflictStrategy.replace) + Future update(LocalMessage person); + + @Query('DELETE FROM LocalMessage WHERE id = :id') + Future delete(int id); + + @Query('DELETE FROM LocalMessage WHERE channelId = :channelId') + Future> deleteByChannel(int channelId); + + @Query('DELETE FROM LocalMessage') Future wipeLocalMessages(); } diff --git a/lib/providers/message/history.g.dart b/lib/providers/message/history.g.dart index 9edbf25..d5e04d4 100644 --- a/lib/providers/message/history.g.dart +++ b/lib/providers/message/history.g.dart @@ -119,6 +119,15 @@ class _$LocalMessageDao extends LocalMessageDao { _localMessageInsertionAdapter = InsertionAdapter( database, 'LocalMessage', + (LocalMessage item) => { + 'id': item.id, + 'data': _remoteMessageConverter.encode(item.data), + 'channelId': item.channelId + }), + _localMessageUpdateAdapter = UpdateAdapter( + database, + 'LocalMessage', + ['id'], (LocalMessage item) => { 'id': item.id, 'data': _remoteMessageConverter.encode(item.data), @@ -133,10 +142,45 @@ class _$LocalMessageDao extends LocalMessageDao { final InsertionAdapter _localMessageInsertionAdapter; + final UpdateAdapter _localMessageUpdateAdapter; + + @override + Future countByChannel(int channelId) async { + return _queryAdapter.query( + 'SELECT COUNT(id) FROM LocalMessage WHERE channelId = ?1', + mapper: (Map row) => row.values.first as int, + arguments: [channelId]); + } + @override Future> findAllByChannel(int channelId) async { return _queryAdapter.queryList( - 'SELECT * FROM LocalMessage WHERE channelId = ?1', + 'SELECT * FROM LocalMessage WHERE channelId = ?1 ORDER BY id DESC', + mapper: (Map row) => LocalMessage( + row['id'] as int, + _remoteMessageConverter.decode(row['data'] as String), + row['channelId'] as int), + arguments: [channelId]); + } + + @override + Future findLastByChannel(int channelId) async { + return _queryAdapter.query( + 'SELECT * FROM LocalMessage WHERE channelId = ?1 ORDER BY id DESC LIMIT 1', + mapper: (Map row) => LocalMessage(row['id'] as int, _remoteMessageConverter.decode(row['data'] as String), row['channelId'] as int), + arguments: [channelId]); + } + + @override + Future delete(int id) async { + await _queryAdapter.queryNoReturn('DELETE FROM LocalMessage WHERE id = ?1', + arguments: [id]); + } + + @override + Future> deleteByChannel(int channelId) async { + return _queryAdapter.queryList( + 'DELETE FROM LocalMessage WHERE channelId = ?1', mapper: (Map row) => LocalMessage( row['id'] as int, _remoteMessageConverter.decode(row['data'] as String), @@ -146,7 +190,7 @@ class _$LocalMessageDao extends LocalMessageDao { @override Future wipeLocalMessages() async { - await _queryAdapter.queryNoReturn('DELETE * FROM LocalMessage'); + await _queryAdapter.queryNoReturn('DELETE FROM LocalMessage'); } @override @@ -159,6 +203,11 @@ class _$LocalMessageDao extends LocalMessageDao { await _localMessageInsertionAdapter.insertList( m, OnConflictStrategy.replace); } + + @override + Future update(LocalMessage person) async { + await _localMessageUpdateAdapter.update(person, OnConflictStrategy.replace); + } } // ignore_for_file: unused_element diff --git a/lib/screens/channel/channel_chat.dart b/lib/screens/channel/channel_chat.dart index b40c26f..7256cba 100644 --- a/lib/screens/channel/channel_chat.dart +++ b/lib/screens/channel/channel_chat.dart @@ -3,17 +3,17 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:solian/exts.dart'; import 'package:solian/models/call.dart'; import 'package:solian/models/channel.dart'; import 'package:solian/models/message.dart'; import 'package:solian/models/packet.dart'; -import 'package:solian/models/pagination.dart'; 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'; @@ -50,8 +50,8 @@ class _ChannelChatScreenState extends State { Call? _ongoingCall; StreamSubscription? _subscription; - final PagingController _pagingController = - PagingController(firstPageKey: 0); + MessageHistoryDb? _db; + List _currentHistory = List.empty(); getProfile() async { final AuthProvider auth = Get.find(); @@ -106,29 +106,14 @@ class _ChannelChatScreenState extends State { setState(() => _isBusy = false); } - Future getMessages(int pageKey) async { - final AuthProvider auth = Get.find(); - if (!await auth.isAuthorized) return; + Future getMessages() async { + await _db!.syncMessages(_channel!, scope: widget.realm); + await syncHistory(); + } - final client = auth.configureClient('messaging'); - - final resp = await client.get( - '/api/channels/${widget.realm}/${widget.alias}/messages?take=10&offset=$pageKey'); - - if (resp.statusCode == 200) { - final PaginationResult result = PaginationResult.fromJson(resp.body); - final parsed = result.data?.map((e) => Message.fromJson(e)).toList(); - - if (parsed != null && parsed.length >= 10) { - _pagingController.appendPage(parsed, pageKey + parsed.length); - } else if (parsed != null) { - _pagingController.appendLastPage(parsed); - } - } else if (resp.statusCode == 403) { - _pagingController.appendLastPage([]); - } else { - _pagingController.error = resp.bodyString; - } + Future syncHistory() async { + _currentHistory = await _db!.localMessages.findAllByChannel(_channel!.id); + setState(() {}); } void listenMessages() { @@ -138,33 +123,19 @@ class _ChannelChatScreenState extends State { case 'messages.new': final payload = Message.fromJson(event.payload!); if (payload.channelId == _channel?.id) { - final idx = _pagingController.itemList - ?.indexWhere((e) => e.uuid == payload.uuid); - if ((idx ?? -1) >= 0) { - _pagingController.itemList?[idx!] = payload; - } else { - _pagingController.itemList?.insert(0, payload); - } + _db?.receiveMessage(payload); } break; case 'messages.update': final payload = Message.fromJson(event.payload!); if (payload.channelId == _channel?.id) { - final idx = _pagingController.itemList - ?.indexWhere((x) => x.uuid == payload.uuid); - if (idx != null) { - _pagingController.itemList?[idx] = payload; - } + _db?.replaceMessage(payload); } break; case 'messages.burnt': final payload = Message.fromJson(event.payload!); if (payload.channelId == _channel?.id) { - final idx = _pagingController.itemList - ?.indexWhere((x) => x.uuid != payload.uuid); - if (idx != null) { - _pagingController.itemList?.removeAt(idx - 1); - } + _db?.burnMessage(payload.id); } break; case 'calls.new': @@ -175,7 +146,7 @@ class _ChannelChatScreenState extends State { _ongoingCall = null; break; } - setState(() {}); + syncHistory(); }); } @@ -200,21 +171,23 @@ class _ChannelChatScreenState extends State { Message? _messageToReplying; Message? _messageToEditing; - Widget buildHistory(context, Message item, index) { + Widget buildHistory(context, index) { bool isMerged = false, hasMerged = false; if (index > 0) { hasMerged = checkMessageMergeable( - _pagingController.itemList?[index - 1], - item, + _currentHistory[index - 1].data, + _currentHistory[index].data, ); } - if (index + 1 < (_pagingController.itemList?.length ?? 0)) { + if (index + 1 < _currentHistory.length) { isMerged = checkMessageMergeable( - item, - _pagingController.itemList?[index + 1], + _currentHistory[index].data, + _currentHistory[index + 1].data, ); } + final item = _currentHistory[index].data; + Widget content; if (item.replyTo != null) { content = Column( @@ -268,14 +241,20 @@ class _ChannelChatScreenState extends State { @override void initState() { - super.initState(); + createHistoryDb().then((db) async { + _db = db; + + await getChannel(); + await syncHistory(); + + getProfile(); + getOngoingCall(); + getMessages(); - getProfile(); - getChannel().then((_) { listenMessages(); - _pagingController.addPageRequestListener(getMessages); }); - getOngoingCall(); + + super.initState(); } @override @@ -352,14 +331,11 @@ class _ChannelChatScreenState extends State { Column( children: [ Expanded( - child: PagedListView( + child: ListView.builder( + itemCount: _currentHistory.length, clipBehavior: Clip.none, reverse: true, - pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: buildHistory, - noItemsFoundIndicatorBuilder: (_) => Container(), - ), + itemBuilder: buildHistory, ).paddingOnly(bottom: 56), ), ], @@ -380,7 +356,8 @@ class _ChannelChatScreenState extends State { channel: _channel!, onSent: (Message item) { setState(() { - _pagingController.itemList?.insert(0, item); + _db?.receiveMessage(item); + syncHistory(); }); }, onReset: () { From 2038d33a3107f13b5ea06032d97ab07f2e1f5468 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 23 Jun 2024 18:03:46 +0800 Subject: [PATCH 3/6] :sparkles: Load more messages --- lib/providers/message/helper.dart | 5 +- lib/screens/channel/channel_chat.dart | 86 ++++++++++++++++++--------- lib/screens/chat.dart | 2 +- 3 files changed, 61 insertions(+), 32 deletions(-) diff --git a/lib/providers/message/helper.dart b/lib/providers/message/helper.dart index 3b3e4dd..d35c904 100644 --- a/lib/providers/message/helper.dart +++ b/lib/providers/message/helper.dart @@ -32,13 +32,14 @@ extension MessageHistoryHelper on MessageHistoryDb { await localMessages.delete(id); } - syncMessages(Channel channel, {String scope = 'global'}) async { + syncMessages(Channel channel, {String scope = 'global', offset = 0}) async { final lastOne = await localMessages.findLastByChannel(channel.id); final data = await _getRemoteMessages( channel, scope, - remainBreath: 5, + remainBreath: 3, + offset: offset, onBrake: (items) { return items.any((x) => x.id == lastOne?.id); }, diff --git a/lib/screens/channel/channel_chat.dart b/lib/screens/channel/channel_chat.dart index 7256cba..c254cc2 100644 --- a/lib/screens/channel/channel_chat.dart +++ b/lib/screens/channel/channel_chat.dart @@ -2,6 +2,7 @@ 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/exts.dart'; import 'package:solian/models/call.dart'; @@ -40,16 +41,20 @@ class ChannelChatScreen extends StatefulWidget { } class _ChannelChatScreenState extends State { + final _chatScrollController = ScrollController(); + bool _isBusy = false; + bool _isLoadingMore = false; int? _accountId; String? _overrideAlias; Channel? _channel; - ChannelMember? _channelProfile; Call? _ongoingCall; + ChannelMember? _channelProfile; StreamSubscription? _subscription; + int _nextHistorySyncOffset = 0; MessageHistoryDb? _db; List _currentHistory = List.empty(); @@ -107,13 +112,19 @@ class _ChannelChatScreenState extends State { } Future getMessages() async { - await _db!.syncMessages(_channel!, scope: widget.realm); + await _db!.syncMessages( + _channel!, + scope: widget.realm, + offset: _nextHistorySyncOffset, + ); await syncHistory(); } Future syncHistory() async { - _currentHistory = await _db!.localMessages.findAllByChannel(_channel!.id); - setState(() {}); + final data = await _db!.localMessages.findAllByChannel(_channel!.id); + setState(() { + _currentHistory = data; + }); } void listenMessages() { @@ -171,6 +182,31 @@ class _ChannelChatScreenState extends State { Message? _messageToReplying; Message? _messageToEditing; + Widget buildHistoryBody(Message item, {bool isMerged = false}) { + if (item.replyTo != null) { + return Column( + children: [ + ChatMessage( + key: Key('m${item.replyTo!.uuid}'), + item: item.replyTo!, + isReply: true, + ).paddingOnly(left: 24, right: 4, bottom: 2), + ChatMessage( + key: Key('m${item.uuid}'), + item: item, + isMerged: isMerged, + ), + ], + ); + } + + return ChatMessage( + key: Key('m${item.uuid}'), + item: item, + isMerged: isMerged, + ); + } + Widget buildHistory(context, index) { bool isMerged = false, hasMerged = false; if (index > 0) { @@ -188,33 +224,9 @@ class _ChannelChatScreenState extends State { final item = _currentHistory[index].data; - Widget content; - if (item.replyTo != null) { - content = Column( - children: [ - ChatMessage( - key: Key('m${item.replyTo!.uuid}'), - item: item.replyTo!, - isReply: true, - ).paddingOnly(left: 24, right: 4, bottom: 2), - ChatMessage( - key: Key('m${item.uuid}'), - item: item, - isMerged: isMerged, - ), - ], - ); - } else { - content = ChatMessage( - key: Key('m${item.uuid}'), - item: item, - isMerged: isMerged, - ); - } - return InkWell( child: Container( - child: content.paddingOnly( + child: buildHistoryBody(item, isMerged: isMerged).paddingOnly( top: !isMerged ? 8 : 0, bottom: !hasMerged ? 8 : 0, ), @@ -241,6 +253,16 @@ class _ChannelChatScreenState extends State { @override void initState() { + _chatScrollController.addListener(() async { + if (_chatScrollController.position.pixels == + _chatScrollController.position.maxScrollExtent) { + setState(() => _isLoadingMore = true); + _nextHistorySyncOffset = _currentHistory.length; + await getMessages(); + setState(() => _isLoadingMore = false); + } + }); + createHistoryDb().then((db) async { _db = db; @@ -330,8 +352,14 @@ class _ChannelChatScreenState extends State { children: [ Column( children: [ + if (_isLoadingMore) + const LinearProgressIndicator() + .paddingOnly(bottom: 4) + .animate() + .slideY(), Expanded( child: ListView.builder( + controller: _chatScrollController, itemCount: _currentHistory.length, clipBehavior: Clip.none, reverse: true, diff --git a/lib/screens/chat.dart b/lib/screens/chat.dart index 5cabea1..526dadf 100644 --- a/lib/screens/chat.dart +++ b/lib/screens/chat.dart @@ -101,7 +101,7 @@ class _ChatScreenState extends State { context), sliver: SliverAppBar( title: AppBarTitle('chat'.tr), - centerTitle: true, + centerTitle: false, floating: true, titleSpacing: SolianTheme.titleSpacing(context), toolbarHeight: SolianTheme.toolbarHeight(context), From aa8eec1a5a5ce599cdf797152a06dd95666b287e Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 23 Jun 2024 18:51:49 +0800 Subject: [PATCH 4/6] :recycle: Use controller instead of state to manage history --- lib/controllers/chat_history_controller.dart | 48 ++++++ lib/models/account.dart | 30 ++-- lib/models/attachment.dart | 64 +++---- lib/models/message.dart | 4 +- lib/models/post.dart | 96 +++++------ lib/providers/account.dart | 4 +- lib/providers/auth.dart | 2 +- lib/providers/message/helper.dart | 50 +++--- lib/screens/channel/call/call.dart | 8 +- lib/screens/channel/channel_chat.dart | 167 ++++++++----------- lib/translations.dart | 4 + lib/widgets/account/friend_list.dart | 2 +- lib/widgets/chat/call/chat_call_action.dart | 2 +- lib/widgets/posts/post_item.dart | 2 +- 14 files changed, 262 insertions(+), 221 deletions(-) create mode 100644 lib/controllers/chat_history_controller.dart diff --git a/lib/controllers/chat_history_controller.dart b/lib/controllers/chat_history_controller.dart new file mode 100644 index 0000000..b7f69c8 --- /dev/null +++ b/lib/controllers/chat_history_controller.dart @@ -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 currentHistory = RxList.empty(growable: true); + final RxInt totalHistoryCount = 0.obs; + + initialize() async { + database = await createHistoryDb(); + currentHistory.clear(); + } + + Future getMessages(Channel channel, String scope) async { + totalHistoryCount.value = await database.syncMessages(channel, scope: scope); + await syncHistory(channel); + } + + Future 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); + } +} diff --git a/lib/models/account.dart b/lib/models/account.dart index 3363a33..36f607b 100644 --- a/lib/models/account.dart +++ b/lib/models/account.dart @@ -81,24 +81,24 @@ class AccountBadge { }); factory AccountBadge.fromJson(Map 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 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, }; } diff --git a/lib/models/attachment.dart b/lib/models/attachment.dart index c4c4a90..be9807d 100644 --- a/lib/models/attachment.dart +++ b/lib/models/attachment.dart @@ -38,40 +38,40 @@ class Attachment { }); factory Attachment.fromJson(Map 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 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, }; } \ No newline at end of file diff --git a/lib/models/message.dart b/lib/models/message.dart index 9164fe0..18b736c 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -44,8 +44,8 @@ class Message { deletedAt: json['deleted_at'], content: json['content'], type: json['type'], - attachments: json["attachments"] != null - ? List.from(json["attachments"]) + attachments: json['attachments'] != null + ? List.from(json['attachments']) : null, channel: json['channel'] != null ? Channel.fromJson(json['channel']) : null, diff --git a/lib/models/post.dart b/lib/models/post.dart index 69e31df..e70cb59 100755 --- a/lib/models/post.dart +++ b/lib/models/post.dart @@ -53,36 +53,36 @@ class Post { }); factory Post.fromJson(Map 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.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.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 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, }; } diff --git a/lib/providers/account.dart b/lib/providers/account.dart index 322eb8d..65c438f 100644 --- a/lib/providers/account.dart +++ b/lib/providers/account.dart @@ -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(); } diff --git a/lib/providers/auth.dart b/lib/providers/auth.dart index deec081..d58ba99 100644 --- a/lib/providers/auth.dart +++ b/lib/providers/auth.dart @@ -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()}'); } } diff --git a/lib/providers/message/helper.dart b/lib/providers/message/helper.dart index d35c904..f397e13 100644 --- a/lib/providers/message/helper.dart +++ b/lib/providers/message/helper.dart @@ -13,19 +13,23 @@ Future 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> _getRemoteMessages( + Future<(List, 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> listMessages(Channel channel) async { diff --git a/lib/screens/channel/call/call.dart b/lib/screens/channel/call/call.dart index 4aed7be..d9eb481 100644 --- a/lib/screens/channel/call/call.dart +++ b/lib/screens/channel/call/call.dart @@ -27,9 +27,9 @@ class _CallScreenState extends State { 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 { 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, diff --git a/lib/screens/channel/channel_chat.dart b/lib/screens/channel/channel_chat.dart index c254cc2..840e0af 100644 --- a/lib/screens/channel/channel_chat.dart +++ b/lib/screens/channel/channel_chat.dart @@ -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 { - final _chatScrollController = ScrollController(); - bool _isBusy = false; - bool _isLoadingMore = false; int? _accountId; String? _overrideAlias; @@ -54,9 +49,7 @@ class _ChannelChatScreenState extends State { ChannelMember? _channelProfile; StreamSubscription? _subscription; - int _nextHistorySyncOffset = 0; - MessageHistoryDb? _db; - List _currentHistory = List.empty(); + late final ChatHistoryController _chatController; getProfile() async { final AuthProvider auth = Get.find(); @@ -111,22 +104,6 @@ class _ChannelChatScreenState extends State { setState(() => _isBusy = false); } - Future getMessages() async { - await _db!.syncMessages( - _channel!, - scope: widget.realm, - offset: _nextHistorySyncOffset, - ); - await syncHistory(); - } - - Future 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 { 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 { _ongoingCall = null; break; } - syncHistory(); }); } @@ -211,18 +187,18 @@ class _ChannelChatScreenState extends State { 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 { @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 { 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( diff --git a/lib/translations.dart b/lib/translations.dart index fb78b7d..e8ec8c4 100644 --- a/lib/translations.dart +++ b/lib/translations.dart @@ -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 发信息', diff --git a/lib/widgets/account/friend_list.dart b/lib/widgets/account/friend_list.dart index af2f6e1..7d8fce7 100644 --- a/lib/widgets/account/friend_list.dart +++ b/lib/widgets/account/friend_list.dart @@ -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), ); } } diff --git a/lib/widgets/chat/call/chat_call_action.dart b/lib/widgets/chat/call/chat_call_action.dart index dc6acfb..704651b 100644 --- a/lib/widgets/chat/call/chat_call_action.dart +++ b/lib/widgets/chat/call/chat_call_action.dart @@ -65,7 +65,7 @@ class _ChatCallButtonState extends State { ? 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 { diff --git a/lib/widgets/posts/post_item.dart b/lib/widgets/posts/post_item.dart index bf9b8e6..7918e4d 100644 --- a/lib/widgets/posts/post_item.dart +++ b/lib/widgets/posts/post_item.dart @@ -98,7 +98,7 @@ class _PostItemState extends State { if (labels.isNotEmpty) { return Text( - labels.join(" · "), + labels.join(' · '), textAlign: TextAlign.left, style: TextStyle( fontSize: 12, From d0cd75d65331201b48476986eac78f0465bb452e Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 23 Jun 2024 19:02:41 +0800 Subject: [PATCH 5/6] :sparkles: Now can load more messages via click the tile --- lib/controllers/chat_history_controller.dart | 19 ++++++-- lib/providers/message/helper.dart | 4 +- lib/screens/channel/channel_chat.dart | 49 ++++++++++++++------ 3 files changed, 52 insertions(+), 20 deletions(-) diff --git a/lib/controllers/chat_history_controller.dart b/lib/controllers/chat_history_controller.dart index b7f69c8..2a97bd6 100644 --- a/lib/controllers/chat_history_controller.dart +++ b/lib/controllers/chat_history_controller.dart @@ -10,16 +10,31 @@ class ChatHistoryController { final RxList currentHistory = RxList.empty(growable: true); final RxInt totalHistoryCount = 0.obs; + final RxBool isLoading = false.obs; + initialize() async { database = await createHistoryDb(); currentHistory.clear(); } Future getMessages(Channel channel, String scope) async { - totalHistoryCount.value = await database.syncMessages(channel, scope: scope); + totalHistoryCount.value = + await database.syncMessages(channel, scope: scope); await syncHistory(channel); } + Future getMoreMessages(Channel channel, String scope) async { + isLoading.value = true; + totalHistoryCount.value = await database.syncMessages( + channel, + breath: 3, + scope: scope, + offset: currentHistory.length, + ); + await syncHistory(channel); + isLoading.value = false; + } + Future syncHistory(Channel channel) async { currentHistory.replaceRange(0, currentHistory.length, await database.localMessages.findAllByChannel(channel.id)); @@ -27,7 +42,6 @@ class ChatHistoryController { receiveMessage(Message remote) async { final entry = await database.receiveMessage(remote); - totalHistoryCount.value++; currentHistory.add(entry); } @@ -42,7 +56,6 @@ class ChatHistoryController { void burnMessage(int id) async { await database.burnMessage(id); - totalHistoryCount.value--; currentHistory.removeWhere((x) => x.id == id); } } diff --git a/lib/providers/message/helper.dart b/lib/providers/message/helper.dart index f397e13..4404f79 100644 --- a/lib/providers/message/helper.dart +++ b/lib/providers/message/helper.dart @@ -36,13 +36,13 @@ extension MessageHistoryHelper on MessageHistoryDb { await localMessages.delete(id); } - syncMessages(Channel channel, {String scope = 'global', offset = 0}) async { + syncMessages(Channel channel, {String scope = 'global', breath = 10, offset = 0}) async { final lastOne = await localMessages.findLastByChannel(channel.id); final data = await _getRemoteMessages( channel, scope, - remainBreath: 10, + remainBreath: breath, offset: offset, onBrake: (items) { return items.any((x) => x.id == lastOne?.id); diff --git a/lib/screens/channel/channel_chat.dart b/lib/screens/channel/channel_chat.dart index 840e0af..8ad1d4c 100644 --- a/lib/screens/channel/channel_chat.dart +++ b/lib/screens/channel/channel_chat.dart @@ -2,6 +2,7 @@ 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'; @@ -330,23 +331,41 @@ class _ChannelChatScreenState extends State { 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 { + + if (amount.value <= 0 || + _chatController.isLoading.isTrue) { return const SliverToBoxAdapter(child: SizedBox()); } + + 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: () { + _chatController.getMoreMessages( + _channel!, + widget.realm, + ); + }, + ), + ); + }), + Obx(() { + if (_chatController.isLoading.isFalse) { + return const SliverToBoxAdapter(child: SizedBox()); + } + + return SliverToBoxAdapter( + child: const LinearProgressIndicator() + .animate() + .slideY() + .paddingOnly(bottom: 4), + ); }), ], ), From 803693008454dadfd065e0c878d8131811926ee5 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 23 Jun 2024 19:13:07 +0800 Subject: [PATCH 6/6] :bug: Bug fixes on duplicate message --- lib/controllers/chat_history_controller.dart | 20 +++++++++++++++++++- lib/providers/auth.dart | 6 ++++++ lib/screens/channel/channel_chat.dart | 3 ++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/lib/controllers/chat_history_controller.dart b/lib/controllers/chat_history_controller.dart index 2a97bd6..0d93047 100644 --- a/lib/controllers/chat_history_controller.dart +++ b/lib/controllers/chat_history_controller.dart @@ -18,9 +18,11 @@ class ChatHistoryController { } Future getMessages(Channel channel, String scope) async { + isLoading.value = true; totalHistoryCount.value = await database.syncMessages(channel, scope: scope); await syncHistory(channel); + isLoading.value = false; } Future getMoreMessages(Channel channel, String scope) async { @@ -42,7 +44,23 @@ class ChatHistoryController { receiveMessage(Message remote) async { final entry = await database.receiveMessage(remote); - currentHistory.add(entry); + final idx = currentHistory.indexWhere((x) => x.data.uuid == remote.uuid); + if (idx != -1) { + currentHistory[idx] = entry; + } else { + currentHistory.insert(0, entry); + } + } + + addTemporaryMessage(Message info) async { + currentHistory.insert( + 0, + LocalMessage( + info.id, + info, + info.channelId, + ), + ); } void replaceMessage(Message remote) async { diff --git a/lib/providers/auth.dart b/lib/providers/auth.dart index d58ba99..e7989fc 100644 --- a/lib/providers/auth.dart +++ b/lib/providers/auth.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:get/get.dart'; import 'package:get/get_connect/http/src/request/request.dart'; +import 'package:solian/controllers/chat_history_controller.dart'; import 'package:solian/providers/account.dart'; import 'package:solian/providers/chat.dart'; import 'package:solian/services.dart'; @@ -135,6 +136,11 @@ class AuthProvider extends GetConnect { Get.find().notifications.clear(); Get.find().notificationUnread.value = 0; + final chatHistory = ChatHistoryController(); + chatHistory.initialize().then((_) async { + await chatHistory.database.localMessages.wipeLocalMessages(); + }); + storage.deleteAll(); } diff --git a/lib/screens/channel/channel_chat.dart b/lib/screens/channel/channel_chat.dart index 8ad1d4c..3e795ea 100644 --- a/lib/screens/channel/channel_chat.dart +++ b/lib/screens/channel/channel_chat.dart @@ -324,6 +324,7 @@ class _ChannelChatScreenState extends State { slivers: [ Obx(() { return SliverList.builder( + key: Key('chat-history#${_channel!.id}'), itemCount: _chatController.currentHistory.length, itemBuilder: buildHistory, ); @@ -382,7 +383,7 @@ class _ChannelChatScreenState extends State { channel: _channel!, onSent: (Message item) { setState(() { - _chatController.receiveMessage(item); + _chatController.addTemporaryMessage(item); }); }, onReset: () {