beta warning dialog

This commit is contained in:
Christien Rioux 2024-07-24 15:20:29 -04:00
parent ba191d3903
commit 6080c2f0c6
26 changed files with 445 additions and 339 deletions

View File

@ -8,6 +8,10 @@
"accounts": "Accounts", "accounts": "Accounts",
"version": "Version" "version": "Version"
}, },
"splash": {
"beta_title": "VeilidChat is BETA SOFTWARE",
"beta_text": "DO NOT USE THIS FOR ANYTHING IMPORTANT\n\nUntil 1.0 is released:\n\n• You should have no expectations of actual privacy, or guarantees of security.\n• You will likely lose accounts, contacts, and messages and need to recreate them.\n\nPlease read our BETA PARTICIPATION GUIDE located here:\n\n"
},
"pager": { "pager": {
"chats": "Chats", "chats": "Chats",
"contacts": "Contacts" "contacts": "Contacts"
@ -99,7 +103,8 @@
}, },
"contacts_page": { "contacts_page": {
"contacts": "Contacts", "contacts": "Contacts",
"invitations": "Invitations" "invitations": "Invitations",
"loading_contacts": "Loading contacts..."
}, },
"add_contact_sheet": { "add_contact_sheet": {
"new_contact": "New Contact", "new_contact": "New Contact",

View File

@ -57,16 +57,11 @@ class AuthorInputSource {
// Get another input batch futher back // Get another input batch futher back
final nextWindow = await cubit.loadElementsFromReader( final nextWindow = await cubit.loadElementsFromReader(
reader, last + 1, (last + 1) - first); reader, last + 1, (last + 1) - first);
final asErr = nextWindow.asError; if (nextWindow == null) {
if (asErr != null) {
return AsyncValue.error(asErr.error, asErr.stackTrace);
}
final asLoading = nextWindow.asLoading;
if (asLoading != null) {
return const AsyncValue.loading(); return const AsyncValue.loading();
} }
_currentWindow = InputWindow( _currentWindow =
elements: nextWindow.asData!.value, first: first, last: last); InputWindow(elements: nextWindow, first: first, last: last);
return const AsyncValue.data(true); return const AsyncValue.data(true);
}); });

View File

@ -19,12 +19,19 @@ import '../chat.dart';
const onEndReachedThreshold = 0.75; const onEndReachedThreshold = 0.75;
class ChatComponentWidget extends StatelessWidget { class ChatComponentWidget extends StatelessWidget {
const ChatComponentWidget._({required super.key}); const ChatComponentWidget(
{required super.key, required TypedKey localConversationRecordKey})
: _localConversationRecordKey = localConversationRecordKey;
/////////////////////////////////////////////////////////////////////
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!;
final textTheme = theme.textTheme;
// Builder wrapper function that takes care of state management requirements
static Widget builder(
{required TypedKey localConversationRecordKey, Key? key}) =>
Builder(builder: (context) {
// Get the account info // Get the account info
final accountInfo = context.watch<AccountInfoCubit>().state; final accountInfo = context.watch<AccountInfoCubit>().state;
@ -37,17 +44,16 @@ class ChatComponentWidget extends StatelessWidget {
// Get the active conversation cubit // Get the active conversation cubit
final activeConversationCubit = context final activeConversationCubit = context
.select<ActiveConversationsBlocMapCubit, ActiveConversationCubit?>( .select<ActiveConversationsBlocMapCubit, ActiveConversationCubit?>(
(x) => x.tryOperateSync(localConversationRecordKey, (x) => x.tryOperateSync(_localConversationRecordKey,
closure: (cubit) => cubit)); closure: (cubit) => cubit));
if (activeConversationCubit == null) { if (activeConversationCubit == null) {
return waitingPage(); return waitingPage();
} }
// Get the messages cubit // Get the messages cubit
final messagesCubit = context.select< final messagesCubit = context.select<ActiveSingleContactChatBlocMapCubit,
ActiveSingleContactChatBlocMapCubit,
SingleContactMessagesCubit?>( SingleContactMessagesCubit?>(
(x) => x.tryOperateSync(localConversationRecordKey, (x) => x.tryOperateSync(_localConversationRecordKey,
closure: (cubit) => cubit)); closure: (cubit) => cubit));
if (messagesCubit == null) { if (messagesCubit == null) {
return waitingPage(); return waitingPage();
@ -63,95 +69,12 @@ class ChatComponentWidget extends StatelessWidget {
activeConversationCubit: activeConversationCubit, activeConversationCubit: activeConversationCubit,
messagesCubit: messagesCubit, messagesCubit: messagesCubit,
), ),
child: ChatComponentWidget._(key: key)); child: Builder(builder: _buildChatComponent));
}); }
///////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////
void _handleSendPressed( Widget _buildChatComponent(BuildContext context) {
ChatComponentCubit chatComponentCubit, types.PartialText message) {
final text = message.text;
if (text.startsWith('/')) {
chatComponentCubit.runCommand(text);
return;
}
chatComponentCubit.sendMessage(message);
}
// void _handleAttachmentPressed() async {
// //
// }
Future<void> _handlePageForward(
ChatComponentCubit chatComponentCubit,
WindowState<types.Message> messageWindow,
ScrollNotification notification) async {
print(
'_handlePageForward: messagesState.length=${messageWindow.length} messagesState.windowTail=${messageWindow.windowTail} messagesState.windowCount=${messageWindow.windowCount} ScrollNotification=$notification');
// Go forward a page
final tail = min(messageWindow.length,
messageWindow.windowTail + (messageWindow.windowCount ~/ 4)) %
messageWindow.length;
// Set follow
final follow = messageWindow.length == 0 ||
tail == 0; // xxx incorporate scroll position
// final scrollOffset = (notification.metrics.maxScrollExtent -
// notification.metrics.minScrollExtent) *
// (1.0 - onEndReachedThreshold);
// chatComponentCubit.scrollOffset = scrollOffset;
await chatComponentCubit.setWindow(
tail: tail, count: messageWindow.windowCount, follow: follow);
// chatComponentCubit.state.scrollController.position.jumpTo(
// chatComponentCubit.state.scrollController.offset + scrollOffset);
//chatComponentCubit.scrollOffset = 0;
}
Future<void> _handlePageBackward(
ChatComponentCubit chatComponentCubit,
WindowState<types.Message> messageWindow,
ScrollNotification notification,
) async {
print(
'_handlePageBackward: messagesState.length=${messageWindow.length} messagesState.windowTail=${messageWindow.windowTail} messagesState.windowCount=${messageWindow.windowCount} ScrollNotification=$notification');
// Go back a page
final tail = max(
messageWindow.windowCount,
(messageWindow.windowTail - (messageWindow.windowCount ~/ 4)) %
messageWindow.length);
// Set follow
final follow = messageWindow.length == 0 ||
tail == 0; // xxx incorporate scroll position
// final scrollOffset = -(notification.metrics.maxScrollExtent -
// notification.metrics.minScrollExtent) *
// (1.0 - onEndReachedThreshold);
// chatComponentCubit.scrollOffset = scrollOffset;
await chatComponentCubit.setWindow(
tail: tail, count: messageWindow.windowCount, follow: follow);
// chatComponentCubit.scrollOffset = scrollOffset;
// chatComponentCubit.state.scrollController.position.jumpTo(
// chatComponentCubit.state.scrollController.offset + scrollOffset);
//chatComponentCubit.scrollOffset = 0;
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!; final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!; final scaleConfig = theme.extension<ScaleConfig>()!;
@ -323,4 +246,89 @@ class ChatComponentWidget extends StatelessWidget {
], ],
); );
} }
void _handleSendPressed(
ChatComponentCubit chatComponentCubit, types.PartialText message) {
final text = message.text;
if (text.startsWith('/')) {
chatComponentCubit.runCommand(text);
return;
}
chatComponentCubit.sendMessage(message);
}
// void _handleAttachmentPressed() async {
// //
// }
Future<void> _handlePageForward(
ChatComponentCubit chatComponentCubit,
WindowState<types.Message> messageWindow,
ScrollNotification notification) async {
print(
'_handlePageForward: messagesState.length=${messageWindow.length} messagesState.windowTail=${messageWindow.windowTail} messagesState.windowCount=${messageWindow.windowCount} ScrollNotification=$notification');
// Go forward a page
final tail = min(messageWindow.length,
messageWindow.windowTail + (messageWindow.windowCount ~/ 4)) %
messageWindow.length;
// Set follow
final follow = messageWindow.length == 0 ||
tail == 0; // xxx incorporate scroll position
// final scrollOffset = (notification.metrics.maxScrollExtent -
// notification.metrics.minScrollExtent) *
// (1.0 - onEndReachedThreshold);
// chatComponentCubit.scrollOffset = scrollOffset;
await chatComponentCubit.setWindow(
tail: tail, count: messageWindow.windowCount, follow: follow);
// chatComponentCubit.state.scrollController.position.jumpTo(
// chatComponentCubit.state.scrollController.offset + scrollOffset);
//chatComponentCubit.scrollOffset = 0;
}
Future<void> _handlePageBackward(
ChatComponentCubit chatComponentCubit,
WindowState<types.Message> messageWindow,
ScrollNotification notification,
) async {
print(
'_handlePageBackward: messagesState.length=${messageWindow.length} messagesState.windowTail=${messageWindow.windowTail} messagesState.windowCount=${messageWindow.windowCount} ScrollNotification=$notification');
// Go back a page
final tail = max(
messageWindow.windowCount,
(messageWindow.windowTail - (messageWindow.windowCount ~/ 4)) %
messageWindow.length);
// Set follow
final follow = messageWindow.length == 0 ||
tail == 0; // xxx incorporate scroll position
// final scrollOffset = -(notification.metrics.maxScrollExtent -
// notification.metrics.minScrollExtent) *
// (1.0 - onEndReachedThreshold);
// chatComponentCubit.scrollOffset = scrollOffset;
await chatComponentCubit.setWindow(
tail: tail, count: messageWindow.windowCount, follow: follow);
// chatComponentCubit.scrollOffset = scrollOffset;
// chatComponentCubit.state.scrollController.position.jumpTo(
// chatComponentCubit.state.scrollController.offset + scrollOffset);
//chatComponentCubit.scrollOffset = 0;
}
////////////////////////////////////////////////////////////////////////////
final TypedKey _localConversationRecordKey;
} }

View File

@ -13,7 +13,7 @@ import 'empty_contact_list_widget.dart';
class ContactListWidget extends StatefulWidget { class ContactListWidget extends StatefulWidget {
const ContactListWidget( const ContactListWidget(
{required this.contactList, required this.disabled, super.key}); {required this.contactList, required this.disabled, super.key});
final IList<proto.Contact> contactList; final IList<proto.Contact>? contactList;
final bool disabled; final bool disabled;
@override @override
@ -46,13 +46,18 @@ class _ContactListWidgetState extends State<ContactListWidget>
title: translate('contacts_page.contacts'), title: translate('contacts_page.contacts'),
sliver: SliverFillRemaining( sliver: SliverFillRemaining(
child: SearchableList<proto.Contact>.sliver( child: SearchableList<proto.Contact>.sliver(
initialList: widget.contactList.toList(), initialList: widget.contactList == null
? []
: widget.contactList!.toList(),
itemBuilder: (c) => itemBuilder: (c) =>
ContactItemWidget(contact: c, disabled: widget.disabled) ContactItemWidget(contact: c, disabled: widget.disabled)
.paddingLTRB(0, 4, 0, 0), .paddingLTRB(0, 4, 0, 0),
filter: (value) { filter: (value) {
final lowerValue = value.toLowerCase(); final lowerValue = value.toLowerCase();
return widget.contactList if (widget.contactList == null) {
return [];
}
return widget.contactList!
.where((element) => .where((element) =>
element.nickname.toLowerCase().contains(lowerValue) || element.nickname.toLowerCase().contains(lowerValue) ||
element.profile.name element.profile.name
@ -65,9 +70,13 @@ class _ContactListWidgetState extends State<ContactListWidget>
}, },
searchFieldHeight: 40, searchFieldHeight: 40,
spaceBetweenSearchAndList: 4, spaceBetweenSearchAndList: 4,
emptyWidget: const EmptyContactListWidget(), emptyWidget: widget.contactList == null
? waitingPage(
text: translate('contacts_page.loading_contacts'))
: const EmptyContactListWidget(),
defaultSuffixIconColor: scale.primaryScale.border, defaultSuffixIconColor: scale.primaryScale.border,
closeKeyboardWhenScrolling: true, closeKeyboardWhenScrolling: true,
searchFieldEnabled: widget.contactList != null,
inputDecoration: InputDecoration( inputDecoration: InputDecoration(
labelText: translate('contact_list.search'), labelText: translate('contact_list.search'),
), ),

View File

@ -37,8 +37,8 @@ class _SingleContactChatState extends Equatable {
]; ];
} }
// Map of localConversationRecordKey to MessagesCubit // Map of localConversationRecordKey to SingleContactMessagesCubit
// Wraps a MessagesCubit to stream the latest messages to the state // Wraps a SingleContactMessagesCubit to stream the latest messages to the state
// Automatically follows the state of a ActiveConversationsBlocMapCubit. // Automatically follows the state of a ActiveConversationsBlocMapCubit.
class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit<TypedKey, class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit<TypedKey,
SingleContactMessagesState, SingleContactMessagesCubit> SingleContactMessagesState, SingleContactMessagesCubit>

View File

@ -86,7 +86,7 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
if (activeChatLocalConversationKey == null) { if (activeChatLocalConversationKey == null) {
return const NoConversationWidget(); return const NoConversationWidget();
} }
return ChatComponentWidget.builder( return ChatComponentWidget(
localConversationRecordKey: activeChatLocalConversationKey, localConversationRecordKey: activeChatLocalConversationKey,
key: ValueKey(activeChatLocalConversationKey)); key: ValueKey(activeChatLocalConversationKey));
} }
@ -104,11 +104,6 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
final activeChat = context.watch<ActiveChatCubit>().state; final activeChat = context.watch<ActiveChatCubit>().state;
final hasActiveChat = activeChat != null; final hasActiveChat = activeChat != null;
// if (hasActiveChat) {
// _chatAnimationController.forward();
// } else {
// _chatAnimationController.reset();
// }
return LayoutBuilder(builder: (context, constraints) { return LayoutBuilder(builder: (context, constraints) {
const leftColumnSize = 300.0; const leftColumnSize = 300.0;

View File

@ -1,9 +1,14 @@
import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart'; import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:quickalert/quickalert.dart';
import 'package:transitioned_indexed_stack/transitioned_indexed_stack.dart'; import 'package:transitioned_indexed_stack/transitioned_indexed_stack.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart'; import '../../account_manager/account_manager.dart';
@ -36,6 +41,8 @@ class HomeScreenState extends State<HomeScreen>
.indexWhere((x) => x.superIdentity.recordKey == activeLocalAccount); .indexWhere((x) => x.superIdentity.recordKey == activeLocalAccount);
final canClose = activeIndex != -1; final canClose = activeIndex != -1;
unawaited(_doBetaDialog(context));
if (!canClose) { if (!canClose) {
await _zoomDrawerController.open!(); await _zoomDrawerController.open!();
} }
@ -43,6 +50,36 @@ class HomeScreenState extends State<HomeScreen>
super.initState(); super.initState();
} }
Future<void> _doBetaDialog(BuildContext context) async {
await QuickAlert.show(
context: context,
title: translate('splash.beta_title'),
widget: RichText(
textAlign: TextAlign.center,
text: TextSpan(
children: <TextSpan>[
TextSpan(
text: translate('splash.beta_text'),
style: const TextStyle(
color: Colors.black87,
),
),
TextSpan(
text: 'https://veilid.com/chat/beta',
style: const TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()
..onTap =
() => launchUrlString('https://veilid.com/chat/beta'),
),
],
),
),
type: QuickAlertType.warning);
}
Widget _buildAccountPage( Widget _buildAccountPage(
BuildContext context, BuildContext context,
TypedKey superIdentityRecordKey, TypedKey superIdentityRecordKey,

View File

@ -43,8 +43,7 @@ class ContactsPageState extends State<ContactsPage> {
final ciState = context.watch<ContactListCubit>().state; final ciState = context.watch<ContactListCubit>().state;
final ciBusy = ciState.busy; final ciBusy = ciState.busy;
final contactList = final contactList =
ciState.state.asData?.value.map((x) => x.value).toIList() ?? ciState.state.asData?.value.map((x) => x.value).toIList();
const IListConst([]);
return CustomScrollView(slivers: [ return CustomScrollView(slivers: [
if (contactInvitationRecordList.isNotEmpty) if (contactInvitationRecordList.isNotEmpty)
@ -53,7 +52,7 @@ class ContactsPageState extends State<ContactsPage> {
sliver: ContactInvitationListWidget( sliver: ContactInvitationListWidget(
contactInvitationRecordList: contactInvitationRecordList, contactInvitationRecordList: contactInvitationRecordList,
disabled: cilBusy)), disabled: cilBusy)),
ContactListWidget(contactList: contactList, disabled: ciBusy), ContactListWidget(contactList: contactList, disabled: ciBusy)
]).paddingLTRB(8, 0, 8, 8); ]).paddingLTRB(8, 0, 8, 8);
} }
} }

View File

@ -1,5 +1,9 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:quickalert/quickalert.dart';
import 'package:radix_colors/radix_colors.dart'; import 'package:radix_colors/radix_colors.dart';
import '../tools/tools.dart'; import '../tools/tools.dart';

View File

@ -37,10 +37,12 @@ extension ModalProgressExt on Widget {
Widget buildProgressIndicator() => Builder(builder: (context) { Widget buildProgressIndicator() => Builder(builder: (context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!; final scale = theme.extension<ScaleScheme>()!;
return SpinKitFoldingCube( return FittedBox(
fit: BoxFit.scaleDown,
child: SpinKitFoldingCube(
color: scale.tertiaryScale.primary, color: scale.tertiaryScale.primary,
size: 80, size: 80,
); ));
}); });
Widget waitingPage({String? text}) => Builder(builder: (context) { Widget waitingPage({String? text}) => Builder(builder: (context) {
@ -48,11 +50,17 @@ Widget waitingPage({String? text}) => Builder(builder: (context) {
final scale = theme.extension<ScaleScheme>()!; final scale = theme.extension<ScaleScheme>()!;
return ColoredBox( return ColoredBox(
color: scale.tertiaryScale.appBackground, color: scale.tertiaryScale.appBackground,
child: Center( child: Column(
child: Column(children: [ crossAxisAlignment: CrossAxisAlignment.stretch,
buildProgressIndicator().expanded(), mainAxisAlignment: MainAxisAlignment.center,
if (text != null) Text(text) children: [
]))); buildProgressIndicator(),
if (text != null)
Text(text,
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall!
.copyWith(color: scale.tertiaryScale.appText))
]));
}); });
Widget debugPage(String text) => Builder( Widget debugPage(String text) => Builder(

View File

@ -37,7 +37,7 @@ typedef DHTLogState<T> = AsyncValue<DHTLogStateData<T>>;
typedef DHTLogBusyState<T> = BlocBusyState<DHTLogState<T>>; typedef DHTLogBusyState<T> = BlocBusyState<DHTLogState<T>>;
class DHTLogCubit<T> extends Cubit<DHTLogBusyState<T>> class DHTLogCubit<T> extends Cubit<DHTLogBusyState<T>>
with BlocBusyWrapper<DHTLogState<T>> { with BlocBusyWrapper<DHTLogState<T>>, RefreshableCubit {
DHTLogCubit({ DHTLogCubit({
required Future<DHTLog> Function() open, required Future<DHTLog> Function() open,
required T Function(List<int> data) decodeElement, required T Function(List<int> data) decodeElement,
@ -52,7 +52,7 @@ class DHTLogCubit<T> extends Cubit<DHTLogBusyState<T>>
_log = await open(); _log = await open();
_wantsCloseRecord = true; _wantsCloseRecord = true;
break; break;
} on VeilidAPIExceptionTryAgain { } on DHTExceptionNotAvailable {
// Wait for a bit // Wait for a bit
await asyncSleep(); await asyncSleep();
} }
@ -91,6 +91,7 @@ class DHTLogCubit<T> extends Cubit<DHTLogBusyState<T>>
await _refreshNoWait(forceRefresh: forceRefresh); await _refreshNoWait(forceRefresh: forceRefresh);
} }
@override
Future<void> refresh({bool forceRefresh = false}) async { Future<void> refresh({bool forceRefresh = false}) async {
await _initWait(); await _initWait();
await _refreshNoWait(forceRefresh: forceRefresh); await _refreshNoWait(forceRefresh: forceRefresh);
@ -101,40 +102,31 @@ class DHTLogCubit<T> extends Cubit<DHTLogBusyState<T>>
Future<void> _refreshInner(void Function(AsyncValue<DHTLogStateData<T>>) emit, Future<void> _refreshInner(void Function(AsyncValue<DHTLogStateData<T>>) emit,
{bool forceRefresh = false}) async { {bool forceRefresh = false}) async {
late final AsyncValue<IList<OnlineElementState<T>>> avElements;
late final int length; late final int length;
await _log.operate((reader) async { final window = await _log.operate((reader) async {
length = reader.length; length = reader.length;
avElements = return loadElementsFromReader(reader, _windowTail, _windowSize);
await loadElementsFromReader(reader, _windowTail, _windowSize);
}); });
final err = avElements.asError; if (window == null) {
if (err != null) { setWantsRefresh();
emit(AsyncValue.error(err.error, err.stackTrace));
return; return;
} }
final loading = avElements.asLoading;
if (loading != null) {
emit(const AsyncValue.loading());
return;
}
final window = avElements.asData!.value;
emit(AsyncValue.data(DHTLogStateData( emit(AsyncValue.data(DHTLogStateData(
length: length, length: length,
window: window, window: window,
windowTail: _windowTail, windowTail: _windowTail,
windowSize: _windowSize, windowSize: _windowSize,
follow: _follow))); follow: _follow)));
setRefreshed();
} }
// Tail is one past the last element to load // Tail is one past the last element to load
Future<AsyncValue<IList<OnlineElementState<T>>>> loadElementsFromReader( Future<IList<OnlineElementState<T>>?> loadElementsFromReader(
DHTLogReadOperations reader, int tail, int count, DHTLogReadOperations reader, int tail, int count,
{bool forceRefresh = false}) async { {bool forceRefresh = false}) async {
try {
final length = reader.length; final length = reader.length;
if (length == 0) { if (length == 0) {
return const AsyncValue.data(IList.empty()); return const IList.empty();
} }
final end = ((tail - 1) % length) + 1; final end = ((tail - 1) % length) + 1;
final start = (count < end) ? end - count : 0; final start = (count < end) ? end - count : 0;
@ -143,9 +135,6 @@ class DHTLogCubit<T> extends Cubit<DHTLogBusyState<T>>
Set<int>? offlinePositions; Set<int>? offlinePositions;
if (_log.writer != null) { if (_log.writer != null) {
offlinePositions = await reader.getOfflinePositions(); offlinePositions = await reader.getOfflinePositions();
if (offlinePositions == null) {
return const AsyncValue.loading();
}
} }
// Get the items // Get the items
@ -156,13 +145,8 @@ class DHTLogCubit<T> extends Cubit<DHTLogBusyState<T>>
value: _decodeElement(x.$2), value: _decodeElement(x.$2),
isOffline: offlinePositions?.contains(x.$1) ?? false)) isOffline: offlinePositions?.contains(x.$1) ?? false))
.toIList(); .toIList();
if (allItems == null) {
return const AsyncValue.loading(); return allItems;
}
return AsyncValue.data(allItems);
} on Exception catch (e, st) {
return AsyncValue.error(e, st);
}
} }
void _update(DHTLogUpdate upd) { void _update(DHTLogUpdate upd) {

View File

@ -47,11 +47,13 @@ class _DHTLogRead implements DHTLogReadOperations {
final chunks = Iterable<int>.generate(length) final chunks = Iterable<int>.generate(length)
.slices(kMaxDHTConcurrency) .slices(kMaxDHTConcurrency)
.map((chunk) => .map((chunk) => chunk
chunk.map((pos) => get(pos + start, forceRefresh: forceRefresh))); .map((pos) async => get(pos + start, forceRefresh: forceRefresh)));
for (final chunk in chunks) { for (final chunk in chunks) {
final elems = await chunk.wait; final elems = await chunk.wait;
// If any element was unavailable, return null
if (elems.contains(null)) { if (elems.contains(null)) {
return null; return null;
} }

View File

@ -296,7 +296,7 @@ class _DHTLogSpine {
segmentKeyBytes); segmentKeyBytes);
} }
Future<DHTShortArray> _openOrCreateSegment(int segmentNumber) async { Future<DHTShortArray?> _openOrCreateSegment(int segmentNumber) async {
assert(_spineMutex.isLocked, 'should be in mutex here'); assert(_spineMutex.isLocked, 'should be in mutex here');
assert(_spineRecord.writer != null, 'should be writable'); assert(_spineRecord.writer != null, 'should be writable');
@ -306,8 +306,10 @@ class _DHTLogSpine {
final subkey = l.subkey; final subkey = l.subkey;
final segment = l.segment; final segment = l.segment;
try {
var subkeyData = await _spineRecord.get(subkey: subkey); var subkeyData = await _spineRecord.get(subkey: subkey);
subkeyData ??= _makeEmptySubkey(); subkeyData ??= _makeEmptySubkey();
while (true) { while (true) {
final segmentKey = _getSegmentKey(subkeyData!, segment); final segmentKey = _getSegmentKey(subkeyData!, segment);
if (segmentKey == null) { if (segmentKey == null) {
@ -352,6 +354,9 @@ class _DHTLogSpine {
} }
// Loop if we need to try again with the new data from the network // Loop if we need to try again with the new data from the network
} }
} on DHTExceptionNotAvailable {
return null;
}
} }
Future<DHTShortArray?> _openSegment(int segmentNumber) async { Future<DHTShortArray?> _openSegment(int segmentNumber) async {
@ -364,6 +369,7 @@ class _DHTLogSpine {
final segment = l.segment; final segment = l.segment;
// See if we have the segment key locally // See if we have the segment key locally
try {
TypedKey? segmentKey; TypedKey? segmentKey;
var subkeyData = await _spineRecord.get( var subkeyData = await _spineRecord.get(
subkey: subkey, refreshMode: DHTRecordRefreshMode.local); subkey: subkey, refreshMode: DHTRecordRefreshMode.local);
@ -392,6 +398,9 @@ class _DHTLogSpine {
routingContext: _spineRecord.routingContext, routingContext: _spineRecord.routingContext,
); );
return segmentRec; return segmentRec;
} on DHTExceptionNotAvailable {
return null;
}
} }
_DHTLogSegmentLookup _lookupSegment(int segmentNumber) { _DHTLogSegmentLookup _lookupSegment(int segmentNumber) {

View File

@ -17,7 +17,7 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations {
} }
final lookup = await _spine.lookupPosition(pos); final lookup = await _spine.lookupPosition(pos);
if (lookup == null) { if (lookup == null) {
throw DHTExceptionInvalidData(); throw const DHTExceptionInvalidData();
} }
// Write item to the segment // Write item to the segment
@ -26,7 +26,7 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations {
final success = final success =
await write.tryWriteItem(lookup.pos, newValue, output: output); await write.tryWriteItem(lookup.pos, newValue, output: output);
if (!success) { if (!success) {
throw DHTExceptionOutdated(); throw const DHTExceptionOutdated();
} }
})); }));
} on DHTExceptionOutdated { } on DHTExceptionOutdated {
@ -45,12 +45,12 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations {
} }
final aLookup = await _spine.lookupPosition(aPos); final aLookup = await _spine.lookupPosition(aPos);
if (aLookup == null) { if (aLookup == null) {
throw DHTExceptionInvalidData(); throw const DHTExceptionInvalidData();
} }
final bLookup = await _spine.lookupPosition(bPos); final bLookup = await _spine.lookupPosition(bPos);
if (bLookup == null) { if (bLookup == null) {
await aLookup.close(); await aLookup.close();
throw DHTExceptionInvalidData(); throw const DHTExceptionInvalidData();
} }
// Swap items in the segments // Swap items in the segments
@ -65,20 +65,20 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations {
if (bItem.value == null) { if (bItem.value == null) {
final aItem = await aWrite.get(aLookup.pos); final aItem = await aWrite.get(aLookup.pos);
if (aItem == null) { if (aItem == null) {
throw DHTExceptionInvalidData(); throw const DHTExceptionInvalidData();
} }
await sb.operateWriteEventual((bWrite) async { await sb.operateWriteEventual((bWrite) async {
final success = await bWrite final success = await bWrite
.tryWriteItem(bLookup.pos, aItem, output: bItem); .tryWriteItem(bLookup.pos, aItem, output: bItem);
if (!success) { if (!success) {
throw DHTExceptionOutdated(); throw const DHTExceptionOutdated();
} }
}); });
} }
final success = final success =
await aWrite.tryWriteItem(aLookup.pos, bItem.value!); await aWrite.tryWriteItem(aLookup.pos, bItem.value!);
if (!success) { if (!success) {
throw DHTExceptionOutdated(); throw const DHTExceptionOutdated();
} }
}))); })));
} }
@ -101,7 +101,7 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations {
await write.clear(); await write.clear();
} else if (lookup.pos != write.length) { } else if (lookup.pos != write.length) {
// We should always be appending at the length // We should always be appending at the length
throw DHTExceptionInvalidData(); throw const DHTExceptionInvalidData();
} }
return write.add(value); return write.add(value);
})); }));
@ -122,7 +122,7 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations {
final lookup = await _spine.lookupPosition(insertPos + valueIdx); final lookup = await _spine.lookupPosition(insertPos + valueIdx);
if (lookup == null) { if (lookup == null) {
throw DHTExceptionInvalidData(); throw const DHTExceptionInvalidData();
} }
final sacount = min(remaining, DHTShortArray.maxElements - lookup.pos); final sacount = min(remaining, DHTShortArray.maxElements - lookup.pos);
@ -137,7 +137,7 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations {
await write.clear(); await write.clear();
} else if (lookup.pos != write.length) { } else if (lookup.pos != write.length) {
// We should always be appending at the length // We should always be appending at the length
throw DHTExceptionInvalidData(); throw const DHTExceptionInvalidData();
} }
return write.addAll(sublistValues); return write.addAll(sublistValues);
})); }));
@ -152,7 +152,7 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations {
await dws(); await dws();
if (!success) { if (!success) {
throw DHTExceptionOutdated(); throw const DHTExceptionOutdated();
} }
} }

View File

@ -134,11 +134,25 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
return null; return null;
} }
final valueData = await _routingContext.getDHTValue(key, subkey, var retry = kDHTTryAgainTries;
ValueData? valueData;
while (true) {
try {
valueData = await _routingContext.getDHTValue(key, subkey,
forceRefresh: refreshMode._forceRefresh); forceRefresh: refreshMode._forceRefresh);
break;
} on VeilidAPIExceptionTryAgain {
retry--;
if (retry == 0) {
throw const DHTExceptionNotAvailable();
}
await asyncSleep();
}
}
if (valueData == null) { if (valueData == null) {
return null; return null;
} }
// See if this get resulted in a newer sequence number // See if this get resulted in a newer sequence number
if (refreshMode == DHTRecordRefreshMode.update && if (refreshMode == DHTRecordRefreshMode.update &&
lastSeq != null && lastSeq != null &&
@ -415,10 +429,10 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
Timestamp? expiration, Timestamp? expiration,
int? count}) async { int? count}) async {
// Set up watch requirements which will get picked up by the next tick // Set up watch requirements which will get picked up by the next tick
final oldWatchState = watchState; final oldWatchState = _watchState;
watchState = _watchState =
_WatchState(subkeys: subkeys, expiration: expiration, count: count); _WatchState(subkeys: subkeys, expiration: expiration, count: count);
if (oldWatchState != watchState) { if (oldWatchState != _watchState) {
_sharedDHTRecordData.needsWatchStateUpdate = true; _sharedDHTRecordData.needsWatchStateUpdate = true;
} }
} }
@ -476,8 +490,8 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
/// Takes effect on the next DHTRecordPool tick /// Takes effect on the next DHTRecordPool tick
Future<void> cancelWatch() async { Future<void> cancelWatch() async {
// Tear down watch requirements // Tear down watch requirements
if (watchState != null) { if (_watchState != null) {
watchState = null; _watchState = null;
_sharedDHTRecordData.needsWatchStateUpdate = true; _sharedDHTRecordData.needsWatchStateUpdate = true;
} }
} }
@ -503,7 +517,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
{required bool local, {required bool local,
required Uint8List? data, required Uint8List? data,
required List<ValueSubkeyRange> subkeys}) { required List<ValueSubkeyRange> subkeys}) {
final ws = watchState; final ws = _watchState;
if (ws != null) { if (ws != null) {
final watchedSubkeys = ws.subkeys; final watchedSubkeys = ws.subkeys;
if (watchedSubkeys == null) { if (watchedSubkeys == null) {
@ -551,6 +565,5 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
final _mutex = Mutex(); final _mutex = Mutex();
int _openCount; int _openCount;
StreamController<DHTRecordWatchChange>? _watchController; StreamController<DHTRecordWatchChange>? _watchController;
@internal _WatchState? _watchState;
_WatchState? watchState;
} }

View File

@ -29,8 +29,7 @@ abstract class DHTRecordCubit<T> extends Cubit<AsyncValue<T>> {
_record = await open(); _record = await open();
_wantsCloseRecord = true; _wantsCloseRecord = true;
break; break;
} on VeilidAPIExceptionKeyNotFound { } on DHTExceptionNotAvailable {
} on VeilidAPIExceptionTryAgain {
// Wait for a bit // Wait for a bit
await asyncSleep(); await asyncSleep();
} }

View File

@ -21,8 +21,11 @@ part 'dht_record_pool_private.dart';
/// Maximum number of concurrent DHT operations to perform on the network /// Maximum number of concurrent DHT operations to perform on the network
const int kMaxDHTConcurrency = 8; const int kMaxDHTConcurrency = 8;
/// Number of times to retry a 'key not found' /// Total number of times to try in a 'VeilidAPIExceptionKeyNotFound' loop
const int kDHTKeyNotFoundRetry = 3; const int kDHTKeyNotFoundTries = 3;
/// Total number of times to try in a 'VeilidAPIExceptionTryAgain' loop
const int kDHTTryAgainTries = 3;
typedef DHTRecordPoolLogger = void Function(String message); typedef DHTRecordPoolLogger = void Function(String message);
@ -280,12 +283,12 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
for (final rec in openedRecordInfo.records) { for (final rec in openedRecordInfo.records) {
// See if the watch had an expiration and if it has expired // See if the watch had an expiration and if it has expired
// otherwise the renewal will keep the same parameters // otherwise the renewal will keep the same parameters
final watchState = rec.watchState; final watchState = rec._watchState;
if (watchState != null) { if (watchState != null) {
final exp = watchState.expiration; final exp = watchState.expiration;
if (exp != null && exp.value < now) { if (exp != null && exp.value < now) {
// Has expiration, and it has expired, clear watch state // Has expiration, and it has expired, clear watch state
rec.watchState = null; rec._watchState = null;
} }
} }
} }
@ -392,7 +395,7 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
if (openedRecordInfo == null) { if (openedRecordInfo == null) {
// Fresh open, just open the record // Fresh open, just open the record
var retry = kDHTKeyNotFoundRetry; var retry = kDHTKeyNotFoundTries;
late final DHTRecordDescriptor recordDescriptor; late final DHTRecordDescriptor recordDescriptor;
while (true) { while (true) {
try { try {
@ -403,7 +406,7 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
await asyncSleep(); await asyncSleep();
retry--; retry--;
if (retry == 0) { if (retry == 0) {
rethrow; throw DHTExceptionNotAvailable();
} }
} }
} }
@ -705,7 +708,7 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
var cancelWatch = true; var cancelWatch = true;
for (final rec in records) { for (final rec in records) {
final ws = rec.watchState; final ws = rec._watchState;
if (ws != null) { if (ws != null) {
cancelWatch = false; cancelWatch = false;
final wsCount = ws.count; final wsCount = ws.count;
@ -762,9 +765,9 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
static void _updateWatchRealExpirations(Iterable<DHTRecord> records, static void _updateWatchRealExpirations(Iterable<DHTRecord> records,
Timestamp realExpiration, Timestamp renewalTime) { Timestamp realExpiration, Timestamp renewalTime) {
for (final rec in records) { for (final rec in records) {
final ws = rec.watchState; final ws = rec._watchState;
if (ws != null) { if (ws != null) {
rec.watchState = _WatchState( rec._watchState = _WatchState(
subkeys: ws.subkeys, subkeys: ws.subkeys,
expiration: ws.expiration, expiration: ws.expiration,
count: ws.count, count: ws.count,

View File

@ -68,7 +68,7 @@ class DHTShortArray implements DHTDeleteable<DHTShortArray> {
} }
}); });
return dhtShortArray; return dhtShortArray;
} on Exception catch (_) { } on Exception {
await dhtRecord.close(); await dhtRecord.close();
await pool.deleteRecord(dhtRecord.key); await pool.deleteRecord(dhtRecord.key);
rethrow; rethrow;
@ -89,7 +89,7 @@ class DHTShortArray implements DHTDeleteable<DHTShortArray> {
final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); final dhtShortArray = DHTShortArray._(headRecord: dhtRecord);
await dhtShortArray._head.operate((head) => head._loadHead()); await dhtShortArray._head.operate((head) => head._loadHead());
return dhtShortArray; return dhtShortArray;
} on Exception catch (_) { } on Exception {
await dhtRecord.close(); await dhtRecord.close();
rethrow; rethrow;
} }
@ -113,7 +113,7 @@ class DHTShortArray implements DHTDeleteable<DHTShortArray> {
final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); final dhtShortArray = DHTShortArray._(headRecord: dhtRecord);
await dhtShortArray._head.operate((head) => head._loadHead()); await dhtShortArray._head.operate((head) => head._loadHead());
return dhtShortArray; return dhtShortArray;
} on Exception catch (_) { } on Exception {
await dhtRecord.close(); await dhtRecord.close();
rethrow; rethrow;
} }

View File

@ -8,6 +8,7 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import '../../../veilid_support.dart'; import '../../../veilid_support.dart';
import '../interfaces/refreshable_cubit.dart';
@immutable @immutable
class DHTShortArrayElementState<T> extends Equatable { class DHTShortArrayElementState<T> extends Equatable {
@ -24,7 +25,7 @@ typedef DHTShortArrayState<T> = AsyncValue<IList<DHTShortArrayElementState<T>>>;
typedef DHTShortArrayBusyState<T> = BlocBusyState<DHTShortArrayState<T>>; typedef DHTShortArrayBusyState<T> = BlocBusyState<DHTShortArrayState<T>>;
class DHTShortArrayCubit<T> extends Cubit<DHTShortArrayBusyState<T>> class DHTShortArrayCubit<T> extends Cubit<DHTShortArrayBusyState<T>>
with BlocBusyWrapper<DHTShortArrayState<T>> { with BlocBusyWrapper<DHTShortArrayState<T>>, RefreshableCubit {
DHTShortArrayCubit({ DHTShortArrayCubit({
required Future<DHTShortArray> Function() open, required Future<DHTShortArray> Function() open,
required T Function(List<int> data) decodeElement, required T Function(List<int> data) decodeElement,
@ -39,7 +40,7 @@ class DHTShortArrayCubit<T> extends Cubit<DHTShortArrayBusyState<T>>
_shortArray = await open(); _shortArray = await open();
_wantsCloseRecord = true; _wantsCloseRecord = true;
break; break;
} on VeilidAPIExceptionTryAgain { } on DHTExceptionNotAvailable {
// Wait for a bit // Wait for a bit
await asyncSleep(); await asyncSleep();
} }
@ -57,6 +58,7 @@ class DHTShortArrayCubit<T> extends Cubit<DHTShortArrayBusyState<T>>
}); });
} }
@override
Future<void> refresh({bool forceRefresh = false}) async { Future<void> refresh({bool forceRefresh = false}) async {
await _initWait(); await _initWait();
await _refreshNoWait(forceRefresh: forceRefresh); await _refreshNoWait(forceRefresh: forceRefresh);
@ -87,9 +89,13 @@ class DHTShortArrayCubit<T> extends Cubit<DHTShortArrayBusyState<T>>
.toIList(); .toIList();
return allItems; return allItems;
}); });
if (newState != null) { if (newState == null) {
emit(AsyncValue.data(newState)); // Mark us as needing refresh
setWantsRefresh();
return;
} }
emit(AsyncValue.data(newState));
setRefreshed();
} on Exception catch (e) { } on Exception catch (e) {
emit(AsyncValue.error(e)); emit(AsyncValue.error(e));
} }

View File

@ -91,7 +91,7 @@ class _DHTShortArrayHead {
if (!await _writeHead()) { if (!await _writeHead()) {
// Failed to write head means head got overwritten so write should // Failed to write head means head got overwritten so write should
// be considered failed // be considered failed
throw DHTExceptionOutdated(); throw const DHTExceptionOutdated();
} }
onUpdatedHead?.call(); onUpdatedHead?.call();

View File

@ -17,11 +17,12 @@ class _DHTShortArrayRead implements DHTShortArrayReadOperations {
throw IndexError.withLength(pos, length); throw IndexError.withLength(pos, length);
} }
try {
final lookup = await _head.lookupPosition(pos, false); final lookup = await _head.lookupPosition(pos, false);
final refresh = forceRefresh || _head.positionNeedsRefresh(pos); final refresh = forceRefresh || _head.positionNeedsRefresh(pos);
final outSeqNum = Output<int>(); final outSeqNum = Output<int>();
final out = lookup.record.get( final out = await lookup.record.get(
subkey: lookup.recordSubkey, subkey: lookup.recordSubkey,
refreshMode: refresh refreshMode: refresh
? DHTRecordRefreshMode.network ? DHTRecordRefreshMode.network
@ -30,8 +31,11 @@ class _DHTShortArrayRead implements DHTShortArrayReadOperations {
if (outSeqNum.value != null) { if (outSeqNum.value != null) {
_head.updatePositionSeq(pos, false, outSeqNum.value!); _head.updatePositionSeq(pos, false, outSeqNum.value!);
} }
return out; return out;
} on DHTExceptionNotAvailable {
// If any element is not available, return null
return null;
}
} }
(int, int) _clampStartLen(int start, int? len) { (int, int) _clampStartLen(int start, int? len) {
@ -56,11 +60,13 @@ class _DHTShortArrayRead implements DHTShortArrayReadOperations {
final chunks = Iterable<int>.generate(length) final chunks = Iterable<int>.generate(length)
.slices(kMaxDHTConcurrency) .slices(kMaxDHTConcurrency)
.map((chunk) => .map((chunk) => chunk
chunk.map((pos) => get(pos + start, forceRefresh: forceRefresh))); .map((pos) async => get(pos + start, forceRefresh: forceRefresh)));
for (final chunk in chunks) { for (final chunk in chunks) {
final elems = await chunk.wait; final elems = await chunk.wait;
// If any element was unavailable, return null
if (elems.contains(null)) { if (elems.contains(null)) {
return null; return null;
} }

View File

@ -1,15 +1,21 @@
class DHTExceptionOutdated implements Exception { class DHTExceptionOutdated implements Exception {
DHTExceptionOutdated( const DHTExceptionOutdated(
[this.cause = 'operation failed due to newer dht value']); [this.cause = 'operation failed due to newer dht value']);
String cause; final String cause;
} }
class DHTExceptionInvalidData implements Exception { class DHTExceptionInvalidData implements Exception {
DHTExceptionInvalidData([this.cause = 'dht data structure is corrupt']); const DHTExceptionInvalidData([this.cause = 'dht data structure is corrupt']);
String cause; final String cause;
} }
class DHTExceptionCancelled implements Exception { class DHTExceptionCancelled implements Exception {
DHTExceptionCancelled([this.cause = 'operation was cancelled']); const DHTExceptionCancelled([this.cause = 'operation was cancelled']);
String cause; final String cause;
}
class DHTExceptionNotAvailable implements Exception {
const DHTExceptionNotAvailable(
[this.cause = 'request could not be completed at this time']);
final String cause;
} }

View File

@ -6,3 +6,4 @@ export 'dht_random_read.dart';
export 'dht_random_write.dart'; export 'dht_random_write.dart';
export 'dht_truncate.dart'; export 'dht_truncate.dart';
export 'exceptions.dart'; export 'exceptions.dart';
export 'refreshable_cubit.dart';

View File

@ -0,0 +1,16 @@
abstract mixin class RefreshableCubit {
Future<void> refresh({bool forceRefresh = false});
void setWantsRefresh() {
_wantsRefresh = true;
}
void setRefreshed() {
_wantsRefresh = false;
}
bool get wantsRefresh => _wantsRefresh;
////////////////////////////////////////////////////////////////////////////
bool _wantsRefresh = false;
}

View File

@ -1576,7 +1576,7 @@ packages:
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
url_launcher: url_launcher:
dependency: transitive dependency: "direct main"
description: description:
name: url_launcher name: url_launcher
sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"

View File

@ -96,6 +96,7 @@ dependencies:
stack_trace: ^1.11.1 stack_trace: ^1.11.1
stream_transform: ^2.1.0 stream_transform: ^2.1.0
transitioned_indexed_stack: ^1.0.2 transitioned_indexed_stack: ^1.0.2
url_launcher: ^6.3.0
uuid: ^4.4.0 uuid: ^4.4.0
veilid: veilid:
# veilid: ^0.0.1 # veilid: ^0.0.1