chat refactor

This commit is contained in:
Christien Rioux 2024-01-30 17:03:14 -05:00
parent 4a8958a868
commit 03a6a781a6
18 changed files with 239 additions and 333 deletions

View File

@ -1 +1,2 @@
export 'cubits/cubits.dart';
export 'views/views.dart';

View File

@ -0,0 +1,10 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:veilid_support/veilid_support.dart';
class ActiveChatCubit extends Cubit<TypedKey?> {
ActiveChatCubit(super.initialState);
void setActiveChat(TypedKey? activeChat) {
emit(activeChat);
}
}

View File

@ -0,0 +1 @@
export 'active_chat_cubit.dart';

View File

@ -0,0 +1,2 @@
export 'cubits/cubits.dart';
export 'views/views.dart';

View File

@ -0,0 +1,72 @@
import 'dart:async';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../proto/proto.dart' as proto;
//////////////////////////////////////////////////
//////////////////////////////////////////////////
// Mutable state for per-account chat list
class ChatListCubit extends DHTShortArrayCubit<proto.Chat> {
ChatListCubit({
required ActiveAccountInfo activeAccountInfo,
required proto.Account account,
}) : super(
open: () => _open(activeAccountInfo, account),
decodeElement: proto.Chat.fromBuffer);
static Future<DHTShortArray> _open(
ActiveAccountInfo activeAccountInfo, proto.Account account) async {
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final chatListRecordKey =
proto.OwnedDHTRecordPointerProto.fromProto(account.chatList);
final dhtRecord = await DHTShortArray.openOwned(chatListRecordKey,
parent: accountRecordKey);
return dhtRecord;
}
/// Create a new chat (singleton for single contact chats)
Future<void> getOrCreateChatSingleContact({
required TypedKey remoteConversationRecordKey,
}) async {
// Create conversation type Chat
final chat = proto.Chat()
..type = proto.ChatType.SINGLE_CONTACT
..remoteConversationKey = remoteConversationRecordKey.toProto();
// Add Chat to account's list
// if this fails, don't keep retrying, user can try again later
if (await shortArray.tryAddItem(chat.writeToBuffer()) == false) {
throw Exception('Failed to add chat');
}
}
/// Delete a chat
Future<void> deleteChat(
{required TypedKey remoteConversationRecordKey}) async {
// Create conversation type Chat
final remoteConversationKey = remoteConversationRecordKey.toProto();
// Remove Chat from account's list
// if this fails, don't keep retrying, user can try again later
for (var i = 0; i < shortArray.length; i++) {
final cbuf = await shortArray.getItem(i);
if (cbuf == null) {
throw Exception('Failed to get chat');
}
final c = proto.Chat.fromBuffer(cbuf);
if (c.remoteConversationKey == remoteConversationKey) {
await shortArray.tryRemoveItem(i);
return;
}
}
}
}

View File

@ -0,0 +1 @@
export 'chat_list_cubit.dart';

View File

@ -1,21 +1,21 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:flutter_translate/flutter_translate.dart';
import '../proto/proto.dart' as proto;
import '../providers/account.dart';
import '../providers/chat.dart';
import '../theme/theme.dart';
import '../../proto/proto.dart' as proto;
import '../../theme/theme.dart';
class ChatSingleContactItemWidget extends ConsumerWidget {
const ChatSingleContactItemWidget({required this.contact, super.key});
class ChatSingleContactItemWidget extends StatelessWidget {
const ChatSingleContactItemWidget({required proto.Contact contact, super.key})
: _contact = contact;
final proto.Contact contact;
final proto.Contact _contact;
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context, WidgetRef ref) {
Widget build(
BuildContext context,
) {
final theme = Theme.of(context);
//final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;

View File

@ -1,2 +1,3 @@
export 'cubits/cubits.dart';
export 'models/models.dart';
export 'views/views.dart';

View File

@ -0,0 +1,104 @@
import 'dart:async';
import 'dart:convert';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart';
//////////////////////////////////////////////////
// Mutable state for per-account contacts
class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
ContactListCubit({
required ActiveAccountInfo activeAccountInfo,
required proto.Account account,
}) : _activeAccountInfo = activeAccountInfo,
super(
open: () => _open(activeAccountInfo, account),
decodeElement: proto.Contact.fromBuffer);
static Future<DHTShortArray> _open(
ActiveAccountInfo activeAccountInfo, proto.Account account) async {
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final contactListRecordKey =
proto.OwnedDHTRecordPointerProto.fromProto(account.contactList);
final dhtRecord = await DHTShortArray.openOwned(contactListRecordKey,
parent: accountRecordKey);
return dhtRecord;
}
Future<void> createContact({
required proto.Profile remoteProfile,
required IdentityMaster remoteIdentity,
required TypedKey remoteConversationRecordKey,
required TypedKey localConversationRecordKey,
}) async {
// Create Contact
final contact = proto.Contact()
..editedProfile = remoteProfile
..remoteProfile = remoteProfile
..identityMasterJson = jsonEncode(remoteIdentity.toJson())
..identityPublicKey = TypedKey(
kind: remoteIdentity.identityRecordKey.kind,
value: remoteIdentity.identityPublicKey)
.toProto()
..remoteConversationRecordKey = remoteConversationRecordKey.toProto()
..localConversationRecordKey = localConversationRecordKey.toProto()
..showAvailability = false;
// Add Contact to account's list
// if this fails, don't keep retrying, user can try again later
if (await shortArray.tryAddItem(contact.writeToBuffer()) == false) {
throw Exception('Failed to add contact record');
}
}
Future<void> deleteContact({required proto.Contact contact}) async {
final pool = DHTRecordPool.instance;
final accountRecordKey =
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final localConversationKey =
proto.TypedKeyProto.fromProto(contact.localConversationRecordKey);
final remoteConversationKey =
proto.TypedKeyProto.fromProto(contact.remoteConversationRecordKey);
// Remove Contact from account's list
for (var i = 0; i < shortArray.length; i++) {
final item =
await shortArray.getItemProtobuf(proto.Contact.fromBuffer, i);
if (item == null) {
throw Exception('Failed to get contact');
}
if (item.remoteConversationRecordKey ==
contact.remoteConversationRecordKey) {
await shortArray.tryRemoveItem(i);
break;
}
}
try {
await (await pool.openRead(localConversationKey,
parent: accountRecordKey))
.delete();
} on Exception catch (e) {
log.debug('error removing local conversation record key: $e', e);
}
try {
if (localConversationKey != remoteConversationKey) {
await (await pool.openRead(remoteConversationKey,
parent: accountRecordKey))
.delete();
}
} on Exception catch (e) {
log.debug('error removing remote conversation record key: $e', e);
}
}
//
final ActiveAccountInfo _activeAccountInfo;
}

View File

@ -0,0 +1 @@
export 'contact_list_cubit.dart';

View File

@ -1,132 +0,0 @@
import 'dart:convert';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../proto/proto.dart' as proto;
import '../../../packages/veilid_support/veilid_support.dart';
import '../../tools/tools.dart';
import '../../old_to_refactor/providers/account.dart';
import '../../old_to_refactor/providers/chat.dart';
part '../../old_to_refactor/providers/contact.g.dart';
Future<void> createContact({
required ActiveAccountInfo activeAccountInfo,
required proto.Profile profile,
required IdentityMaster remoteIdentity,
required TypedKey remoteConversationRecordKey,
required TypedKey localConversationRecordKey,
}) async {
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
// Create Contact
final contact = proto.Contact()
..editedProfile = profile
..remoteProfile = profile
..identityMasterJson = jsonEncode(remoteIdentity.toJson())
..identityPublicKey = TypedKey(
kind: remoteIdentity.identityRecordKey.kind,
value: remoteIdentity.identityPublicKey)
.toProto()
..remoteConversationRecordKey = remoteConversationRecordKey.toProto()
..localConversationRecordKey = localConversationRecordKey.toProto()
..showAvailability = false;
// Add Contact to account's list
// if this fails, don't keep retrying, user can try again later
await (await DHTShortArray.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(
activeAccountInfo.account.contactList),
parent: accountRecordKey))
.scope((contactList) async {
if (await contactList.tryAddItem(contact.writeToBuffer()) == false) {
throw Exception('Failed to add contact');
}
});
}
Future<void> deleteContact(
{required ActiveAccountInfo activeAccountInfo,
required proto.Contact contact}) async {
final pool = await DHTRecordPool.instance();
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final localConversationKey =
proto.TypedKeyProto.fromProto(contact.localConversationRecordKey);
final remoteConversationKey =
proto.TypedKeyProto.fromProto(contact.remoteConversationRecordKey);
// Remove any chats for this contact
await deleteChat(
activeAccountInfo: activeAccountInfo,
remoteConversationRecordKey: remoteConversationKey);
// Remove Contact from account's list
await (await DHTShortArray.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(
activeAccountInfo.account.contactList),
parent: accountRecordKey))
.scope((contactList) async {
for (var i = 0; i < contactList.length; i++) {
final item =
await contactList.getItemProtobuf(proto.Contact.fromBuffer, i);
if (item == null) {
throw Exception('Failed to get contact');
}
if (item.remoteConversationRecordKey ==
contact.remoteConversationRecordKey) {
await contactList.tryRemoveItem(i);
break;
}
}
try {
await (await pool.openRead(localConversationKey,
parent: accountRecordKey))
.delete();
} on Exception catch (e) {
log.debug('error removing local conversation record key: $e', e);
}
try {
if (localConversationKey != remoteConversationKey) {
await (await pool.openRead(remoteConversationKey,
parent: accountRecordKey))
.delete();
}
} on Exception catch (e) {
log.debug('error removing remote conversation record key: $e', e);
}
});
}
/// Get the active account contact list
@riverpod
Future<IList<proto.Contact>?> fetchContactList(FetchContactListRef ref) async {
// See if we've logged into this account or if it is locked
final activeAccountInfo = await ref.watch(fetchActiveAccountProvider.future);
if (activeAccountInfo == null) {
return null;
}
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
// Decode the contact list from the DHT
IList<proto.Contact> out = const IListConst([]);
await (await DHTShortArray.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(
activeAccountInfo.account.contactList),
parent: accountRecordKey))
.scope((cList) async {
for (var i = 0; i < cList.length; i++) {
final cir = await cList.getItem(i);
if (cir == null) {
throw Exception('Failed to get contact');
}
out = out.add(proto.Contact.fromBuffer(cir));
}
});
return out;
}

View File

@ -1,10 +1,13 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:flutter_translate/flutter_translate.dart';
import '../../chat_list/chat_list.dart';
import '../../proto/proto.dart' as proto;
import '../../theme/theme.dart';
import '../contacts.dart';
class ContactItemWidget extends StatelessWidget {
const ContactItemWidget({required this.contact, super.key});
@ -38,16 +41,15 @@ class ContactItemWidget extends StatelessWidget {
children: [
SlidableAction(
onPressed: (context) async {
final activeAccountInfo =
await ref.read(fetchActiveAccountProvider.future);
if (activeAccountInfo != null) {
await deleteContact(
activeAccountInfo: activeAccountInfo,
contact: contact);
ref
..invalidate(fetchContactListProvider)
..invalidate(fetchChatListProvider);
}
final contactListCubit = context.read<ContactListCubit>();
final chatListCubit = context.read<ChatListCubit>();
// Remove any chats for this contact
await chatListCubit.deleteChat(
remoteConversationRecordKey: remoteConversationKey);
// Delete the contact itself
await contactListCubit.deleteContact(contact: contact);
},
backgroundColor: scale.tertiaryScale.background,
foregroundColor: scale.tertiaryScale.text,

View File

@ -2,16 +2,16 @@ import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:searchable_listview/searchable_listview.dart';
import '../proto/proto.dart' as proto;
import '../tools/tools.dart';
import '../../proto/proto.dart' as proto;
import '../../theme/theme.dart';
import '../../tools/tools.dart';
import 'contact_item_widget.dart';
import 'empty_contact_list_widget.dart';
class ContactListWidget extends ConsumerWidget {
class ContactListWidget extends StatelessWidget {
const ContactListWidget({required this.contactList, super.key});
final IList<proto.Contact> contactList;
@ -22,7 +22,7 @@ class ContactListWidget extends ConsumerWidget {
}
@override
Widget build(BuildContext context, WidgetRef ref) {
Widget build(BuildContext context) {
final theme = Theme.of(context);
//final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;

View File

@ -1,8 +1,6 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../theme/theme.dart';
@ -39,8 +37,7 @@ class HomePageState extends State<HomePage> with TickerProviderStateMixin {
super.dispose();
}
Widget buildWithLogin(BuildContext context, IList<LocalAccount> localAccounts,
Typed<FixedEncodedString43>? activeUserLogin) {
Widget buildWithLogin(BuildContext context) {
final activeUserLogin = context.watch<ActiveUserLoginCubit>().state;
if (activeUserLogin == null) {
@ -64,7 +61,7 @@ class HomePageState extends State<HomePage> with TickerProviderStateMixin {
child: BlocProvider(
create: (context) => AccountRecordCubit(
record: accountInfo.activeAccountInfo!.accountRecord),
child: HomeAccountReady()));
child: const HomeAccountReady()));
}
}

View File

@ -6,6 +6,8 @@ import 'package:flutter_translate/flutter_translate.dart';
import 'package:go_router/go_router.dart';
import '../../../account_manager/account_manager.dart';
import '../../../chat/chat.dart';
import '../../../chat_list/chat_list.dart';
import '../../../contact_invitation/contact_invitation.dart';
import '../../../theme/theme.dart';
import '../../../tools/tools.dart';
@ -106,9 +108,18 @@ class HomeAccountReadyState extends State<HomeAccountReady>
return waitingPage(context);
}
return BlocProvider(
create: (context) => ContactInvitationListCubit(
activeAccountInfo: activeAccountInfo, account: accountData.value),
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => ContactInvitationListCubit(
activeAccountInfo: activeAccountInfo,
account: accountData.value)),
BlocProvider(
create: (context) => ChatListCubit(
activeAccountInfo: activeAccountInfo,
account: accountData.value)),
BlocProvider(create: (context) => ActiveChatCubit(null))
],
child: responsiveVisibility(
context: context,
phone: false,

View File

@ -1,12 +1,11 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../../../contact_invitation/contact_invitation.dart';
import '../../../../contacts/contacts.dart';
import '../../../../theme/theme.dart';
class AccountPage extends StatefulWidget {

View File

@ -27,14 +27,9 @@ class ChatsPageState extends State<ChatsPage> {
super.dispose();
}
/// We have an active, unlocked, user login
Widget buildChatList(
BuildContext context,
IList<LocalAccount> localAccounts,
TypedKey activeUserLogin,
proto.Account account,
// ignore: prefer_expression_function_bodies
) {
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final contactList = ref.watch(fetchContactListProvider).asData?.value ??
const IListConst([]);
final chatList =
@ -47,45 +42,4 @@ class ChatsPageState extends State<ChatsPage> {
.expanded(),
if (chatList.isEmpty) const EmptyChatListWidget().expanded(),
]);
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final localAccountsV = ref.watch(localAccountsProvider);
final loginsV = ref.watch(loginsProvider);
if (!localAccountsV.hasValue || !loginsV.hasValue) {
return waitingPage(context);
}
final localAccounts = localAccountsV.requireValue;
final logins = loginsV.requireValue;
final activeUserLogin = logins.activeUserLogin;
if (activeUserLogin == null) {
// If no logged in user is active show a placeholder
return waitingPage(context);
}
final accountV = ref
.watch(fetchAccountProvider(accountMasterRecordKey: activeUserLogin));
if (!accountV.hasValue) {
return waitingPage(context);
}
final account = accountV.requireValue;
switch (account.status) {
case AccountInfoStatus.noAccount:
return waitingPage(context);
case AccountInfoStatus.accountInvalid:
return waitingPage(context);
case AccountInfoStatus.accountLocked:
return waitingPage(context);
case AccountInfoStatus.accountReady:
return buildChatList(
context,
localAccounts,
activeUserLogin,
account.account!,
);
}
}
}

View File

@ -1,118 +0,0 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../proto/proto.dart' as proto;
import '../../../packages/veilid_support/veilid_support.dart';
import 'account.dart';
part 'chat.g.dart';
/// Create a new chat (singleton for single contact chats)
Future<void> getOrCreateChatSingleContact({
required ActiveAccountInfo activeAccountInfo,
required TypedKey remoteConversationRecordKey,
}) async {
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
// Create conversation type Chat
final chat = proto.Chat()
..type = proto.ChatType.SINGLE_CONTACT
..remoteConversationKey = remoteConversationRecordKey.toProto();
// Add Chat to account's list
// if this fails, don't keep retrying, user can try again later
await (await DHTShortArray.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(
activeAccountInfo.account.chatList),
parent: accountRecordKey))
.scope((chatList) async {
for (var i = 0; i < chatList.length; i++) {
final cbuf = await chatList.getItem(i);
if (cbuf == null) {
throw Exception('Failed to get chat');
}
final c = proto.Chat.fromBuffer(cbuf);
if (c == chat) {
return;
}
}
if (await chatList.tryAddItem(chat.writeToBuffer()) == false) {
throw Exception('Failed to add chat');
}
});
}
/// Delete a chat
Future<void> deleteChat(
{required ActiveAccountInfo activeAccountInfo,
required TypedKey remoteConversationRecordKey}) async {
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
// Create conversation type Chat
final remoteConversationKey = remoteConversationRecordKey.toProto();
// Add Chat to account's list
// if this fails, don't keep retrying, user can try again later
await (await DHTShortArray.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(
activeAccountInfo.account.chatList),
parent: accountRecordKey))
.scope((chatList) async {
for (var i = 0; i < chatList.length; i++) {
final cbuf = await chatList.getItem(i);
if (cbuf == null) {
throw Exception('Failed to get chat');
}
final c = proto.Chat.fromBuffer(cbuf);
if (c.remoteConversationKey == remoteConversationKey) {
await chatList.tryRemoveItem(i);
if (activeChatState.state == remoteConversationRecordKey) {
activeChatState.state = null;
}
return;
}
}
});
}
/// Get the active account contact list
@riverpod
Future<IList<proto.Chat>?> fetchChatList(FetchChatListRef ref) async {
// See if we've logged into this account or if it is locked
final activeAccountInfo = await ref.watch(fetchActiveAccountProvider.future);
if (activeAccountInfo == null) {
return null;
}
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
// Decode the chat list from the DHT
IList<proto.Chat> out = const IListConst([]);
await (await DHTShortArray.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(
activeAccountInfo.account.chatList),
parent: accountRecordKey))
.scope((cList) async {
for (var i = 0; i < cList.length; i++) {
final cir = await cList.getItem(i);
if (cir == null) {
throw Exception('Failed to get chat');
}
out = out.add(proto.Chat.fromBuffer(cir));
}
});
return out;
}
// The selected chat
final activeChatState = StateController<TypedKey?>(null);
final activeChatStateProvider =
StateNotifierProvider<StateController<TypedKey?>, TypedKey?>(
(ref) => activeChatState);