Surface/lib/providers/channel.dart

212 lines
6.9 KiB
Dart

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<SnNetworkProvider>();
_ud = context.read<UserDirectoryProvider>();
_ua = context.read<UserProvider>();
_dt = context.read<DatabaseProvider>();
_rels = context.read<SnRealmProvider>();
}
Future<void> _saveChannelToLocal(Iterable<SnChannel> 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<List<SnChannel>> _fetchChannelsFromServer({
bool doNotSave = false,
}) async {
final resp = await _sn.client.get('/cgi/im/channels/me/available');
final out = List<SnChannel>.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<SnChannel> 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<List<SnChannel>> 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<SnChannel> 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<List<SnChatMessage>> getLastMessages(
Iterable<SnChannel> channels,
) async {
final result = List<Future<SnLocalChatMessageData?>>.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<void> _saveMemberToLocal(Iterable<SnChannelMember> 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<void> 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<void> updateChannelProfile(SnChannelMember member) {
return _saveMemberToLocal([member]);
}
Future<SnChannelMember> 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;
}
}