diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 997acf3..e2a2e3d 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -51,6 +51,14 @@
to determine the Window background behind the Flutter UI. -->
+
+
+
+
+
+
+
+
@@ -61,14 +69,6 @@
-
-
-
-
-
-
-
-
11.0.0)
- - firebase_analytics (11.3.0):
+ - firebase_analytics (11.3.1):
- Firebase/Analytics (= 11.0.0)
- firebase_core
- Flutter
- firebase_core (3.4.1):
- Firebase/CoreOnly (= 11.0.0)
- Flutter
- - firebase_crashlytics (4.1.0):
+ - firebase_crashlytics (4.1.1):
- Firebase/Crashlytics (= 11.0.0)
- firebase_core
- Flutter
- - firebase_messaging (15.1.0):
+ - firebase_messaging (15.1.1):
- Firebase/Messaging (= 11.0.0)
- firebase_core
- Flutter
- - firebase_performance (0.10.0-5):
+ - firebase_performance (0.10.0-6):
- Firebase/Performance (= 11.0.0)
- firebase_core
- Flutter
@@ -264,6 +264,24 @@ PODS:
- sqflite (0.0.3):
- Flutter
- FlutterMacOS
+ - "sqlite3 (3.46.1+1)":
+ - "sqlite3/common (= 3.46.1+1)"
+ - "sqlite3/common (3.46.1+1)"
+ - "sqlite3/dbstatvtab (3.46.1+1)":
+ - sqlite3/common
+ - "sqlite3/fts5 (3.46.1+1)":
+ - sqlite3/common
+ - "sqlite3/perf-threadsafe (3.46.1+1)":
+ - sqlite3/common
+ - "sqlite3/rtree (3.46.1+1)":
+ - sqlite3/common
+ - sqlite3_flutter_libs (0.0.1):
+ - Flutter
+ - "sqlite3 (~> 3.46.0+1)"
+ - sqlite3/dbstatvtab
+ - sqlite3/fts5
+ - sqlite3/perf-threadsafe
+ - sqlite3/rtree
- SwiftyGif (5.4.5)
- TOCropViewController (2.7.4)
- url_launcher_ios (0.0.1):
@@ -305,6 +323,7 @@ DEPENDENCIES:
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
+ - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
@@ -334,6 +353,7 @@ SPEC REPOS:
- PromisesObjC
- PromisesSwift
- SDWebImage
+ - sqlite3
- SwiftyGif
- TOCropViewController
- WebRTC-SDK
@@ -399,6 +419,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite:
:path: ".symlinks/plugins/sqflite/darwin"
+ sqlite3_flutter_libs:
+ :path: ".symlinks/plugins/sqlite3_flutter_libs/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
volume_controller:
@@ -413,11 +435,11 @@ SPEC CHECKSUMS:
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9
- firebase_analytics: 1a66fe8d4375eccff44671ea37897683a78b2675
+ firebase_analytics: b8ce6c2c4b245d3c3bb3a147965d09da0f455959
firebase_core: ba84e940cf5cbbc601095f86556560937419195c
- firebase_crashlytics: e4f04180f443d5a8b56fbc0685bdbd7d90dd26f0
- firebase_messaging: 15d8b557010f3bb7b98d0302e1c7c8fbcd244425
- firebase_performance: d373c742649e2d85d92cc223b4511c3d132887ef
+ firebase_crashlytics: 4111f8198b78c99471c955af488cecd8224967e6
+ firebase_messaging: c40f84e7a98da956d5262fada373b5c458edcf13
+ firebase_performance: 8b7b9ca5adf3a9b3afa12b4eb96b9cabefc2c248
FirebaseABTesting: c2e22c3aab99afa81d0561708b2c1c356c556976
FirebaseAnalytics: 27eb78b97880ea4a004839b9bac0b58880f5a92a
FirebaseCore: 3cf438f431f18c12cdf2aaf64434648b63f7e383
@@ -460,6 +482,8 @@ SPEC CHECKSUMS:
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
+ sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
+ sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
diff --git a/lib/controllers/chat_events_controller.dart b/lib/controllers/chat_events_controller.dart
index 60f52ef..dad4c7e 100644
--- a/lib/controllers/chat_events_controller.dart
+++ b/lib/controllers/chat_events_controller.dart
@@ -2,45 +2,32 @@ import 'package:get/get.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/models/event.dart';
import 'package:solian/platform.dart';
-import 'package:solian/providers/message/adaptor.dart';
-import 'package:solian/providers/message/events.dart';
+import 'package:solian/providers/database/database.dart';
+import 'package:solian/providers/database/services/messages.dart';
class ChatEventController {
- late final MessageHistoryDb database;
+ late final MessagesFetchingProvider src;
- final RxList currentEvents = RxList.empty(growable: true);
+ final RxList currentEvents =
+ RxList.empty(growable: true);
final RxInt totalEvents = 0.obs;
- final RxBool isLoading = false.obs;
+ final RxBool isLoading = true.obs;
Channel? channel;
String? scope;
Future initialize() async {
- if (!PlatformInfo.isWeb) {
- database = await createHistoryDb();
- }
+ src = Get.find();
currentEvents.clear();
}
- Future getEvent(int id) async {
+ Future getEvent(int id) async {
if (channel == null || scope == null) return null;
-
- if (PlatformInfo.isWeb) {
- final remoteRecord = await getRemoteEvent(id, channel!, scope!);
- if (remoteRecord == null) return null;
- return LocalEvent(
- remoteRecord.id,
- remoteRecord,
- remoteRecord.channelId,
- remoteRecord.createdAt,
- );
- } else {
- return await database.getEvent(id, channel!, scope: scope!);
- }
+ return await src.getEvent(id, channel!, scope: scope!);
}
- Future getEvents(Channel channel, String scope) async {
+ Future getInitialEvents(Channel channel, String scope) async {
this.channel = channel;
this.scope = scope;
@@ -48,24 +35,30 @@ class ChatEventController {
isLoading.value = true;
if (PlatformInfo.isWeb) {
- final result = await getRemoteEvents(
+ final result = await src.fetchRemoteEvents(
channel,
scope,
- remainDepth: 3,
+ depth: 1,
offset: 0,
);
totalEvents.value = result?.$2 ?? 0;
if (result != null) {
for (final x in result.$1.reversed) {
- final entry = LocalEvent(x.id, x, x.channelId, x.createdAt);
+ final entry = LocalMessageEventTableData(
+ id: x.id,
+ channelId: x.channelId,
+ createdAt: x.createdAt,
+ data: x,
+ );
insertEvent(entry);
applyEvent(entry);
}
}
} else {
- final result = await database.syncRemoteEvents(
+ final result = await src.pullRemoteEvents(
channel,
scope: scope,
+ depth: 1,
);
totalEvents.value = result?.$2 ?? 0;
await syncLocal(channel);
@@ -76,22 +69,27 @@ class ChatEventController {
Future loadEvents(Channel channel, String scope) async {
isLoading.value = true;
if (PlatformInfo.isWeb) {
- final result = await getRemoteEvents(
+ final result = await src.fetchRemoteEvents(
channel,
scope,
- remainDepth: 3,
+ depth: 3,
offset: currentEvents.length,
);
if (result != null) {
totalEvents.value = result.$2;
for (final x in result.$1.reversed) {
- final entry = LocalEvent(x.id, x, x.channelId, x.createdAt);
+ final entry = LocalMessageEventTableData(
+ id: x.id,
+ channelId: x.channelId,
+ createdAt: x.createdAt,
+ data: x,
+ );
currentEvents.add(entry);
applyEvent(entry);
}
}
} else {
- final result = await database.syncRemoteEvents(
+ final result = await src.pullRemoteEvents(
channel,
depth: 3,
scope: scope,
@@ -105,7 +103,7 @@ class ChatEventController {
Future syncLocal(Channel channel) async {
if (PlatformInfo.isWeb) return false;
- final data = await database.localEvents.findAllByChannel(channel.id);
+ final data = await src.listEvents(channel);
currentEvents.replaceRange(0, currentEvents.length, data);
for (final x in data.reversed) {
applyEvent(x);
@@ -114,26 +112,29 @@ class ChatEventController {
}
receiveEvent(Event remote) async {
- LocalEvent entry;
+ LocalMessageEventTableData entry;
if (PlatformInfo.isWeb) {
- entry = LocalEvent(
- remote.id,
- remote,
- remote.channelId,
- remote.createdAt,
+ entry = LocalMessageEventTableData(
+ id: remote.id,
+ channelId: remote.channelId,
+ createdAt: remote.createdAt,
+ data: remote,
);
} else {
- entry = await database.receiveEvent(remote);
+ entry = await src.receiveEvent(remote);
}
+ totalEvents.value++;
insertEvent(entry);
applyEvent(entry);
}
- insertEvent(LocalEvent entry) {
+ void insertEvent(LocalMessageEventTableData entry) {
if (entry.channelId != channel?.id) return;
- final idx = currentEvents.indexWhere((x) => x.data.uuid == entry.data.uuid);
+ final idx = currentEvents.indexWhere(
+ (x) => x.data!.uuid == entry.data!.uuid,
+ );
if (idx != -1) {
currentEvents[idx] = entry;
} else {
@@ -141,36 +142,36 @@ class ChatEventController {
}
}
- applyEvent(LocalEvent entry) {
+ void applyEvent(LocalMessageEventTableData entry) {
if (entry.channelId != channel?.id) return;
- switch (entry.data.type) {
+ switch (entry.data!.type) {
case 'messages.edit':
- final body = EventMessageBody.fromJson(entry.data.body);
+ final body = EventMessageBody.fromJson(entry.data!.body);
if (body.relatedEvent != null) {
final idx =
- currentEvents.indexWhere((x) => x.data.id == body.relatedEvent);
+ currentEvents.indexWhere((x) => x.data!.id == body.relatedEvent);
if (idx != -1) {
- currentEvents[idx].data.body = entry.data.body;
- currentEvents[idx].data.updatedAt = entry.data.updatedAt;
+ currentEvents[idx].data!.body = entry.data!.body;
+ currentEvents[idx].data!.updatedAt = entry.data!.updatedAt;
}
}
case 'messages.delete':
- final body = EventMessageBody.fromJson(entry.data.body);
+ final body = EventMessageBody.fromJson(entry.data!.body);
if (body.relatedEvent != null) {
currentEvents.removeWhere((x) => x.id == body.relatedEvent);
}
}
}
- addPendingEvent(Event info) async {
+ Future addPendingEvent(Event info) async {
currentEvents.insert(
0,
- LocalEvent(
- info.id,
- info,
- info.channelId,
- DateTime.now(),
+ LocalMessageEventTableData(
+ id: info.id,
+ channelId: info.channelId,
+ createdAt: DateTime.now(),
+ data: info,
),
);
}
diff --git a/lib/main.dart b/lib/main.dart
index 9263d4c..5bbfb0b 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -13,6 +13,8 @@ import 'package:solian/firebase_options.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/attachment_uploader.dart';
import 'package:solian/providers/daily_sign.dart';
+import 'package:solian/providers/database/database.dart';
+import 'package:solian/providers/database/services/messages.dart';
import 'package:solian/providers/last_read.dart';
import 'package:solian/providers/link_expander.dart';
import 'package:solian/providers/navigation.dart';
@@ -43,6 +45,7 @@ void main() async {
GoRouter.optionURLReflectsImperativeAPIs = true;
+ Get.put(DatabaseProvider());
Get.put(AppTranslations());
await AppTranslations.init();
@@ -135,6 +138,7 @@ class SolianApp extends StatelessWidget {
Get.lazyPut(() => StatusProvider());
Get.lazyPut(() => ChannelProvider());
Get.lazyPut(() => RealmProvider());
+ Get.lazyPut(() => MessagesFetchingProvider());
Get.lazyPut(() => ChatCallProvider());
Get.lazyPut(() => AttachmentUploaderController());
Get.lazyPut(() => LinkExpandProvider());
diff --git a/lib/providers/auth.dart b/lib/providers/auth.dart
index e1541de..840ce67 100644
--- a/lib/providers/auth.dart
+++ b/lib/providers/auth.dart
@@ -6,7 +6,6 @@ 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_events_controller.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart';
import 'package:solian/providers/websocket.dart';
@@ -199,11 +198,6 @@ class AuthProvider extends GetConnect {
Get.find().notifications.clear();
Get.find().notificationUnread.value = 0;
- final chatHistory = ChatEventController();
- chatHistory.initialize().then((_) async {
- await chatHistory.database.localEvents.wipeLocalEvents();
- });
-
storage.deleteAll();
}
diff --git a/lib/providers/database/database.dart b/lib/providers/database/database.dart
new file mode 100644
index 0000000..3ee3795
--- /dev/null
+++ b/lib/providers/database/database.dart
@@ -0,0 +1,24 @@
+import 'package:drift/drift.dart';
+import 'package:drift_flutter/drift_flutter.dart';
+import 'package:get/get.dart' hide Value;
+import 'package:solian/providers/database/tables/messages.dart';
+
+import 'package:solian/models/event.dart';
+
+part 'database.g.dart';
+
+@DriftDatabase(tables: [LocalMessageEventTable])
+class AppDatabase extends _$AppDatabase {
+ AppDatabase() : super(_openConnection());
+
+ @override
+ int get schemaVersion => 1;
+
+ static QueryExecutor _openConnection() {
+ return driftDatabase(name: 'solar_network_local_db');
+ }
+}
+
+class DatabaseProvider extends GetxController {
+ final database = AppDatabase();
+}
diff --git a/lib/providers/database/database.g.dart b/lib/providers/database/database.g.dart
new file mode 100644
index 0000000..9b4c0db
--- /dev/null
+++ b/lib/providers/database/database.g.dart
@@ -0,0 +1,429 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'database.dart';
+
+// ignore_for_file: type=lint
+class $LocalMessageEventTableTable extends LocalMessageEventTable
+ with TableInfo<$LocalMessageEventTableTable, LocalMessageEventTableData> {
+ @override
+ final GeneratedDatabase attachedDatabase;
+ final String? _alias;
+ $LocalMessageEventTableTable(this.attachedDatabase, [this._alias]);
+ static const VerificationMeta _idMeta = const VerificationMeta('id');
+ @override
+ late final GeneratedColumn id = GeneratedColumn(
+ 'id', aliasedName, false,
+ hasAutoIncrement: true,
+ type: DriftSqlType.int,
+ requiredDuringInsert: false,
+ defaultConstraints:
+ GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
+ static const VerificationMeta _channelIdMeta =
+ const VerificationMeta('channelId');
+ @override
+ late final GeneratedColumn channelId = GeneratedColumn(
+ 'channel_id', aliasedName, false,
+ type: DriftSqlType.int, requiredDuringInsert: true);
+ static const VerificationMeta _dataMeta = const VerificationMeta('data');
+ @override
+ late final GeneratedColumnWithTypeConverter data =
+ GeneratedColumn('data', aliasedName, false,
+ type: DriftSqlType.string, requiredDuringInsert: true)
+ .withConverter($LocalMessageEventTableTable.$converterdata);
+ static const VerificationMeta _createdAtMeta =
+ const VerificationMeta('createdAt');
+ @override
+ late final GeneratedColumn createdAt = GeneratedColumn(
+ 'created_at', aliasedName, false,
+ type: DriftSqlType.dateTime,
+ requiredDuringInsert: false,
+ defaultValue: Constant(DateTime.now()));
+ @override
+ List get $columns => [id, channelId, data, createdAt];
+ @override
+ String get aliasedName => _alias ?? actualTableName;
+ @override
+ String get actualTableName => $name;
+ static const String $name = 'local_message_event_table';
+ @override
+ VerificationContext validateIntegrity(
+ Insertable instance,
+ {bool isInserting = false}) {
+ final context = VerificationContext();
+ final data = instance.toColumns(true);
+ if (data.containsKey('id')) {
+ context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
+ }
+ if (data.containsKey('channel_id')) {
+ context.handle(_channelIdMeta,
+ channelId.isAcceptableOrUnknown(data['channel_id']!, _channelIdMeta));
+ } else if (isInserting) {
+ context.missing(_channelIdMeta);
+ }
+ context.handle(_dataMeta, const VerificationResult.success());
+ if (data.containsKey('created_at')) {
+ context.handle(_createdAtMeta,
+ createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
+ }
+ return context;
+ }
+
+ @override
+ Set get $primaryKey => {id};
+ @override
+ LocalMessageEventTableData map(Map data,
+ {String? tablePrefix}) {
+ final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
+ return LocalMessageEventTableData(
+ id: attachedDatabase.typeMapping
+ .read(DriftSqlType.int, data['${effectivePrefix}id'])!,
+ channelId: attachedDatabase.typeMapping
+ .read(DriftSqlType.int, data['${effectivePrefix}channel_id'])!,
+ data: $LocalMessageEventTableTable.$converterdata.fromSql(attachedDatabase
+ .typeMapping
+ .read(DriftSqlType.string, data['${effectivePrefix}data'])!),
+ createdAt: attachedDatabase.typeMapping
+ .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
+ );
+ }
+
+ @override
+ $LocalMessageEventTableTable createAlias(String alias) {
+ return $LocalMessageEventTableTable(attachedDatabase, alias);
+ }
+
+ static TypeConverter $converterdata =
+ const MessageEventConverter();
+}
+
+class LocalMessageEventTableData extends DataClass
+ implements Insertable {
+ final int id;
+ final int channelId;
+ final Event? data;
+ final DateTime createdAt;
+ const LocalMessageEventTableData(
+ {required this.id,
+ required this.channelId,
+ this.data,
+ required this.createdAt});
+ @override
+ Map toColumns(bool nullToAbsent) {
+ final map = {};
+ map['id'] = Variable(id);
+ map['channel_id'] = Variable(channelId);
+ if (!nullToAbsent || data != null) {
+ map['data'] = Variable(
+ $LocalMessageEventTableTable.$converterdata.toSql(data));
+ }
+ map['created_at'] = Variable(createdAt);
+ return map;
+ }
+
+ LocalMessageEventTableCompanion toCompanion(bool nullToAbsent) {
+ return LocalMessageEventTableCompanion(
+ id: Value(id),
+ channelId: Value(channelId),
+ data: data == null && nullToAbsent ? const Value.absent() : Value(data),
+ createdAt: Value(createdAt),
+ );
+ }
+
+ factory LocalMessageEventTableData.fromJson(Map json,
+ {ValueSerializer? serializer}) {
+ serializer ??= driftRuntimeOptions.defaultSerializer;
+ return LocalMessageEventTableData(
+ id: serializer.fromJson(json['id']),
+ channelId: serializer.fromJson(json['channelId']),
+ data: serializer.fromJson(json['data']),
+ createdAt: serializer.fromJson(json['createdAt']),
+ );
+ }
+ @override
+ Map toJson({ValueSerializer? serializer}) {
+ serializer ??= driftRuntimeOptions.defaultSerializer;
+ return {
+ 'id': serializer.toJson(id),
+ 'channelId': serializer.toJson(channelId),
+ 'data': serializer.toJson(data),
+ 'createdAt': serializer.toJson(createdAt),
+ };
+ }
+
+ LocalMessageEventTableData copyWith(
+ {int? id,
+ int? channelId,
+ Value data = const Value.absent(),
+ DateTime? createdAt}) =>
+ LocalMessageEventTableData(
+ id: id ?? this.id,
+ channelId: channelId ?? this.channelId,
+ data: data.present ? data.value : this.data,
+ createdAt: createdAt ?? this.createdAt,
+ );
+ LocalMessageEventTableData copyWithCompanion(
+ LocalMessageEventTableCompanion data) {
+ return LocalMessageEventTableData(
+ id: data.id.present ? data.id.value : this.id,
+ channelId: data.channelId.present ? data.channelId.value : this.channelId,
+ data: data.data.present ? data.data.value : this.data,
+ createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
+ );
+ }
+
+ @override
+ String toString() {
+ return (StringBuffer('LocalMessageEventTableData(')
+ ..write('id: $id, ')
+ ..write('channelId: $channelId, ')
+ ..write('data: $data, ')
+ ..write('createdAt: $createdAt')
+ ..write(')'))
+ .toString();
+ }
+
+ @override
+ int get hashCode => Object.hash(id, channelId, data, createdAt);
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ (other is LocalMessageEventTableData &&
+ other.id == this.id &&
+ other.channelId == this.channelId &&
+ other.data == this.data &&
+ other.createdAt == this.createdAt);
+}
+
+class LocalMessageEventTableCompanion
+ extends UpdateCompanion {
+ final Value id;
+ final Value channelId;
+ final Value data;
+ final Value createdAt;
+ const LocalMessageEventTableCompanion({
+ this.id = const Value.absent(),
+ this.channelId = const Value.absent(),
+ this.data = const Value.absent(),
+ this.createdAt = const Value.absent(),
+ });
+ LocalMessageEventTableCompanion.insert({
+ this.id = const Value.absent(),
+ required int channelId,
+ required Event? data,
+ this.createdAt = const Value.absent(),
+ }) : channelId = Value(channelId),
+ data = Value(data);
+ static Insertable custom({
+ Expression? id,
+ Expression? channelId,
+ Expression? data,
+ Expression? createdAt,
+ }) {
+ return RawValuesInsertable({
+ if (id != null) 'id': id,
+ if (channelId != null) 'channel_id': channelId,
+ if (data != null) 'data': data,
+ if (createdAt != null) 'created_at': createdAt,
+ });
+ }
+
+ LocalMessageEventTableCompanion copyWith(
+ {Value? id,
+ Value? channelId,
+ Value? data,
+ Value? createdAt}) {
+ return LocalMessageEventTableCompanion(
+ id: id ?? this.id,
+ channelId: channelId ?? this.channelId,
+ data: data ?? this.data,
+ createdAt: createdAt ?? this.createdAt,
+ );
+ }
+
+ @override
+ Map toColumns(bool nullToAbsent) {
+ final map = {};
+ if (id.present) {
+ map['id'] = Variable(id.value);
+ }
+ if (channelId.present) {
+ map['channel_id'] = Variable(channelId.value);
+ }
+ if (data.present) {
+ map['data'] = Variable(
+ $LocalMessageEventTableTable.$converterdata.toSql(data.value));
+ }
+ if (createdAt.present) {
+ map['created_at'] = Variable(createdAt.value);
+ }
+ return map;
+ }
+
+ @override
+ String toString() {
+ return (StringBuffer('LocalMessageEventTableCompanion(')
+ ..write('id: $id, ')
+ ..write('channelId: $channelId, ')
+ ..write('data: $data, ')
+ ..write('createdAt: $createdAt')
+ ..write(')'))
+ .toString();
+ }
+}
+
+abstract class _$AppDatabase extends GeneratedDatabase {
+ _$AppDatabase(QueryExecutor e) : super(e);
+ $AppDatabaseManager get managers => $AppDatabaseManager(this);
+ late final $LocalMessageEventTableTable localMessageEventTable =
+ $LocalMessageEventTableTable(this);
+ @override
+ Iterable> get allTables =>
+ allSchemaEntities.whereType>();
+ @override
+ List get allSchemaEntities => [localMessageEventTable];
+}
+
+typedef $$LocalMessageEventTableTableCreateCompanionBuilder
+ = LocalMessageEventTableCompanion Function({
+ Value id,
+ required int channelId,
+ required Event? data,
+ Value createdAt,
+});
+typedef $$LocalMessageEventTableTableUpdateCompanionBuilder
+ = LocalMessageEventTableCompanion Function({
+ Value id,
+ Value channelId,
+ Value data,
+ Value createdAt,
+});
+
+class $$LocalMessageEventTableTableFilterComposer
+ extends FilterComposer<_$AppDatabase, $LocalMessageEventTableTable> {
+ $$LocalMessageEventTableTableFilterComposer(super.$state);
+ ColumnFilters get id => $state.composableBuilder(
+ column: $state.table.id,
+ builder: (column, joinBuilders) =>
+ ColumnFilters(column, joinBuilders: joinBuilders));
+
+ ColumnFilters get channelId => $state.composableBuilder(
+ column: $state.table.channelId,
+ builder: (column, joinBuilders) =>
+ ColumnFilters(column, joinBuilders: joinBuilders));
+
+ ColumnWithTypeConverterFilters get data =>
+ $state.composableBuilder(
+ column: $state.table.data,
+ builder: (column, joinBuilders) => ColumnWithTypeConverterFilters(
+ column,
+ joinBuilders: joinBuilders));
+
+ ColumnFilters get createdAt => $state.composableBuilder(
+ column: $state.table.createdAt,
+ builder: (column, joinBuilders) =>
+ ColumnFilters(column, joinBuilders: joinBuilders));
+}
+
+class $$LocalMessageEventTableTableOrderingComposer
+ extends OrderingComposer<_$AppDatabase, $LocalMessageEventTableTable> {
+ $$LocalMessageEventTableTableOrderingComposer(super.$state);
+ ColumnOrderings get id => $state.composableBuilder(
+ column: $state.table.id,
+ builder: (column, joinBuilders) =>
+ ColumnOrderings(column, joinBuilders: joinBuilders));
+
+ ColumnOrderings get channelId => $state.composableBuilder(
+ column: $state.table.channelId,
+ builder: (column, joinBuilders) =>
+ ColumnOrderings(column, joinBuilders: joinBuilders));
+
+ ColumnOrderings get data => $state.composableBuilder(
+ column: $state.table.data,
+ builder: (column, joinBuilders) =>
+ ColumnOrderings(column, joinBuilders: joinBuilders));
+
+ ColumnOrderings get createdAt => $state.composableBuilder(
+ column: $state.table.createdAt,
+ builder: (column, joinBuilders) =>
+ ColumnOrderings(column, joinBuilders: joinBuilders));
+}
+
+class $$LocalMessageEventTableTableTableManager extends RootTableManager<
+ _$AppDatabase,
+ $LocalMessageEventTableTable,
+ LocalMessageEventTableData,
+ $$LocalMessageEventTableTableFilterComposer,
+ $$LocalMessageEventTableTableOrderingComposer,
+ $$LocalMessageEventTableTableCreateCompanionBuilder,
+ $$LocalMessageEventTableTableUpdateCompanionBuilder,
+ (
+ LocalMessageEventTableData,
+ BaseReferences<_$AppDatabase, $LocalMessageEventTableTable,
+ LocalMessageEventTableData>
+ ),
+ LocalMessageEventTableData,
+ PrefetchHooks Function()> {
+ $$LocalMessageEventTableTableTableManager(
+ _$AppDatabase db, $LocalMessageEventTableTable table)
+ : super(TableManagerState(
+ db: db,
+ table: table,
+ filteringComposer: $$LocalMessageEventTableTableFilterComposer(
+ ComposerState(db, table)),
+ orderingComposer: $$LocalMessageEventTableTableOrderingComposer(
+ ComposerState(db, table)),
+ updateCompanionCallback: ({
+ Value id = const Value.absent(),
+ Value channelId = const Value.absent(),
+ Value data = const Value.absent(),
+ Value createdAt = const Value.absent(),
+ }) =>
+ LocalMessageEventTableCompanion(
+ id: id,
+ channelId: channelId,
+ data: data,
+ createdAt: createdAt,
+ ),
+ createCompanionCallback: ({
+ Value id = const Value.absent(),
+ required int channelId,
+ required Event? data,
+ Value createdAt = const Value.absent(),
+ }) =>
+ LocalMessageEventTableCompanion.insert(
+ id: id,
+ channelId: channelId,
+ data: data,
+ createdAt: createdAt,
+ ),
+ withReferenceMapper: (p0) => p0
+ .map((e) => (e.readTable(table), BaseReferences(db, table, e)))
+ .toList(),
+ prefetchHooksCallback: null,
+ ));
+}
+
+typedef $$LocalMessageEventTableTableProcessedTableManager
+ = ProcessedTableManager<
+ _$AppDatabase,
+ $LocalMessageEventTableTable,
+ LocalMessageEventTableData,
+ $$LocalMessageEventTableTableFilterComposer,
+ $$LocalMessageEventTableTableOrderingComposer,
+ $$LocalMessageEventTableTableCreateCompanionBuilder,
+ $$LocalMessageEventTableTableUpdateCompanionBuilder,
+ (
+ LocalMessageEventTableData,
+ BaseReferences<_$AppDatabase, $LocalMessageEventTableTable,
+ LocalMessageEventTableData>
+ ),
+ LocalMessageEventTableData,
+ PrefetchHooks Function()>;
+
+class $AppDatabaseManager {
+ final _$AppDatabase _db;
+ $AppDatabaseManager(this._db);
+ $$LocalMessageEventTableTableTableManager get localMessageEventTable =>
+ $$LocalMessageEventTableTableTableManager(
+ _db, _db.localMessageEventTable);
+}
diff --git a/lib/providers/database/services/messages.dart b/lib/providers/database/services/messages.dart
new file mode 100644
index 0000000..7251a44
--- /dev/null
+++ b/lib/providers/database/services/messages.dart
@@ -0,0 +1,203 @@
+import 'package:drift/drift.dart';
+import 'package:get/get.dart' hide Value;
+import 'package:solian/exceptions/request.dart';
+import 'package:solian/models/channel.dart';
+import 'package:solian/models/event.dart';
+import 'package:solian/models/pagination.dart';
+import 'package:solian/providers/auth.dart';
+import 'package:solian/providers/database/database.dart';
+
+class MessagesFetchingProvider extends GetxController {
+ Future<(List, int)?> getWhatsNewEvents(int pivot, {take = 10}) async {
+ final AuthProvider auth = Get.find();
+ if (auth.isAuthorized.isFalse) return null;
+
+ final client = auth.configureClient('messaging');
+
+ final resp = await client.get(
+ '/whats-new?pivot=$pivot&take=$take',
+ );
+
+ if (resp.statusCode != 200) {
+ throw RequestException(resp);
+ }
+
+ final PaginationResult response = PaginationResult.fromJson(resp.body);
+ final result =
+ response.data?.map((e) => Event.fromJson(e)).toList() ?? List.empty();
+
+ return (result, response.count);
+ }
+
+ Future fetchRemoteEvent(int id, Channel channel, String scope) async {
+ final AuthProvider auth = Get.find();
+ if (auth.isAuthorized.isFalse) return null;
+
+ final client = auth.configureClient('messaging');
+
+ final resp = await client.get(
+ '/channels/$scope/${channel.alias}/events/$id',
+ );
+
+ if (resp.statusCode == 404) {
+ return null;
+ } else if (resp.statusCode != 200) {
+ throw RequestException(resp);
+ }
+
+ return Event.fromJson(resp.body);
+ }
+
+ Future<(List, int)?> fetchRemoteEvents(
+ Channel channel,
+ String scope, {
+ required int depth,
+ bool Function(List items)? onBrake,
+ take = 10,
+ offset = 0,
+ }) async {
+ if (depth <= 0) {
+ return null;
+ }
+
+ final AuthProvider auth = Get.find();
+ if (auth.isAuthorized.isFalse) return null;
+
+ final client = auth.configureClient('messaging');
+
+ final resp = await client.get(
+ '/channels/$scope/${channel.alias}/events?take=$take&offset=$offset',
+ );
+
+ if (resp.statusCode != 200) {
+ throw RequestException(resp);
+ }
+
+ final PaginationResult response = PaginationResult.fromJson(resp.body);
+ final result =
+ response.data?.map((e) => Event.fromJson(e)).toList() ?? List.empty();
+
+ if (onBrake != null && onBrake(result)) {
+ return (result, response.count);
+ }
+
+ final expandResult = (await fetchRemoteEvents(
+ channel,
+ scope,
+ depth: depth - 1,
+ take: take,
+ offset: offset + result.length,
+ ))
+ ?.$1 ??
+ List.empty();
+
+ return ([...result, ...expandResult], response.count);
+ }
+
+ Future receiveEvent(Event remote) async {
+ // Insert record
+ final database = Get.find().database;
+ final entry = await database
+ .into(database.localMessageEventTable)
+ .insertReturning(LocalMessageEventTableCompanion.insert(
+ id: Value(remote.id),
+ channelId: remote.channelId,
+ data: remote,
+ createdAt: Value(remote.createdAt),
+ ));
+
+ // Handle side-effect like editing and deleting
+ switch (remote.type) {
+ case 'messages.edit':
+ final body = EventMessageBody.fromJson(remote.body);
+ if (body.relatedEvent != null) {
+ final target = await (database.select(database.localMessageEventTable)
+ ..where((x) => x.id.equals(body.relatedEvent!)))
+ .getSingleOrNull();
+ if (target != null) {
+ target.data!.body = remote.body;
+ target.data!.updatedAt = remote.updatedAt;
+ await (database.update(database.localMessageEventTable)
+ ..where((x) => x.id.equals(target.id)))
+ .write(
+ LocalMessageEventTableCompanion(data: Value(target.data)),
+ );
+ }
+ }
+ case 'messages.delete':
+ final body = EventMessageBody.fromJson(remote.body);
+ if (body.relatedEvent != null) {
+ await (database.delete(database.localMessageEventTable)
+ ..where((x) => x.id.equals(body.relatedEvent!)))
+ .go();
+ }
+ }
+ return entry;
+ }
+
+ Future getEvent(int id, Channel channel,
+ {String scope = 'global'}) async {
+ final database = Get.find().database;
+ final localRecord = await (database.select(database.localMessageEventTable)
+ ..where((x) => x.id.equals(id)))
+ .getSingleOrNull();
+ if (localRecord != null) return localRecord;
+
+ final remoteRecord = await fetchRemoteEvent(id, channel, scope);
+ if (remoteRecord == null) return null;
+
+ return await receiveEvent(remoteRecord);
+ }
+
+ /// Pull the remote events to local database
+ Future<(List, int)?> pullRemoteEvents(Channel channel,
+ {String scope = 'global', depth = 10, offset = 0}) async {
+ final database = Get.find().database;
+ final lastOne = await (database.select(database.localMessageEventTable)
+ ..where((x) => x.channelId.equals(channel.id))
+ ..orderBy([(t) => OrderingTerm.desc(t.id)])
+ ..limit(1))
+ .getSingleOrNull();
+
+ final data = await fetchRemoteEvents(
+ channel,
+ scope,
+ depth: depth,
+ offset: offset,
+ onBrake: (items) {
+ return items.any((x) => x.id == lastOne?.id);
+ },
+ );
+ if (data != null) {
+ await database.batch((batch) {
+ batch.insertAllOnConflictUpdate(
+ database.localMessageEventTable,
+ data.$1.map((x) => LocalMessageEventTableCompanion(
+ id: Value(x.id),
+ channelId: Value(x.channelId),
+ data: Value(x),
+ createdAt: Value(x.createdAt),
+ )),
+ );
+ });
+ }
+
+ return data;
+ }
+
+ Future> listEvents(Channel channel) async {
+ final database = Get.find().database;
+ return await (database.select(database.localMessageEventTable)
+ ..where((x) => x.channelId.equals(channel.id))
+ ..orderBy([(t) => OrderingTerm.desc(t.id)]))
+ .get();
+ }
+
+ Future getLastInChannel(Channel channel) async {
+ final database = Get.find().database;
+ return await (database.select(database.localMessageEventTable)
+ ..where((x) => x.channelId.equals(channel.id))
+ ..orderBy([(t) => OrderingTerm.desc(t.id)]))
+ .getSingleOrNull();
+ }
+}
diff --git a/lib/providers/database/tables/json.dart b/lib/providers/database/tables/json.dart
new file mode 100644
index 0000000..f7f4e24
--- /dev/null
+++ b/lib/providers/database/tables/json.dart
@@ -0,0 +1,13 @@
+import 'dart:convert';
+
+import 'package:drift/drift.dart';
+
+class JsonConverter extends TypeConverter