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: () {