mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2024-10-01 06:55:46 -04:00
beta warning dialog
This commit is contained in:
parent
ba191d3903
commit
6080c2f0c6
@ -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",
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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'),
|
||||||
),
|
),
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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';
|
||||||
|
@ -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(
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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';
|
||||||
|
@ -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;
|
||||||
|
}
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user