import 'dart:convert'; import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:surface/database/database.dart'; import 'package:surface/providers/database.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_realm.dart'; import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/userinfo.dart'; import 'package:surface/types/chat.dart'; class ChatChannelProvider extends ChangeNotifier { static const kChatChannelBoxName = 'nex_chat_channels'; late final SnNetworkProvider _sn; late final UserDirectoryProvider _ud; late final UserProvider _ua; late final DatabaseProvider _dt; late final SnRealmProvider _rels; ChatChannelProvider(BuildContext context) { _sn = context.read(); _ud = context.read(); _ua = context.read(); _dt = context.read(); _rels = context.read(); } Future _saveChannelToLocal(Iterable channels) async { await Future.wait( channels.map( (ele) => _dt.db.snLocalChatChannel.insertOne( SnLocalChatChannelCompanion.insert( id: Value(ele.id), alias: ele.key, content: ele, createdAt: Value(ele.createdAt), ), onConflict: DoUpdate( (_) => SnLocalChatChannelCompanion.custom( content: Constant(jsonEncode(ele.toJson())), ), ), ), ), ); } Future> _fetchChannelsFromServer({ bool doNotSave = false, }) async { final resp = await _sn.client.get('/cgi/im/channels/me/available'); final out = List.from( resp.data?.map((e) => SnChannel.fromJson(e)) ?? [], ); if (!doNotSave) _saveChannelToLocal(out); return out; } /// The get channel method will return the channel with the given alias. /// It will use the local storage as much as possible. /// The alias should include the scope, formatted as `scope:alias`. Future getChannel(String key) async { final local = await (_dt.db.snLocalChatChannel.select() ..where((e) => e.alias.equals(key))) .getSingleOrNull(); if (local != null) { final out = local.content; if (out.realmId != null) { return out.copyWith(realm: await _rels.getRealm(out.realmId!)); } else { return out; } } var resp = await _sn.client.get('/cgi/im/channels/${key.replaceAll(':', '/')}'); var out = SnChannel.fromJson(resp.data); // Preload realm of the channel if (out.realmId != null) { out = out.copyWith(realm: await _rels.getRealm(out.realmId!)); } _saveChannelToLocal([out]); return out; } /// The fetch channel method return a stream, which will emit twice. /// The first time is when the data was fetched from the local storage. /// And the second time is when the data was fetched from the server. /// But there is some exception that will only cause one of them to be emitted. /// Like the local storage is broken or the server is down. Stream> fetchChannels( {bool noRemote = false, bool noLocal = false}) async* { if (!noLocal) { final local = await (_dt.db.snLocalChatChannel.select() ..orderBy([ (e) => OrderingTerm(expression: e.createdAt, mode: OrderingMode.desc) ])) .get(); final out = local.map((e) => e.content).toList(); for (var idx = 0; idx < out.length; idx++) { final channel = out[idx]; if (channel.realmId != null) { out[idx] = out[idx].copyWith( realm: await _rels.getRealm(channel.realmId!), ); } } yield out; } if (noRemote) return; final List result = List.empty(growable: true); final channels = await _fetchChannelsFromServer(); for (var idx = 0; idx < channels.length; idx++) { final channel = channels[idx]; if (channel.realmId != null) { channels[idx] = channels[idx].copyWith( realm: await _rels.getRealm(channel.realmId!), ); } } result.addAll(channels); yield result; } Future> getLastMessages( Iterable channels, ) async { final result = List>.empty(growable: true); for (final channel in channels) { final out = (_dt.db.snLocalChatMessage.select() ..where((e) => e.channelId.equals(channel.id)) ..orderBy([ (e) => OrderingTerm(expression: e.createdAt, mode: OrderingMode.desc) ]) ..limit(1)) .getSingleOrNull(); result.add(out); } final out = (await Future.wait(result)) .where((e) => e != null) .map((e) => e!.content) .toList(); await _ud.listAccount(out.map((ele) => ele.sender.accountId).toSet()); return out; } Future _saveMemberToLocal(Iterable members) async { final queries = members.map((ele) { return _dt.db.snLocalChannelMember.insertOne( SnLocalChannelMemberCompanion.insert( id: Value(ele.id), channelId: ele.channelId, accountId: ele.accountId, content: ele, cacheExpiredAt: DateTime.now().add(const Duration(days: 7)), ), onConflict: DoUpdate( (_) => SnLocalChannelMemberCompanion.custom( content: Constant(jsonEncode(ele.toJson())), cacheExpiredAt: Constant(DateTime.now().add(const Duration(days: 7))), ), ), ); }); await Future.wait(queries); } Future removeLocalChannel(SnChannel channel) async { await _dt.db.transaction(() async { await (_dt.db.snLocalChannelMember.delete() ..where((e) => e.channelId.equals(channel.id))) .go(); await (_dt.db.snLocalChatChannel.delete() ..where((e) => e.id.equals(channel.id))) .go(); await (_dt.db.snLocalChatMessage.delete() ..where((e) => e.channelId.equals(channel.id))) .go(); }); } Future updateChannelProfile(SnChannelMember member) { return _saveMemberToLocal([member]); } Future getChannelProfile(SnChannel channel) async { if (_ua.user == null) throw Exception('User not logged in'); final local = await (_dt.db.snLocalChannelMember.select() ..where((e) => e.channelId.equals(channel.id)) ..where((e) => e.accountId.equals(_ua.user!.id))) .getSingleOrNull(); if (local != null) { return local.content; } final resp = await _sn.client.get('/cgi/im/channels/${channel.keyPath}/me'); final out = SnChannelMember.fromJson(resp.data); _saveMemberToLocal([out]); return out; } }