more refactor, conversation work

This commit is contained in:
Christien Rioux 2024-01-28 21:31:53 -05:00
parent 20047a956f
commit 7bd426ce98
15 changed files with 204 additions and 390 deletions

View File

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

View File

@ -1,11 +1,11 @@
import 'dart:async';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../contacts/contacts.dart';
import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart';
import '../models/models.dart';
@ -33,19 +33,19 @@ class InvitationStatus {
//////////////////////////////////////////////////
// Mutable state for per-account contact invitations
class ContactInvitationListCubit extends DHTShortArrayCubit<proto.ContactInvitation> {
class ContactInvitationListCubit
extends DHTShortArrayCubit<proto.ContactInvitationRecord> {
ContactInvitationListCubit({
required ActiveAccountInfo activeAccountInfo,
required proto.Account account,
required DHTShortArray dhtRecord,
}) : _activeAccountInfo = activeAccountInfo,
_account = account,
_dhtRecord = dhtRecord,
super(shortArray: dhtRecord, decodeElement: proto.ContactInvitation.fromBuffer);
xxx convert the rest of this to cubit
static Future<ContactInvitationRepository> open(
ActiveAccountInfo activeAccountInfo, proto.Account account) async {
super(
open: () => _open(activeAccountInfo, account),
decodeElement: proto.ContactInvitationRecord.fromBuffer);
static Future<DHTShortArray> _open(
ActiveAccountInfo activeAccountInfo, proto.Account account) async {
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
@ -57,28 +57,9 @@ xxx convert the rest of this to cubit
contactInvitationListRecordKey,
parent: accountRecordKey);
return ContactInvitationRepository._(
activeAccountInfo: activeAccountInfo,
account: account,
dhtRecord: dhtRecord);
return dhtRecord;
}
@override
Future<void> close() async {
await _dhtRecord.close();
await super.close();
}
// Future<void> refresh() async {
// for (var i = 0; i < _dhtRecord.length; i++) {
// final cir = await _dhtRecord.getItem(i);
// if (cir == null) {
// throw Exception('Failed to get contact invitation record');
// }
// _records = _records.add(proto.ContactInvitationRecord.fromBuffer(cir));
// }
// }
Future<Uint8List> createInvitation(
{required EncryptionKeyType encryptionKeyType,
required String encryptionKey,
@ -98,7 +79,8 @@ xxx convert the rest of this to cubit
encryptionKey: encryptionKey,
);
// Create local chat DHT record with the account record key as its parent
// Create local conversation DHT record with the account record key as its
// parent.
// Do not set the encryption of this key yet as it will not yet be written
// to and it will be eventually encrypted with the DH of the contact's
// identity key
@ -163,7 +145,7 @@ xxx convert the rest of this to cubit
// Add ContactInvitationRecord to account's list
// if this fails, don't keep retrying, user can try again later
if (await _dhtRecord.tryAddItem(cinvrec.writeToBuffer()) == false) {
if (await shortArray.tryAddItem(cinvrec.writeToBuffer()) == false) {
throw Exception('Failed to add contact invitation record');
}
});
@ -180,15 +162,15 @@ xxx convert the rest of this to cubit
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
// Remove ContactInvitationRecord from account's list
for (var i = 0; i < _dhtRecord.length; i++) {
final item = await _dhtRecord.getItemProtobuf(
for (var i = 0; i < shortArray.length; i++) {
final item = await shortArray.getItemProtobuf(
proto.ContactInvitationRecord.fromBuffer, i);
if (item == null) {
throw Exception('Failed to get contact invitation record');
}
if (item.contactRequestInbox.recordKey ==
contactInvitationRecord.contactRequestInbox.recordKey) {
await _dhtRecord.tryRemoveItem(i);
await shortArray.tryRemoveItem(i);
break;
}
}
@ -229,11 +211,11 @@ xxx convert the rest of this to cubit
final cs = await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind);
// See if we're chatting to ourselves, if so, don't delete it here
final isSelf = _contactIdentityMaster.identityPublicKey ==
_activeAccountInfo.localAccount.identityMaster.identityPublicKey;
xxx this doesn't work and the upper one doesnt either
final isSelf = _records.indexWhere((cir) =>
// Compare the invitation's contact request
// inbox with our list of extant invitations
// If we're chatting to ourselves,
// we are validating an invitation we have created
final isSelf = state.data!.value.indexWhere((cir) =>
proto.TypedKeyProto.fromProto(cir.contactRequestInbox.recordKey) ==
contactRequestInboxKey) !=
-1;
@ -283,10 +265,7 @@ xxx this doesn't work and the upper one doesnt either
out = ValidContactInvitation(
activeAccountInfo: _activeAccountInfo,
signedContactInvitation: signedContactInvitation,
contactInvitation: contactInvitation,
contactRequestInboxKey: contactRequestInboxKey,
contactRequest: contactRequest,
contactRequestPrivate: contactRequestPrivate,
contactIdentityMaster: contactIdentityMaster,
writer: writer);
@ -296,13 +275,12 @@ xxx this doesn't work and the upper one doesnt either
}
Future<InvitationStatus?> checkInvitationStatus(
{required ActiveAccountInfo activeAccountInfo,
required proto.ContactInvitationRecord contactInvitationRecord}) async {
{required proto.ContactInvitationRecord contactInvitationRecord}) async {
// Open the contact request inbox
try {
final pool = DHTRecordPool.instance;
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final accountRecordKey = _activeAccountInfo
.userLogin.accountRecordInfo.accountRecord.recordKey;
final writerKey =
proto.CryptoKeyProto.fromProto(contactInvitationRecord.writerKey);
final writerSecret =
@ -350,11 +328,14 @@ xxx this doesn't work and the upper one doesnt either
// Pull profile from remote conversation key
final remoteConversationRecordKey = proto.TypedKeyProto.fromProto(
contactResponse.remoteConversationRecordKey);
final remoteConversation = await readRemoteConversation(
activeAccountInfo: activeAccountInfo,
final conversation = ConversationManager(
activeAccountInfo: _activeAccountInfo,
remoteIdentityPublicKey:
contactIdentityMaster.identityPublicTypedKey(),
remoteConversationRecordKey: remoteConversationRecordKey);
final remoteConversation = await conversation.readRemoteConversation();
if (remoteConversation == null) {
log.info('Remote conversation could not be read. Waiting...');
return null;
@ -362,11 +343,9 @@ xxx this doesn't work and the upper one doesnt either
// Complete the local conversation now that we have the remote profile
final localConversationRecordKey = proto.TypedKeyProto.fromProto(
contactInvitationRecord.localConversationRecordKey);
return createConversation(
activeAccountInfo: activeAccountInfo,
remoteIdentityPublicKey:
contactIdentityMaster.identityPublicTypedKey(),
return conversation.initLocalConversation(
existingConversationRecordKey: localConversationRecordKey,
profile: xxx LOCAL PROFILE HERE NOT REMOTE
// ignore: prefer_expression_function_bodies
callback: (localConversation) async {
return InvitationStatus(
@ -400,9 +379,6 @@ xxx this doesn't work and the upper one doesnt either
}
//
final ActiveAccountInfo _activeAccountInfo;
final proto.Account _account;
final DHTShortArray _dhtRecord;
//IList<proto.ContactInvitationRecord> _records;
}

View File

@ -1,2 +1,2 @@
export 'accepted_contact.dart';
export '../repository/valid_contact_invitation.dart';
export 'valid_contact_invitation.dart';

View File

@ -4,8 +4,8 @@ import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart';
import '../models/models.dart';
import 'contact_invitation_repository.dart';
import 'models.dart';
import '../cubits/contact_invitation_list_cubit.dart';
//////////////////////////////////////////////////
///
@ -14,18 +14,12 @@ class ValidContactInvitation {
@internal
ValidContactInvitation(
{required ActiveAccountInfo activeAccountInfo,
required proto.SignedContactInvitation signedContactInvitation,
required proto.ContactInvitation contactInvitation,
required TypedKey contactRequestInboxKey,
required proto.ContactRequest contactRequest,
required proto.ContactRequestPrivate contactRequestPrivate,
required IdentityMaster contactIdentityMaster,
required KeyPair writer})
: _activeAccountInfo = activeAccountInfo,
_signedContactInvitation = signedContactInvitation,
_contactInvitation = contactInvitation,
_contactRequestInboxKey = contactRequestInboxKey,
_contactRequest = contactRequest,
_contactRequestPrivate = contactRequestPrivate,
_contactIdentityMaster = contactIdentityMaster,
_writer = writer;
@ -140,8 +134,5 @@ class ValidContactInvitation {
final TypedKey _contactRequestInboxKey;
final IdentityMaster _contactIdentityMaster;
final KeyPair _writer;
proto.SignedContactInvitation _signedContactInvitation;
proto.ContactInvitation _contactInvitation;
proto.ContactRequest _contactRequest;
proto.ContactRequestPrivate _contactRequestPrivate;
final proto.ContactRequestPrivate _contactRequestPrivate;
}

View File

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

View File

@ -2,6 +2,7 @@
// Each Contact in the ContactList has at most one Conversation between the
// remote contact and the local account
import 'dart:async';
import 'dart:convert';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
@ -11,32 +12,86 @@ import '../../account_manager/account_manager.dart';
import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart';
import 'chat.dart';
class Conversation {
Conversation._(
class ConversationManager {
ConversationManager(
{required ActiveAccountInfo activeAccountInfo,
required TypedKey localConversationRecordKey,
required TypedKey remoteIdentityPublicKey,
required TypedKey remoteConversationRecordKey})
TypedKey? localConversationRecordKey,
TypedKey? remoteConversationRecordKey})
: _activeAccountInfo = activeAccountInfo,
_localConversationRecordKey = localConversationRecordKey,
_remoteIdentityPublicKey = remoteIdentityPublicKey,
_remoteConversationRecordKey = remoteConversationRecordKey;
Future<Conversation> open() async {}
// Initialize a local conversation
// If we were the initiator of the conversation there may be an
// incomplete 'existingConversationRecord' that we need to fill
// in now that we have the remote identity key
Future<T> initLocalConversation<T>(
{required proto.Profile profile,
required FutureOr<T> Function(DHTRecord) callback,
TypedKey? existingConversationRecordKey}) async {
final pool = DHTRecordPool.instance;
final accountRecordKey =
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
Future<void> close() async {
//
final crypto = await getConversationCrypto();
final writer = _activeAccountInfo.conversationWriter;
// Open with SMPL scheme for identity writer
late final DHTRecord localConversationRecord;
if (existingConversationRecordKey != null) {
localConversationRecord = await pool.openWrite(
existingConversationRecordKey, writer,
parent: accountRecordKey, crypto: crypto);
} else {
final localConversationRecordCreate = await pool.create(
parent: accountRecordKey,
crypto: crypto,
schema: DHTSchema.smpl(
oCnt: 0, members: [DHTSchemaMember(mKey: writer.key, mCnt: 1)]));
await localConversationRecordCreate.close();
localConversationRecord = await pool.openWrite(
localConversationRecordCreate.key, writer,
parent: accountRecordKey, crypto: crypto);
}
final out = localConversationRecord
// ignore: prefer_expression_function_bodies
.deleteScope((localConversation) async {
// Make messages log
return (await DHTShortArray.create(
parent: localConversation.key,
crypto: crypto,
smplWriter: writer))
.deleteScope((messages) async {
// Write local conversation key
final conversation = proto.Conversation()
..profile = profile
..identityMasterJson = jsonEncode(
_activeAccountInfo.localAccount.identityMaster.toJson())
..messages = messages.record.key.toProto();
//
final update = await localConversation.tryWriteProtobuf(
proto.Conversation.fromBuffer, conversation);
if (update != null) {
throw Exception('Failed to write local conversation');
}
return await callback(localConversation);
});
});
// If success, save the new local conversation record key in this object
_localConversationRecordKey = localConversationRecord.key;
return out;
}
Future<proto.Conversation?> readRemoteConversation() async {
final accountRecordKey =
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final pool = await DHTRecordPool.instance();
final pool = DHTRecordPool.instance;
final crypto = await getConversationCrypto();
return (await pool.openRead(_remoteConversationRecordKey,
return (await pool.openRead(_remoteConversationRecordKey!,
parent: accountRecordKey, crypto: crypto))
.scope((remoteConversation) async {
//
@ -49,10 +104,10 @@ class Conversation {
Future<proto.Conversation?> readLocalConversation() async {
final accountRecordKey =
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final pool = await DHTRecordPool.instance();
final pool = DHTRecordPool.instance;
final crypto = await getConversationCrypto();
return (await pool.openRead(_localConversationRecordKey,
return (await pool.openRead(_localConversationRecordKey!,
parent: accountRecordKey, crypto: crypto))
.scope((localConversation) async {
//
@ -70,12 +125,12 @@ class Conversation {
}) async {
final accountRecordKey =
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final pool = await DHTRecordPool.instance();
final pool = DHTRecordPool.instance;
final crypto = await getConversationCrypto();
final writer = _activeAccountInfo.getConversationWriter();
final writer = _activeAccountInfo.conversationWriter;
return (await pool.openWrite(_localConversationRecordKey, writer,
return (await pool.openWrite(_localConversationRecordKey!, writer,
parent: accountRecordKey, crypto: crypto))
.scope((localConversation) async {
//
@ -97,7 +152,7 @@ class Conversation {
final messagesRecordKey =
proto.TypedKeyProto.fromProto(conversation.messages);
final crypto = await getConversationCrypto();
final writer = _activeAccountInfo.getConversationWriter();
final writer = _activeAccountInfo.conversationWriter;
await (await DHTShortArray.openWrite(messagesRecordKey, writer,
parent: _localConversationRecordKey, crypto: crypto))
@ -116,7 +171,7 @@ class Conversation {
final messagesRecordKey =
proto.TypedKeyProto.fromProto(conversation.messages);
final crypto = await getConversationCrypto();
final writer = _activeAccountInfo.getConversationWriter();
final writer = _activeAccountInfo.conversationWriter;
newMessages = newMessages.sort((a, b) => Timestamp.fromInt64(a.timestamp)
.compareTo(Timestamp.fromInt64(b.timestamp)));
@ -195,9 +250,8 @@ class Conversation {
if (conversationCrypto != null) {
return conversationCrypto;
}
final veilid = await eventualVeilid.future;
final identitySecret = _activeAccountInfo.userLogin.identitySecret;
final cs = await veilid.getCryptoSystem(identitySecret.kind);
final cs = await Veilid.instance.getCryptoSystem(identitySecret.kind);
final sharedSecret =
await cs.cachedDH(_remoteIdentityPublicKey.value, identitySecret.value);
@ -232,119 +286,60 @@ class Conversation {
}
final ActiveAccountInfo _activeAccountInfo;
final TypedKey _localConversationRecordKey;
final TypedKey _remoteIdentityPublicKey;
final TypedKey _remoteConversationRecordKey;
TypedKey? _localConversationRecordKey;
TypedKey? _remoteConversationRecordKey;
//
DHTRecordCrypto? _conversationCrypto;
}
// Create a conversation
// If we were the initiator of the conversation there may be an
// incomplete 'existingConversationRecord' that we need to fill
// in now that we have the remote identity key
Future<T> createConversation<T>(
{required ActiveAccountInfo activeAccountInfo,
required TypedKey remoteIdentityPublicKey,
required FutureOr<T> Function(DHTRecord) callback,
TypedKey? existingConversationRecordKey}) async {
final pool = await DHTRecordPool.instance();
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final crypto = await getConversationCrypto(
activeAccountInfo: activeAccountInfo,
remoteIdentityPublicKey: remoteIdentityPublicKey);
final writer = activeAccountInfo.getConversationWriter();
// //
// //
// //
// //
// Open with SMPL scheme for identity writer
late final DHTRecord localConversationRecord;
if (existingConversationRecordKey != null) {
localConversationRecord = await pool.openWrite(
existingConversationRecordKey, writer,
parent: accountRecordKey, crypto: crypto);
} else {
final localConversationRecordCreate = await pool.create(
parent: accountRecordKey,
crypto: crypto,
schema: DHTSchema.smpl(
oCnt: 0, members: [DHTSchemaMember(mKey: writer.key, mCnt: 1)]));
await localConversationRecordCreate.close();
localConversationRecord = await pool.openWrite(
localConversationRecordCreate.key, writer,
parent: accountRecordKey, crypto: crypto);
}
return localConversationRecord
// ignore: prefer_expression_function_bodies
.deleteScope((localConversation) async {
// Make messages log
return (await DHTShortArray.create(
parent: localConversation.key, crypto: crypto, smplWriter: writer))
.deleteScope((messages) async {
// Write local conversation key
final conversation = proto.Conversation()
..profile = activeAccountInfo.account.profile
..identityMasterJson =
jsonEncode(activeAccountInfo.localAccount.identityMaster.toJson())
..messages = messages.record.key.toProto();
// @riverpod
// class ActiveConversationMessages extends _$ActiveConversationMessages {
// /// Get message for active conversation
// @override
// FutureOr<IList<proto.Message>?> build() async {
// await eventualVeilid.future;
//
final update = await localConversation.tryWriteProtobuf(
proto.Conversation.fromBuffer, conversation);
if (update != null) {
throw Exception('Failed to write local conversation');
}
return await callback(localConversation);
});
});
}
// final activeChat = ref.watch(activeChatStateProvider);
// if (activeChat == null) {
// return null;
// }
//
//
//
//
// final activeAccountInfo =
// await ref.watch(fetchActiveAccountProvider.future);
// if (activeAccountInfo == null) {
// return null;
// }
@riverpod
class ActiveConversationMessages extends _$ActiveConversationMessages {
/// Get message for active conversation
@override
FutureOr<IList<proto.Message>?> build() async {
await eventualVeilid.future;
// final contactList = ref.watch(fetchContactListProvider).asData?.value ??
// const IListConst([]);
final activeChat = ref.watch(activeChatStateProvider);
if (activeChat == null) {
return null;
}
// final activeChatContactIdx = contactList.indexWhere(
// (c) =>
// proto.TypedKeyProto.fromProto(c.remoteConversationRecordKey) ==
// activeChat,
// );
// if (activeChatContactIdx == -1) {
// return null;
// }
// final activeChatContact = contactList[activeChatContactIdx];
// final remoteIdentityPublicKey =
// proto.TypedKeyProto.fromProto(activeChatContact.identityPublicKey);
// // final remoteConversationRecordKey = proto.TypedKeyProto.fromProto(
// // activeChatContact.remoteConversationRecordKey);
// final localConversationRecordKey = proto.TypedKeyProto.fromProto(
// activeChatContact.localConversationRecordKey);
final activeAccountInfo =
await ref.watch(fetchActiveAccountProvider.future);
if (activeAccountInfo == null) {
return null;
}
final contactList = ref.watch(fetchContactListProvider).asData?.value ??
const IListConst([]);
final activeChatContactIdx = contactList.indexWhere(
(c) =>
proto.TypedKeyProto.fromProto(c.remoteConversationRecordKey) ==
activeChat,
);
if (activeChatContactIdx == -1) {
return null;
}
final activeChatContact = contactList[activeChatContactIdx];
final remoteIdentityPublicKey =
proto.TypedKeyProto.fromProto(activeChatContact.identityPublicKey);
// final remoteConversationRecordKey = proto.TypedKeyProto.fromProto(
// activeChatContact.remoteConversationRecordKey);
final localConversationRecordKey = proto.TypedKeyProto.fromProto(
activeChatContact.localConversationRecordKey);
return await getLocalConversationMessages(
activeAccountInfo: activeAccountInfo,
localConversationRecordKey: localConversationRecordKey,
remoteIdentityPublicKey: remoteIdentityPublicKey,
);
}
}
// return await getLocalConversationMessages(
// activeAccountInfo: activeAccountInfo,
// localConversationRecordKey: localConversationRecordKey,
// remoteIdentityPublicKey: remoteIdentityPublicKey,
// );
// }
// }

View File

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

View File

@ -61,12 +61,7 @@ class HomePageState extends State<HomePage> with TickerProviderStateMixin {
return BlocProvider(
create: (context) => AccountRecordCubit(
record: accountInfo.activeAccountInfo!.accountRecord),
child: context.watch<AccountRecordCubit>().state.builder(
(context, account) => HomeAccountReady(
localAccounts: localAccounts,
activeUserLogin: activeUserLogin,
activeAccountInfo: accountInfo.activeAccountInfo!,
account: account)));
child: HomeAccountReady());
}
}

View File

@ -1,13 +1,11 @@
import 'dart:async';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:equatable/equatable.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:go_router/go_router.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../../account_manager/account_manager.dart';
import '../../../contact_invitation/contact_invitation.dart';
@ -19,11 +17,13 @@ import 'main_pager/main_pager.dart';
class HomeAccountReady extends StatefulWidget {
const HomeAccountReady(
{required ActiveAccountInfo activeAccountInfo,
required Account account,
required proto.Account account,
super.key})
: _accountReadyContext = accountReadyContext;
: _activeAccountInfo = activeAccountInfo,
_account = account;
final AccountReadyContext _accountReadyContext;
final ActiveAccountInfo _activeAccountInfo;
final proto.Account _account;
@override
HomeAccountReadyState createState() => HomeAccountReadyState();
@ -31,32 +31,10 @@ class HomeAccountReady extends StatefulWidget {
class HomeAccountReadyState extends State<HomeAccountReady>
with TickerProviderStateMixin {
//
ContactInvitationRepository? _contactInvitationRepository;
//
@override
void initState() {
super.initState();
// Async initialize repositories for the active user
// xxx: this should not be necessary
// xxx: but RepositoryProvider doesn't call dispose()
Future.delayed(Duration.zero, () async {
//
final cir = await ContactInvitationRepository.open(
widget.activeAccountInfo, widget._accountReadyContext.account);
setState(() {
_contactInvitationRepository = cir;
});
});
}
@override
void dispose() {
super.dispose();
_contactInvitationRepository?.dispose();
}
Widget buildUnlockAccount(
@ -87,11 +65,11 @@ class HomeAccountReadyState extends State<HomeAccountReady>
context.go('/home/settings');
}).paddingLTRB(0, 0, 8, 0),
ProfileWidget(
name: widget._accountReadyContext.account.profile.name,
pronouns: widget._accountReadyContext.account.profile.pronouns,
name: widget._account.profile.name,
pronouns: widget._account.profile.pronouns,
).expanded(),
]).paddingAll(8),
MainPager().expanded()
const MainPager().expanded()
]);
}
@ -129,18 +107,14 @@ class HomeAccountReadyState extends State<HomeAccountReady>
}
@override
Widget build(BuildContext context) {
if (_contactInvitationRepository == null) {
return waitingPage(context);
}
return RepositoryProvider.value(
value: _contactInvitationRepository,
child: responsiveVisibility(
context: context,
phone: false,
)
? buildTablet(context)
: buildPhone(context));
}
Widget build(BuildContext context) => BlocProvider(
create: (context) => ContactInvitationListCubit(
activeAccountInfo: widget._activeAccountInfo,
account: widget._account),
child: responsiveVisibility(
context: context,
phone: false,
)
? buildTablet(context)
: buildPhone(context));
}

View File

@ -2,11 +2,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 '../../../../account_manager/account_manager.dart';
import '../../../../proto/proto.dart' as proto;
import '../../../../contact_invitation/contact_invitation.dart';
import '../../../../theme/theme.dart';
class AccountPage extends StatefulWidget {
@ -39,19 +39,17 @@ class AccountPageState extends State<AccountPage> {
final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;
final records = widget.account.contactInvitationRecords;
final contactInvitationRecordList =
ref.watch(fetchContactInvitationRecordsProvider).asData?.value ??
context.watch<ContactInvitationListCubit>().state.data?.value ??
const IListConst([]);
final contactList = ref.watch(fetchContactListProvider).asData?.value ??
final contactList = context.watch<ContactListCubit>().state.data?.value ??
const IListConst([]);
return SizedBox(
child: Column(children: <Widget>[
if (contactInvitationRecordList.isNotEmpty)
ExpansionTile(
tilePadding: EdgeInsets.fromLTRB(8, 0, 8, 0),
tilePadding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
backgroundColor: scale.primaryScale.border,
collapsedBackgroundColor: scale.primaryScale.border,
shape: RoundedRectangleBorder(

View File

@ -1,7 +1,5 @@
import 'dart:async';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_animate/flutter_animate.dart';
@ -9,41 +7,22 @@ import 'package:flutter_translate/flutter_translate.dart';
import 'package:preload_page_view/preload_page_view.dart';
import 'package:stylish_bottom_bar/model/bar_items.dart';
import 'package:stylish_bottom_bar/stylish_bottom_bar.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../../../proto/proto.dart' as proto;
import '../../../../contact_invitation/contact_invitation.dart';
import '../../../../theme/theme.dart';
import '../../../../tools/tools.dart';
import '../../../account_manager/account_manager.dart';
import '../../../contact_invitation/contact_invitation.dart';
import '../../../theme/theme.dart';
import 'account_page.dart';
import 'bottom_sheet_action_button.dart';
import 'chats_page.dart';
class MainPager extends StatefulWidget {
const MainPager(
{required this.localAccounts,
required this.activeUserLogin,
required this.account,
super.key});
final IList<LocalAccount> localAccounts;
final TypedKey activeUserLogin;
final proto.Account account;
const MainPager({super.key});
@override
MainPagerState createState() => MainPagerState();
static MainPagerState? of(BuildContext context) =>
context.findAncestorStateOfType<MainPagerState>();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(IterableProperty<LocalAccount>('localAccounts', localAccounts))
..add(DiagnosticsProperty<TypedKey>('activeUserLogin', activeUserLogin))
..add(DiagnosticsProperty<proto.Account>('account', account));
}
}
class MainPagerState extends State<MainPager> with TickerProviderStateMixin {
@ -187,12 +166,9 @@ class MainPagerState extends State<MainPager> with TickerProviderStateMixin {
_currentPage = index;
});
},
children: [
AccountPage(
localAccounts: widget.localAccounts,
activeUserLogin: widget.activeUserLogin,
account: widget.account),
const ChatsPage(),
children: const [
AccountPage(),
ChatsPage(),
])),
// appBar: AppBar(
// toolbarHeight: 24,

View File

@ -1,55 +0,0 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:circular_profile_avatar/circular_profile_avatar.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:window_manager/window_manager.dart';
import '../entities/local_account.dart';
import '../providers/logins.dart';
class AccountBubble extends ConsumerWidget {
const AccountBubble({required this.account, super.key});
final LocalAccount account;
@override
Widget build(BuildContext context, WidgetRef ref) {
windowManager.setTitleBarStyle(TitleBarStyle.normal);
final logins = ref.watch(loginsProvider);
return ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 300),
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Expanded(
flex: 4,
child: CircularProfileAvatar('',
child: Container(color: Theme.of(context).disabledColor))),
const Expanded(child: Text('Placeholder'))
]));
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<LocalAccount>('account', account));
}
}
class AddAccountBubble extends ConsumerWidget {
const AddAccountBubble({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
windowManager.setTitleBarStyle(TitleBarStyle.normal);
final logins = ref.watch(loginsProvider);
return Column(mainAxisAlignment: MainAxisAlignment.center, children: [
CircularProfileAvatar('',
borderWidth: 4,
borderColor: Theme.of(context).unselectedWidgetColor,
child: Container(
color: Colors.blue, child: const Icon(Icons.add, size: 50))),
const Text('Add Account').paddingLTRB(0, 4, 0, 0)
]);
}
}

View File

@ -1,56 +0,0 @@
import 'dart:typed_data';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:fixnum/fixnum.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../entities/local_account.dart';
import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart';
import '../../../packages/veilid_support/veilid_support.dart';
import 'account.dart';
import 'conversation.dart';
part 'contact_invite.g.dart';
/// Get the active account contact invitation list
@riverpod
Future<IList<proto.ContactInvitationRecord>?> fetchContactInvitationRecords(
FetchContactInvitationRecordsRef 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 invitation list from the DHT
IList<proto.ContactInvitationRecord> out = const IListConst([]);
try {
await (await DHTShortArray.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(
activeAccountInfo.account.contactInvitationRecords),
parent: accountRecordKey))
.scope((cirList) async {
for (var i = 0; i < cirList.length; i++) {
final cir = await cirList.getItem(i);
if (cir == null) {
throw Exception('Failed to get contact invitation record');
}
out = out.add(proto.ContactInvitationRecord.fromBuffer(cir));
}
});
} on VeilidAPIExceptionTryAgain catch (_) {
// Try again later
ref.invalidateSelf();
return null;
} on Exception catch (_) {
// Try again later
ref.invalidateSelf();
rethrow;
}
return out;
}

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:meta/meta.dart';
import '../../veilid_support.dart';
@ -41,6 +42,19 @@ class DHTShortArrayCubit<T> extends Cubit<AsyncValue<IList<T>>> {
});
}
Future<void> refresh({bool forceRefresh = false}) async {
var out = IList<T>();
// xxx could be parallelized but we need to watch out for rate limits
for (var i = 0; i < _shortArray.length; i++) {
final cir = await _shortArray.getItem(i, forceRefresh: forceRefresh);
if (cir == null) {
throw Exception('Failed to get short array element');
}
out = out.add(_decodeElement(cir));
}
emit(AsyncValue.data(out));
}
void _update() {
// Run at most one background update process
_wantsUpdate = true;
@ -91,6 +105,9 @@ class DHTShortArrayCubit<T> extends Cubit<AsyncValue<IList<T>>> {
await super.close();
}
@protected
DHTShortArray get shortArray => _shortArray;
late final DHTShortArray _shortArray;
final T Function(List<int> data) _decodeElement;
StreamSubscription<void>? _subscription;