mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2024-12-28 09:09:27 -05:00
message length limit
This commit is contained in:
parent
9dfb8c3f71
commit
94988718e8
@ -36,18 +36,19 @@
|
||||
"name": "Name",
|
||||
"pronouns": "Pronouns",
|
||||
"remove_account": "Remove Account",
|
||||
"delete_identity": "Delete Identity",
|
||||
"destroy_account": "Destroy Account",
|
||||
"remove_account_confirm": "Confirm Account Removal",
|
||||
"remove_account_description": "Remove account from this device only",
|
||||
"remove_account_confirm_message": " • Your account will be removed from this device ONLY\n • Your identity will remain recoverable with the recovery key\n • Your messages and contacts will remain available on other devices\n",
|
||||
"delete_identity_description": "Delete identity from all devices everywhere",
|
||||
"delete_identity_confirm_message": "This action is PERMANENT, and your identity will no longer be recoverable with the recovery key. Restoring from backups will not recover your account!",
|
||||
"delete_identity_confirm_message_details": "You will lose access to:\n • Your entire message history\n • Your contacts\n • This will not remove your messages you have sent from other people's devices\n",
|
||||
"destroy_account_confirm": "Confirm Account Destruction",
|
||||
"destroy_account_description": "Destroy account, removing it completely from all devices everywhere",
|
||||
"destroy_account_confirm_message": "This action is PERMANENT, and your VeilidChat account will no longer be recoverable with the recovery key. Restoring from backups will not recover your account!",
|
||||
"destroy_account_confirm_message_details": "You will lose access to:\n • Your entire message history\n • Your contacts\n • This will not remove your messages you have sent from other people's devices\n",
|
||||
"confirm_are_you_sure": "Are you sure you want to do this?",
|
||||
"failed_to_remove": "Failed to remove account.\n\nTry again when you have a more stable network connection.",
|
||||
"failed_to_delete": "Failed to delete identity.\n\nTry again when you have a more stable network connection.",
|
||||
"failed_to_destroy": "Failed to destroy account.\n\nTry again when you have a more stable network connection.",
|
||||
"account_removed": "Account removed successfully",
|
||||
"identity_deleted": "Identity deleted successfully"
|
||||
"account_destroyed": "Account destroyed successfully"
|
||||
},
|
||||
"show_recovery_key_page": {
|
||||
"titlebar": "Save Recovery Key",
|
||||
@ -103,7 +104,8 @@
|
||||
},
|
||||
"chat": {
|
||||
"start_a_conversation": "Start A Conversation",
|
||||
"say_something": "Say Something"
|
||||
"say_something": "Say Something",
|
||||
"message_too_long": "Message too long"
|
||||
},
|
||||
"create_invitation_dialog": {
|
||||
"title": "Create Contact Invitation",
|
||||
|
@ -7,7 +7,7 @@ import '../../../proto/proto.dart' as proto;
|
||||
import '../../tools/tools.dart';
|
||||
import '../models/models.dart';
|
||||
|
||||
const String veilidChatAccountKey = 'com.veilid.veilidchat';
|
||||
const String veilidChatApplicationId = 'com.veilid.veilidchat';
|
||||
|
||||
enum AccountRepositoryChange { localAccounts, userLogins, activeLocalAccount }
|
||||
|
||||
@ -194,6 +194,33 @@ class AccountRepository {
|
||||
/// Recover an account with the master identity secret
|
||||
|
||||
/// Delete an account from all devices
|
||||
Future<bool> destroyAccount(TypedKey superIdentityRecordKey,
|
||||
OwnedDHTRecordPointer accountRecord) async {
|
||||
// Get which local account we want to fetch the profile for
|
||||
final localAccount = fetchLocalAccount(superIdentityRecordKey);
|
||||
if (localAccount == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// See if we've logged into this account or if it is locked
|
||||
final userLogin = fetchUserLogin(superIdentityRecordKey);
|
||||
if (userLogin == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final success = await localAccount.superIdentity.currentInstance
|
||||
.removeAccount(
|
||||
superRecordKey: localAccount.superIdentity.recordKey,
|
||||
secretKey: userLogin.identitySecret.value,
|
||||
applicationId: veilidChatApplicationId,
|
||||
removeAccountCallback: (accountRecordInfos) async =>
|
||||
accountRecordInfos.singleOrNull);
|
||||
if (!success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return deleteLocalAccount(superIdentityRecordKey, accountRecord);
|
||||
}
|
||||
|
||||
Future<void> switchToAccount(TypedKey? superIdentityRecordKey) async {
|
||||
final activeLocalAccount = await _activeLocalAccount.get();
|
||||
@ -231,7 +258,7 @@ class AccountRepository {
|
||||
await superIdentity.currentInstance.addAccount(
|
||||
superRecordKey: superIdentity.recordKey,
|
||||
secretKey: identitySecret,
|
||||
accountKey: veilidChatAccountKey,
|
||||
applicationId: veilidChatApplicationId,
|
||||
createAccountCallback: (parent) async {
|
||||
// Make empty contact list
|
||||
log.debug('Creating contacts list');
|
||||
@ -305,7 +332,7 @@ class AccountRepository {
|
||||
.readAccount(
|
||||
superRecordKey: superIdentity.recordKey,
|
||||
secretKey: identitySecret,
|
||||
accountKey: veilidChatAccountKey);
|
||||
applicationId: veilidChatApplicationId);
|
||||
if (accountRecordInfoList.length > 1) {
|
||||
throw IdentityException.limitExceeded;
|
||||
} else if (accountRecordInfoList.isEmpty) {
|
||||
|
@ -134,8 +134,70 @@ class _EditAccountPageState extends State<EditAccountPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onDeleteIdentity() async {
|
||||
//
|
||||
Future<void> _onDestroyAccount() async {
|
||||
final confirmed = await StyledDialog.show<bool>(
|
||||
context: context,
|
||||
title: translate('edit_account_page.destroy_account_confirm'),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Text(translate('edit_account_page.destroy_account_confirm_message'))
|
||||
.paddingLTRB(24, 24, 24, 0),
|
||||
Text(translate(
|
||||
'edit_account_page.destroy_account_confirm_message_details'))
|
||||
.paddingLTRB(24, 24, 24, 0),
|
||||
Text(translate('edit_account_page.confirm_are_you_sure'))
|
||||
.paddingAll(8),
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
const Icon(Icons.cancel, size: 16).paddingLTRB(0, 0, 4, 0),
|
||||
Text(translate('button.no_cancel')).paddingLTRB(0, 0, 4, 0)
|
||||
])),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0),
|
||||
Text(translate('button.yes_proceed')).paddingLTRB(0, 0, 4, 0)
|
||||
]))
|
||||
]).paddingAll(24)
|
||||
]));
|
||||
if (confirmed != null && confirmed && mounted) {
|
||||
// dismiss the keyboard by unfocusing the textfield
|
||||
FocusScope.of(context).unfocus();
|
||||
|
||||
try {
|
||||
setState(() {
|
||||
_isInAsyncCall = true;
|
||||
});
|
||||
try {
|
||||
final success = await AccountRepository.instance.destroyAccount(
|
||||
widget.superIdentityRecordKey, widget.accountRecord);
|
||||
if (success && mounted) {
|
||||
showInfoToast(
|
||||
context, translate('edit_account_page.account_destroyed'));
|
||||
GoRouterHelper(context).pop();
|
||||
} else if (mounted) {
|
||||
showErrorToast(
|
||||
context, translate('edit_account_page.failed_to_destroy'));
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isInAsyncCall = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (mounted) {
|
||||
await showErrorModal(
|
||||
context, translate('new_account_page.error'), 'Exception: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSubmit(GlobalKey<FormBuilderState> formKey) async {
|
||||
@ -234,13 +296,13 @@ class _EditAccountPageState extends State<EditAccountPage> {
|
||||
Text(translate('edit_account_page.remove_account'))
|
||||
.paddingLTRB(0, 0, 4, 0)
|
||||
])).paddingLTRB(0, 8, 0, 24),
|
||||
Text(translate('edit_account_page.delete_identity_description')),
|
||||
Text(translate('edit_account_page.destroy_account_description')),
|
||||
ElevatedButton(
|
||||
onPressed: _onDeleteIdentity,
|
||||
onPressed: _onDestroyAccount,
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
const Icon(Icons.person_off, size: 16)
|
||||
.paddingLTRB(0, 0, 4, 0),
|
||||
Text(translate('edit_account_page.delete_identity'))
|
||||
Text(translate('edit_account_page.destroy_account'))
|
||||
.paddingLTRB(0, 0, 4, 0)
|
||||
])).paddingLTRB(0, 8, 0, 24)
|
||||
]).paddingSymmetric(horizontal: 24, vertical: 8))
|
||||
|
@ -42,6 +42,7 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
|
||||
super(ChatComponentState(
|
||||
chatKey: GlobalKey<ChatState>(),
|
||||
scrollController: AutoScrollController(),
|
||||
textEditingController: InputTextFieldController(),
|
||||
localUser: null,
|
||||
remoteUsers: const IMap.empty(),
|
||||
historicalRemoteUsers: const IMap.empty(),
|
||||
|
@ -97,11 +97,13 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
||||
// Initialize everything
|
||||
Future<void> _init() async {
|
||||
_unsentMessagesQueue = PersistentQueue<proto.Message>(
|
||||
table: 'SingleContactUnsentMessages',
|
||||
key: _remoteConversationRecordKey.toString(),
|
||||
fromBuffer: proto.Message.fromBuffer,
|
||||
closure: _processUnsentMessages,
|
||||
);
|
||||
table: 'SingleContactUnsentMessages',
|
||||
key: _remoteConversationRecordKey.toString(),
|
||||
fromBuffer: proto.Message.fromBuffer,
|
||||
closure: _processUnsentMessages,
|
||||
onError: (e, sp) {
|
||||
log.error('Exception while processing unsent messages: $e\n$sp\n');
|
||||
});
|
||||
|
||||
// Make crypto
|
||||
await _initCrypto();
|
||||
@ -297,16 +299,23 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
||||
proto.Message? previousMessage;
|
||||
final processedMessages = messages.toList();
|
||||
for (final message in processedMessages) {
|
||||
await _processMessageToSend(message, previousMessage);
|
||||
previousMessage = message;
|
||||
try {
|
||||
await _processMessageToSend(message, previousMessage);
|
||||
previousMessage = message;
|
||||
} on Exception catch (e) {
|
||||
log.error('Exception processing unsent message: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// _sendingMessages = messages;
|
||||
|
||||
// _renderState();
|
||||
|
||||
await _sentMessagesCubit!.operateAppendEventual((writer) =>
|
||||
writer.addAll(messages.map((m) => m.writeToBuffer()).toList()));
|
||||
try {
|
||||
await _sentMessagesCubit!.operateAppendEventual((writer) =>
|
||||
writer.addAll(messages.map((m) => m.writeToBuffer()).toList()));
|
||||
} on Exception catch (e) {
|
||||
log.error('Exception appending unsent messages: $e');
|
||||
}
|
||||
|
||||
// _sendingMessages = const IList.empty();
|
||||
}
|
||||
@ -403,6 +412,10 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
||||
..author = _accountInfo.identityTypedPublicKey.toProto()
|
||||
..timestamp = Veilid.instance.now().toInt64();
|
||||
|
||||
if ((message.writeToBuffer().lengthInBytes + 256) > 4096) {
|
||||
throw const FormatException('message is too long');
|
||||
}
|
||||
|
||||
// Put in the queue
|
||||
_unsentMessagesQueue.addSync(message);
|
||||
|
||||
|
@ -2,7 +2,8 @@ import 'package:async_tools/async_tools.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart' show Message, User;
|
||||
import 'package:flutter_chat_ui/flutter_chat_ui.dart' show ChatState;
|
||||
import 'package:flutter_chat_ui/flutter_chat_ui.dart'
|
||||
show ChatState, InputTextFieldController;
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
import 'package:veilid_support/veilid_support.dart';
|
||||
@ -19,6 +20,8 @@ class ChatComponentState with _$ChatComponentState {
|
||||
required GlobalKey<ChatState> chatKey,
|
||||
// ScrollController for the chat
|
||||
required AutoScrollController scrollController,
|
||||
// TextEditingController for the chat
|
||||
required InputTextFieldController textEditingController,
|
||||
// Local user
|
||||
required User? localUser,
|
||||
// Active remote users
|
||||
|
@ -20,6 +20,8 @@ mixin _$ChatComponentState {
|
||||
GlobalKey<ChatState> get chatKey =>
|
||||
throw _privateConstructorUsedError; // ScrollController for the chat
|
||||
AutoScrollController get scrollController =>
|
||||
throw _privateConstructorUsedError; // TextEditingController for the chat
|
||||
InputTextFieldController get textEditingController =>
|
||||
throw _privateConstructorUsedError; // Local user
|
||||
User? get localUser =>
|
||||
throw _privateConstructorUsedError; // Active remote users
|
||||
@ -47,6 +49,7 @@ abstract class $ChatComponentStateCopyWith<$Res> {
|
||||
$Res call(
|
||||
{GlobalKey<ChatState> chatKey,
|
||||
AutoScrollController scrollController,
|
||||
InputTextFieldController textEditingController,
|
||||
User? localUser,
|
||||
IMap<Typed<FixedEncodedString43>, User> remoteUsers,
|
||||
IMap<Typed<FixedEncodedString43>, User> historicalRemoteUsers,
|
||||
@ -72,6 +75,7 @@ class _$ChatComponentStateCopyWithImpl<$Res, $Val extends ChatComponentState>
|
||||
$Res call({
|
||||
Object? chatKey = null,
|
||||
Object? scrollController = null,
|
||||
Object? textEditingController = null,
|
||||
Object? localUser = freezed,
|
||||
Object? remoteUsers = null,
|
||||
Object? historicalRemoteUsers = null,
|
||||
@ -88,6 +92,10 @@ class _$ChatComponentStateCopyWithImpl<$Res, $Val extends ChatComponentState>
|
||||
? _value.scrollController
|
||||
: scrollController // ignore: cast_nullable_to_non_nullable
|
||||
as AutoScrollController,
|
||||
textEditingController: null == textEditingController
|
||||
? _value.textEditingController
|
||||
: textEditingController // ignore: cast_nullable_to_non_nullable
|
||||
as InputTextFieldController,
|
||||
localUser: freezed == localUser
|
||||
? _value.localUser
|
||||
: localUser // ignore: cast_nullable_to_non_nullable
|
||||
@ -136,6 +144,7 @@ abstract class _$$ChatComponentStateImplCopyWith<$Res>
|
||||
$Res call(
|
||||
{GlobalKey<ChatState> chatKey,
|
||||
AutoScrollController scrollController,
|
||||
InputTextFieldController textEditingController,
|
||||
User? localUser,
|
||||
IMap<Typed<FixedEncodedString43>, User> remoteUsers,
|
||||
IMap<Typed<FixedEncodedString43>, User> historicalRemoteUsers,
|
||||
@ -160,6 +169,7 @@ class __$$ChatComponentStateImplCopyWithImpl<$Res>
|
||||
$Res call({
|
||||
Object? chatKey = null,
|
||||
Object? scrollController = null,
|
||||
Object? textEditingController = null,
|
||||
Object? localUser = freezed,
|
||||
Object? remoteUsers = null,
|
||||
Object? historicalRemoteUsers = null,
|
||||
@ -176,6 +186,10 @@ class __$$ChatComponentStateImplCopyWithImpl<$Res>
|
||||
? _value.scrollController
|
||||
: scrollController // ignore: cast_nullable_to_non_nullable
|
||||
as AutoScrollController,
|
||||
textEditingController: null == textEditingController
|
||||
? _value.textEditingController
|
||||
: textEditingController // ignore: cast_nullable_to_non_nullable
|
||||
as InputTextFieldController,
|
||||
localUser: freezed == localUser
|
||||
? _value.localUser
|
||||
: localUser // ignore: cast_nullable_to_non_nullable
|
||||
@ -210,6 +224,7 @@ class _$ChatComponentStateImpl implements _ChatComponentState {
|
||||
const _$ChatComponentStateImpl(
|
||||
{required this.chatKey,
|
||||
required this.scrollController,
|
||||
required this.textEditingController,
|
||||
required this.localUser,
|
||||
required this.remoteUsers,
|
||||
required this.historicalRemoteUsers,
|
||||
@ -223,6 +238,9 @@ class _$ChatComponentStateImpl implements _ChatComponentState {
|
||||
// ScrollController for the chat
|
||||
@override
|
||||
final AutoScrollController scrollController;
|
||||
// TextEditingController for the chat
|
||||
@override
|
||||
final InputTextFieldController textEditingController;
|
||||
// Local user
|
||||
@override
|
||||
final User? localUser;
|
||||
@ -244,7 +262,7 @@ class _$ChatComponentStateImpl implements _ChatComponentState {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ChatComponentState(chatKey: $chatKey, scrollController: $scrollController, localUser: $localUser, remoteUsers: $remoteUsers, historicalRemoteUsers: $historicalRemoteUsers, unknownUsers: $unknownUsers, messageWindow: $messageWindow, title: $title)';
|
||||
return 'ChatComponentState(chatKey: $chatKey, scrollController: $scrollController, textEditingController: $textEditingController, localUser: $localUser, remoteUsers: $remoteUsers, historicalRemoteUsers: $historicalRemoteUsers, unknownUsers: $unknownUsers, messageWindow: $messageWindow, title: $title)';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -255,6 +273,8 @@ class _$ChatComponentStateImpl implements _ChatComponentState {
|
||||
(identical(other.chatKey, chatKey) || other.chatKey == chatKey) &&
|
||||
(identical(other.scrollController, scrollController) ||
|
||||
other.scrollController == scrollController) &&
|
||||
(identical(other.textEditingController, textEditingController) ||
|
||||
other.textEditingController == textEditingController) &&
|
||||
(identical(other.localUser, localUser) ||
|
||||
other.localUser == localUser) &&
|
||||
(identical(other.remoteUsers, remoteUsers) ||
|
||||
@ -273,6 +293,7 @@ class _$ChatComponentStateImpl implements _ChatComponentState {
|
||||
runtimeType,
|
||||
chatKey,
|
||||
scrollController,
|
||||
textEditingController,
|
||||
localUser,
|
||||
remoteUsers,
|
||||
historicalRemoteUsers,
|
||||
@ -292,6 +313,7 @@ abstract class _ChatComponentState implements ChatComponentState {
|
||||
const factory _ChatComponentState(
|
||||
{required final GlobalKey<ChatState> chatKey,
|
||||
required final AutoScrollController scrollController,
|
||||
required final InputTextFieldController textEditingController,
|
||||
required final User? localUser,
|
||||
required final IMap<Typed<FixedEncodedString43>, User> remoteUsers,
|
||||
required final IMap<Typed<FixedEncodedString43>, User>
|
||||
@ -304,6 +326,8 @@ abstract class _ChatComponentState implements ChatComponentState {
|
||||
GlobalKey<ChatState> get chatKey;
|
||||
@override // ScrollController for the chat
|
||||
AutoScrollController get scrollController;
|
||||
@override // TextEditingController for the chat
|
||||
InputTextFieldController get textEditingController;
|
||||
@override // Local user
|
||||
User? get localUser;
|
||||
@override // Active remote users
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:async_tools/async_tools.dart';
|
||||
@ -6,6 +7,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
||||
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../../account_manager/account_manager.dart';
|
||||
@ -154,6 +156,14 @@ class ChatComponentWidget extends StatelessWidget {
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final chatTheme = makeChatTheme(scale, textTheme);
|
||||
final errorChatTheme = (ChatThemeEditor(chatTheme)
|
||||
..inputTextColor = scale.errorScale.primary
|
||||
..sendButtonIcon = Image.asset(
|
||||
'assets/icon-send.png',
|
||||
color: scale.errorScale.primary,
|
||||
package: 'flutter_chat_ui',
|
||||
))
|
||||
.commit();
|
||||
|
||||
// Get the enclosing chat component cubit that contains our state
|
||||
// (created by ChatComponentWidget.builder())
|
||||
@ -216,80 +226,125 @@ class ChatComponentWidget extends StatelessWidget {
|
||||
),
|
||||
Expanded(
|
||||
child: DecoratedBox(
|
||||
decoration: const BoxDecoration(),
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (notification) {
|
||||
if (chatComponentCubit.scrollOffset != 0) {
|
||||
decoration: const BoxDecoration(),
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (notification) {
|
||||
if (chatComponentCubit.scrollOffset != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isFirstPage &&
|
||||
notification.metrics.pixels <=
|
||||
((notification.metrics.maxScrollExtent -
|
||||
notification.metrics
|
||||
.minScrollExtent) *
|
||||
(1.0 - onEndReachedThreshold) +
|
||||
notification
|
||||
.metrics.minScrollExtent)) {
|
||||
//
|
||||
final scrollOffset = (notification
|
||||
.metrics.maxScrollExtent -
|
||||
notification.metrics.minScrollExtent) *
|
||||
(1.0 - onEndReachedThreshold);
|
||||
|
||||
chatComponentCubit.scrollOffset = scrollOffset;
|
||||
|
||||
//
|
||||
singleFuture(chatComponentState.chatKey,
|
||||
() async {
|
||||
await _handlePageForward(chatComponentCubit,
|
||||
messageWindow, notification);
|
||||
});
|
||||
} else if (!isLastPage &&
|
||||
notification.metrics.pixels >=
|
||||
((notification.metrics.maxScrollExtent -
|
||||
notification.metrics
|
||||
.minScrollExtent) *
|
||||
onEndReachedThreshold +
|
||||
notification
|
||||
.metrics.minScrollExtent)) {
|
||||
//
|
||||
final scrollOffset = -(notification
|
||||
.metrics.maxScrollExtent -
|
||||
notification.metrics.minScrollExtent) *
|
||||
(1.0 - onEndReachedThreshold);
|
||||
|
||||
chatComponentCubit.scrollOffset = scrollOffset;
|
||||
//
|
||||
singleFuture(chatComponentState.chatKey,
|
||||
() async {
|
||||
await _handlePageBackward(chatComponentCubit,
|
||||
messageWindow, notification);
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable:
|
||||
chatComponentState.textEditingController,
|
||||
builder: (context, textEditingValue, __) {
|
||||
final messageIsValid = utf8
|
||||
.encode(textEditingValue.text)
|
||||
.lengthInBytes <
|
||||
2048;
|
||||
|
||||
if (!isFirstPage &&
|
||||
notification.metrics.pixels <=
|
||||
((notification.metrics.maxScrollExtent -
|
||||
notification
|
||||
.metrics.minScrollExtent) *
|
||||
(1.0 - onEndReachedThreshold) +
|
||||
notification.metrics.minScrollExtent)) {
|
||||
//
|
||||
final scrollOffset = (notification
|
||||
.metrics.maxScrollExtent -
|
||||
notification.metrics.minScrollExtent) *
|
||||
(1.0 - onEndReachedThreshold);
|
||||
|
||||
chatComponentCubit.scrollOffset = scrollOffset;
|
||||
|
||||
//
|
||||
singleFuture(chatComponentState.chatKey,
|
||||
() async {
|
||||
await _handlePageForward(chatComponentCubit,
|
||||
messageWindow, notification);
|
||||
});
|
||||
} else if (!isLastPage &&
|
||||
notification.metrics.pixels >=
|
||||
((notification.metrics.maxScrollExtent -
|
||||
notification
|
||||
.metrics.minScrollExtent) *
|
||||
onEndReachedThreshold +
|
||||
notification.metrics.minScrollExtent)) {
|
||||
//
|
||||
final scrollOffset = -(notification
|
||||
.metrics.maxScrollExtent -
|
||||
notification.metrics.minScrollExtent) *
|
||||
(1.0 - onEndReachedThreshold);
|
||||
|
||||
chatComponentCubit.scrollOffset = scrollOffset;
|
||||
//
|
||||
singleFuture(chatComponentState.chatKey,
|
||||
() async {
|
||||
await _handlePageBackward(chatComponentCubit,
|
||||
messageWindow, notification);
|
||||
});
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: Chat(
|
||||
key: chatComponentState.chatKey,
|
||||
theme: chatTheme,
|
||||
messages: messageWindow.window.toList(),
|
||||
scrollToBottomOnSend: isFirstPage,
|
||||
scrollController:
|
||||
chatComponentState.scrollController,
|
||||
// isLastPage: isLastPage,
|
||||
// onEndReached: () async {
|
||||
// await _handlePageBackward(
|
||||
// chatComponentCubit, messageWindow);
|
||||
// },
|
||||
//onEndReachedThreshold: onEndReachedThreshold,
|
||||
//onAttachmentPressed: _handleAttachmentPressed,
|
||||
//onMessageTap: _handleMessageTap,
|
||||
//onPreviewDataFetched: _handlePreviewDataFetched,
|
||||
onSendPressed: (pt) =>
|
||||
_handleSendPressed(chatComponentCubit, pt),
|
||||
//showUserAvatars: false,
|
||||
//showUserNames: true,
|
||||
user: localUser,
|
||||
emptyState: const EmptyChatWidget())),
|
||||
),
|
||||
return Chat(
|
||||
key: chatComponentState.chatKey,
|
||||
theme: messageIsValid
|
||||
? chatTheme
|
||||
: errorChatTheme,
|
||||
messages: messageWindow.window.toList(),
|
||||
scrollToBottomOnSend: isFirstPage,
|
||||
scrollController:
|
||||
chatComponentState.scrollController,
|
||||
inputOptions: InputOptions(
|
||||
inputClearMode: messageIsValid
|
||||
? InputClearMode.always
|
||||
: InputClearMode.never,
|
||||
textEditingController:
|
||||
chatComponentState
|
||||
.textEditingController),
|
||||
// isLastPage: isLastPage,
|
||||
// onEndReached: () async {
|
||||
// await _handlePageBackward(
|
||||
// chatComponentCubit, messageWindow);
|
||||
// },
|
||||
//onEndReachedThreshold: onEndReachedThreshold,
|
||||
//onAttachmentPressed: _handleAttachmentPressed,
|
||||
//onMessageTap: _handleMessageTap,
|
||||
//onPreviewDataFetched: _handlePreviewDataFetched,
|
||||
onSendPressed: (pt) {
|
||||
try {
|
||||
if (!messageIsValid) {
|
||||
showErrorToast(
|
||||
context,
|
||||
translate(
|
||||
'chat.message_too_long'));
|
||||
return;
|
||||
}
|
||||
_handleSendPressed(
|
||||
chatComponentCubit, pt);
|
||||
} on FormatException {
|
||||
showErrorToast(
|
||||
context,
|
||||
translate(
|
||||
'chat.message_too_long'));
|
||||
}
|
||||
},
|
||||
listBottomWidget: messageIsValid
|
||||
? null
|
||||
: Text(
|
||||
translate(
|
||||
'chat.message_too_long'),
|
||||
style: TextStyle(
|
||||
color: scale
|
||||
.errorScale.primary))
|
||||
.toCenter(),
|
||||
//showUserAvatars: false,
|
||||
//showUserNames: true,
|
||||
user: localUser,
|
||||
emptyState: const EmptyChatWidget());
|
||||
}))),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -1,3 +1,5 @@
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
|
||||
|
||||
@ -52,3 +54,398 @@ ChatTheme makeChatTheme(ScaleScheme scale, TextTheme textTheme) =>
|
||||
color: Colors.white,
|
||||
fontSize: 64,
|
||||
));
|
||||
|
||||
class EditedChatTheme extends ChatTheme {
|
||||
const EditedChatTheme({
|
||||
required super.attachmentButtonIcon,
|
||||
required super.attachmentButtonMargin,
|
||||
required super.backgroundColor,
|
||||
super.bubbleMargin,
|
||||
required super.dateDividerMargin,
|
||||
required super.dateDividerTextStyle,
|
||||
required super.deliveredIcon,
|
||||
required super.documentIcon,
|
||||
required super.emptyChatPlaceholderTextStyle,
|
||||
required super.errorColor,
|
||||
required super.errorIcon,
|
||||
required super.inputBackgroundColor,
|
||||
required super.inputSurfaceTintColor,
|
||||
required super.inputElevation,
|
||||
required super.inputBorderRadius,
|
||||
super.inputContainerDecoration,
|
||||
required super.inputMargin,
|
||||
required super.inputPadding,
|
||||
required super.inputTextColor,
|
||||
super.inputTextCursorColor,
|
||||
required super.inputTextDecoration,
|
||||
required super.inputTextStyle,
|
||||
required super.messageBorderRadius,
|
||||
required super.messageInsetsHorizontal,
|
||||
required super.messageInsetsVertical,
|
||||
required super.messageMaxWidth,
|
||||
required super.primaryColor,
|
||||
required super.receivedEmojiMessageTextStyle,
|
||||
super.receivedMessageBodyBoldTextStyle,
|
||||
super.receivedMessageBodyCodeTextStyle,
|
||||
super.receivedMessageBodyLinkTextStyle,
|
||||
required super.receivedMessageBodyTextStyle,
|
||||
required super.receivedMessageCaptionTextStyle,
|
||||
required super.receivedMessageDocumentIconColor,
|
||||
required super.receivedMessageLinkDescriptionTextStyle,
|
||||
required super.receivedMessageLinkTitleTextStyle,
|
||||
required super.secondaryColor,
|
||||
required super.seenIcon,
|
||||
required super.sendButtonIcon,
|
||||
required super.sendButtonMargin,
|
||||
required super.sendingIcon,
|
||||
required super.sentEmojiMessageTextStyle,
|
||||
super.sentMessageBodyBoldTextStyle,
|
||||
super.sentMessageBodyCodeTextStyle,
|
||||
super.sentMessageBodyLinkTextStyle,
|
||||
required super.sentMessageBodyTextStyle,
|
||||
required super.sentMessageCaptionTextStyle,
|
||||
required super.sentMessageDocumentIconColor,
|
||||
required super.sentMessageLinkDescriptionTextStyle,
|
||||
required super.sentMessageLinkTitleTextStyle,
|
||||
required super.statusIconPadding,
|
||||
required super.systemMessageTheme,
|
||||
required super.typingIndicatorTheme,
|
||||
required super.unreadHeaderTheme,
|
||||
required super.userAvatarImageBackgroundColor,
|
||||
required super.userAvatarNameColors,
|
||||
required super.userAvatarTextStyle,
|
||||
required super.userNameTextStyle,
|
||||
super.highlightMessageColor,
|
||||
});
|
||||
}
|
||||
|
||||
class ChatThemeEditor {
|
||||
ChatThemeEditor(ChatTheme base)
|
||||
: attachmentButtonIcon = base.attachmentButtonIcon,
|
||||
attachmentButtonMargin = base.attachmentButtonMargin,
|
||||
backgroundColor = base.backgroundColor,
|
||||
bubbleMargin = base.bubbleMargin,
|
||||
dateDividerMargin = base.dateDividerMargin,
|
||||
dateDividerTextStyle = base.dateDividerTextStyle,
|
||||
deliveredIcon = base.deliveredIcon,
|
||||
documentIcon = base.documentIcon,
|
||||
emptyChatPlaceholderTextStyle = base.emptyChatPlaceholderTextStyle,
|
||||
errorColor = base.errorColor,
|
||||
errorIcon = base.errorIcon,
|
||||
inputBackgroundColor = base.inputBackgroundColor,
|
||||
inputSurfaceTintColor = base.inputSurfaceTintColor,
|
||||
inputElevation = base.inputElevation,
|
||||
inputBorderRadius = base.inputBorderRadius,
|
||||
inputContainerDecoration = base.inputContainerDecoration,
|
||||
inputMargin = base.inputMargin,
|
||||
inputPadding = base.inputPadding,
|
||||
inputTextColor = base.inputTextColor,
|
||||
inputTextCursorColor = base.inputTextCursorColor,
|
||||
inputTextDecoration = base.inputTextDecoration,
|
||||
inputTextStyle = base.inputTextStyle,
|
||||
messageBorderRadius = base.messageBorderRadius,
|
||||
messageInsetsHorizontal = base.messageInsetsHorizontal,
|
||||
messageInsetsVertical = base.messageInsetsVertical,
|
||||
messageMaxWidth = base.messageMaxWidth,
|
||||
primaryColor = base.primaryColor,
|
||||
receivedEmojiMessageTextStyle = base.receivedEmojiMessageTextStyle,
|
||||
receivedMessageBodyBoldTextStyle =
|
||||
base.receivedMessageBodyBoldTextStyle,
|
||||
receivedMessageBodyCodeTextStyle =
|
||||
base.receivedMessageBodyCodeTextStyle,
|
||||
receivedMessageBodyLinkTextStyle =
|
||||
base.receivedMessageBodyLinkTextStyle,
|
||||
receivedMessageBodyTextStyle = base.receivedMessageBodyTextStyle,
|
||||
receivedMessageCaptionTextStyle = base.receivedMessageCaptionTextStyle,
|
||||
receivedMessageDocumentIconColor =
|
||||
base.receivedMessageDocumentIconColor,
|
||||
receivedMessageLinkDescriptionTextStyle =
|
||||
base.receivedMessageLinkDescriptionTextStyle,
|
||||
receivedMessageLinkTitleTextStyle =
|
||||
base.receivedMessageLinkTitleTextStyle,
|
||||
secondaryColor = base.secondaryColor,
|
||||
seenIcon = base.seenIcon,
|
||||
sendButtonIcon = base.sendButtonIcon,
|
||||
sendButtonMargin = base.sendButtonMargin,
|
||||
sendingIcon = base.sendingIcon,
|
||||
sentEmojiMessageTextStyle = base.sentEmojiMessageTextStyle,
|
||||
sentMessageBodyBoldTextStyle = base.sentMessageBodyBoldTextStyle,
|
||||
sentMessageBodyCodeTextStyle = base.sentMessageBodyCodeTextStyle,
|
||||
sentMessageBodyLinkTextStyle = base.sentMessageBodyLinkTextStyle,
|
||||
sentMessageBodyTextStyle = base.sentMessageBodyTextStyle,
|
||||
sentMessageCaptionTextStyle = base.sentMessageCaptionTextStyle,
|
||||
sentMessageDocumentIconColor = base.sentMessageDocumentIconColor,
|
||||
sentMessageLinkDescriptionTextStyle =
|
||||
base.sentMessageLinkDescriptionTextStyle,
|
||||
sentMessageLinkTitleTextStyle = base.sentMessageLinkTitleTextStyle,
|
||||
statusIconPadding = base.statusIconPadding,
|
||||
systemMessageTheme = base.systemMessageTheme,
|
||||
typingIndicatorTheme = base.typingIndicatorTheme,
|
||||
unreadHeaderTheme = base.unreadHeaderTheme,
|
||||
userAvatarImageBackgroundColor = base.userAvatarImageBackgroundColor,
|
||||
userAvatarNameColors = base.userAvatarNameColors,
|
||||
userAvatarTextStyle = base.userAvatarTextStyle,
|
||||
userNameTextStyle = base.userNameTextStyle,
|
||||
highlightMessageColor = base.highlightMessageColor;
|
||||
|
||||
EditedChatTheme commit() => EditedChatTheme(
|
||||
attachmentButtonIcon: attachmentButtonIcon,
|
||||
attachmentButtonMargin: attachmentButtonMargin,
|
||||
backgroundColor: backgroundColor,
|
||||
bubbleMargin: bubbleMargin,
|
||||
dateDividerMargin: dateDividerMargin,
|
||||
dateDividerTextStyle: dateDividerTextStyle,
|
||||
deliveredIcon: deliveredIcon,
|
||||
documentIcon: documentIcon,
|
||||
emptyChatPlaceholderTextStyle: emptyChatPlaceholderTextStyle,
|
||||
errorColor: errorColor,
|
||||
errorIcon: errorIcon,
|
||||
inputBackgroundColor: inputBackgroundColor,
|
||||
inputSurfaceTintColor: inputSurfaceTintColor,
|
||||
inputElevation: inputElevation,
|
||||
inputBorderRadius: inputBorderRadius,
|
||||
inputContainerDecoration: inputContainerDecoration,
|
||||
inputMargin: inputMargin,
|
||||
inputPadding: inputPadding,
|
||||
inputTextColor: inputTextColor,
|
||||
inputTextCursorColor: inputTextCursorColor,
|
||||
inputTextDecoration: inputTextDecoration,
|
||||
inputTextStyle: inputTextStyle,
|
||||
messageBorderRadius: messageBorderRadius,
|
||||
messageInsetsHorizontal: messageInsetsHorizontal,
|
||||
messageInsetsVertical: messageInsetsVertical,
|
||||
messageMaxWidth: messageMaxWidth,
|
||||
primaryColor: primaryColor,
|
||||
receivedEmojiMessageTextStyle: receivedEmojiMessageTextStyle,
|
||||
receivedMessageBodyBoldTextStyle: receivedMessageBodyBoldTextStyle,
|
||||
receivedMessageBodyCodeTextStyle: receivedMessageBodyCodeTextStyle,
|
||||
receivedMessageBodyLinkTextStyle: receivedMessageBodyLinkTextStyle,
|
||||
receivedMessageBodyTextStyle: receivedMessageBodyTextStyle,
|
||||
receivedMessageCaptionTextStyle: receivedMessageCaptionTextStyle,
|
||||
receivedMessageDocumentIconColor: receivedMessageDocumentIconColor,
|
||||
receivedMessageLinkDescriptionTextStyle:
|
||||
receivedMessageLinkDescriptionTextStyle,
|
||||
receivedMessageLinkTitleTextStyle: receivedMessageLinkTitleTextStyle,
|
||||
secondaryColor: secondaryColor,
|
||||
seenIcon: seenIcon,
|
||||
sendButtonIcon: sendButtonIcon,
|
||||
sendButtonMargin: sendButtonMargin,
|
||||
sendingIcon: sendingIcon,
|
||||
sentEmojiMessageTextStyle: sentEmojiMessageTextStyle,
|
||||
sentMessageBodyBoldTextStyle: sentMessageBodyBoldTextStyle,
|
||||
sentMessageBodyCodeTextStyle: sentMessageBodyCodeTextStyle,
|
||||
sentMessageBodyLinkTextStyle: sentMessageBodyLinkTextStyle,
|
||||
sentMessageBodyTextStyle: sentMessageBodyTextStyle,
|
||||
sentMessageCaptionTextStyle: sentMessageCaptionTextStyle,
|
||||
sentMessageDocumentIconColor: sentMessageDocumentIconColor,
|
||||
sentMessageLinkDescriptionTextStyle:
|
||||
sentMessageLinkDescriptionTextStyle,
|
||||
sentMessageLinkTitleTextStyle: sentMessageLinkTitleTextStyle,
|
||||
statusIconPadding: statusIconPadding,
|
||||
systemMessageTheme: systemMessageTheme,
|
||||
typingIndicatorTheme: typingIndicatorTheme,
|
||||
unreadHeaderTheme: unreadHeaderTheme,
|
||||
userAvatarImageBackgroundColor: userAvatarImageBackgroundColor,
|
||||
userAvatarNameColors: userAvatarNameColors,
|
||||
userAvatarTextStyle: userAvatarTextStyle,
|
||||
userNameTextStyle: userNameTextStyle,
|
||||
highlightMessageColor: highlightMessageColor,
|
||||
);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/// Icon for select attachment button.
|
||||
Widget? attachmentButtonIcon;
|
||||
|
||||
/// Margin of attachment button.
|
||||
EdgeInsets? attachmentButtonMargin;
|
||||
|
||||
/// Used as a background color of a chat widget.
|
||||
Color backgroundColor;
|
||||
|
||||
// Margin around the message bubble.
|
||||
EdgeInsetsGeometry? bubbleMargin;
|
||||
|
||||
/// Margin around date dividers.
|
||||
EdgeInsets dateDividerMargin;
|
||||
|
||||
/// Text style of the date dividers.
|
||||
TextStyle dateDividerTextStyle;
|
||||
|
||||
/// Icon for message's `delivered` status. For the best look use size of 16.
|
||||
Widget? deliveredIcon;
|
||||
|
||||
/// Icon inside file message.
|
||||
Widget? documentIcon;
|
||||
|
||||
/// Text style of the empty chat placeholder.
|
||||
TextStyle emptyChatPlaceholderTextStyle;
|
||||
|
||||
/// Color to indicate something bad happened (usually - shades of red).
|
||||
Color errorColor;
|
||||
|
||||
/// Icon for message's `error` status. For the best look use size of 16.
|
||||
Widget? errorIcon;
|
||||
|
||||
/// Color of the bottom bar where text field is.
|
||||
Color inputBackgroundColor;
|
||||
|
||||
/// Surface Tint Color of the bottom bar where text field is.
|
||||
Color inputSurfaceTintColor;
|
||||
|
||||
double inputElevation;
|
||||
|
||||
/// Top border radius of the bottom bar where text field is.
|
||||
BorderRadius inputBorderRadius;
|
||||
|
||||
/// Decoration of the container wrapping the text field.
|
||||
Decoration? inputContainerDecoration;
|
||||
|
||||
/// Outer insets of the bottom bar where text field is.
|
||||
EdgeInsets inputMargin;
|
||||
|
||||
/// Inner insets of the bottom bar where text field is.
|
||||
EdgeInsets inputPadding;
|
||||
|
||||
/// Color of the text field's text and attachment/send buttons.
|
||||
Color inputTextColor;
|
||||
|
||||
/// Color of the text field's cursor.
|
||||
Color? inputTextCursorColor;
|
||||
|
||||
/// Decoration of the input text field.
|
||||
InputDecoration inputTextDecoration;
|
||||
|
||||
/// Text style of the message input. To change the color use [inputTextColor].
|
||||
TextStyle inputTextStyle;
|
||||
|
||||
/// Border radius of message container.
|
||||
double messageBorderRadius;
|
||||
|
||||
/// Horizontal message bubble insets.
|
||||
double messageInsetsHorizontal;
|
||||
|
||||
/// Vertical message bubble insets.
|
||||
double messageInsetsVertical;
|
||||
|
||||
/// Message bubble max width. set to [double.infinity] adaptive screen.
|
||||
double messageMaxWidth;
|
||||
|
||||
/// Primary color of the chat used as a background of sent messages
|
||||
/// and statuses.
|
||||
Color primaryColor;
|
||||
|
||||
/// Text style used for displaying emojis on text messages.
|
||||
TextStyle receivedEmojiMessageTextStyle;
|
||||
|
||||
/// Body text style used for displaying bold text on received text messages.
|
||||
/// Default to a bold version of [receivedMessageBodyTextStyle].
|
||||
TextStyle? receivedMessageBodyBoldTextStyle;
|
||||
|
||||
/// Body text style used for displaying code text on received text messages.
|
||||
/// Defaults to a mono version of [receivedMessageBodyTextStyle].
|
||||
TextStyle? receivedMessageBodyCodeTextStyle;
|
||||
|
||||
/// Text style used for displaying link text on received text messages.
|
||||
/// Defaults to [receivedMessageBodyTextStyle].
|
||||
TextStyle? receivedMessageBodyLinkTextStyle;
|
||||
|
||||
/// Body text style used for displaying text on different types
|
||||
/// of received messages.
|
||||
TextStyle receivedMessageBodyTextStyle;
|
||||
|
||||
/// Caption text style used for displaying secondary info (e.g. file size) on
|
||||
/// different types of received messages.
|
||||
TextStyle receivedMessageCaptionTextStyle;
|
||||
|
||||
/// Color of the document icon on received messages. Has no effect when
|
||||
/// [documentIcon] is used.
|
||||
Color receivedMessageDocumentIconColor;
|
||||
|
||||
/// Text style used for displaying link description on received messages.
|
||||
TextStyle receivedMessageLinkDescriptionTextStyle;
|
||||
|
||||
/// Text style used for displaying link title on received messages.
|
||||
TextStyle receivedMessageLinkTitleTextStyle;
|
||||
|
||||
/// Secondary color, used as a background of received messages.
|
||||
Color secondaryColor;
|
||||
|
||||
/// Icon for message's `seen` status. For the best look use size of 16.
|
||||
Widget? seenIcon;
|
||||
|
||||
/// Icon for send button.
|
||||
Widget? sendButtonIcon;
|
||||
|
||||
/// Margin of send button.
|
||||
EdgeInsets? sendButtonMargin;
|
||||
|
||||
/// Icon for message's `sending` status. For the best look use size of 10.
|
||||
Widget? sendingIcon;
|
||||
|
||||
/// Text style used for displaying emojis on text messages.
|
||||
TextStyle sentEmojiMessageTextStyle;
|
||||
|
||||
/// Body text style used for displaying bold text on sent text messages.
|
||||
/// Defaults to a bold version of [sentMessageBodyTextStyle].
|
||||
TextStyle? sentMessageBodyBoldTextStyle;
|
||||
|
||||
/// Body text style used for displaying code text on sent text messages.
|
||||
/// Defaults to a mono version of [sentMessageBodyTextStyle].
|
||||
TextStyle? sentMessageBodyCodeTextStyle;
|
||||
|
||||
/// Text style used for displaying link text on sent text messages.
|
||||
/// Defaults to [sentMessageBodyTextStyle].
|
||||
TextStyle? sentMessageBodyLinkTextStyle;
|
||||
|
||||
/// Body text style used for displaying text on different types
|
||||
/// of sent messages.
|
||||
TextStyle sentMessageBodyTextStyle;
|
||||
|
||||
/// Caption text style used for displaying secondary info (e.g. file size) on
|
||||
/// different types of sent messages.
|
||||
TextStyle sentMessageCaptionTextStyle;
|
||||
|
||||
/// Color of the document icon on sent messages. Has no effect when
|
||||
/// [documentIcon] is used.
|
||||
Color sentMessageDocumentIconColor;
|
||||
|
||||
/// Text style used for displaying link description on sent messages.
|
||||
TextStyle sentMessageLinkDescriptionTextStyle;
|
||||
|
||||
/// Text style used for displaying link title on sent messages.
|
||||
TextStyle sentMessageLinkTitleTextStyle;
|
||||
|
||||
/// Padding around status icons.
|
||||
EdgeInsets statusIconPadding;
|
||||
|
||||
/// Theme for the system message. Will not have an effect if a custom builder
|
||||
/// is provided.
|
||||
SystemMessageTheme systemMessageTheme;
|
||||
|
||||
/// Theme for typing indicator. See [TypingIndicator].
|
||||
TypingIndicatorTheme typingIndicatorTheme;
|
||||
|
||||
/// Theme for the unread header.
|
||||
UnreadHeaderTheme unreadHeaderTheme;
|
||||
|
||||
/// Color used as a background for user avatar if an image is provided.
|
||||
/// Visible if the image has some transparent parts.
|
||||
Color userAvatarImageBackgroundColor;
|
||||
|
||||
/// Colors used as backgrounds for user avatars with no image and so,
|
||||
/// corresponding user names.
|
||||
/// Calculated based on a user ID, so unique across the whole app.
|
||||
List<Color> userAvatarNameColors;
|
||||
|
||||
/// Text style used for displaying initials on user avatar if no
|
||||
/// image is provided.
|
||||
TextStyle userAvatarTextStyle;
|
||||
|
||||
/// User names text style. Color will be overwritten
|
||||
/// with [userAvatarNameColors].
|
||||
TextStyle userNameTextStyle;
|
||||
|
||||
/// Color used as background of message row on highligth.
|
||||
Color? highlightMessageColor;
|
||||
}
|
||||
|
@ -3,7 +3,8 @@ enum IdentityException implements Exception {
|
||||
readError('identity could not be read'),
|
||||
noAccount('no account record info'),
|
||||
limitExceeded('too many items for the limit'),
|
||||
invalid('identity is corrupted or secret is invalid');
|
||||
invalid('identity is corrupted or secret is invalid'),
|
||||
cancelled('account operation cancelled');
|
||||
|
||||
const IdentityException(this.message);
|
||||
final String message;
|
||||
|
@ -68,12 +68,12 @@ class IdentityInstance with _$IdentityInstance {
|
||||
return cs;
|
||||
}
|
||||
|
||||
/// Read the account record info for a specific accountKey from the identity
|
||||
/// instance record using the identity instance secret key to decrypt
|
||||
/// Read the account record info for a specific applicationId from the
|
||||
/// identity instance record using the identity instance secret key to decrypt
|
||||
Future<List<AccountRecordInfo>> readAccount(
|
||||
{required TypedKey superRecordKey,
|
||||
required SecretKey secretKey,
|
||||
required String accountKey}) async {
|
||||
required String applicationId}) async {
|
||||
// Read the identity key to get the account keys
|
||||
final pool = DHTRecordPool.instance;
|
||||
|
||||
@ -91,7 +91,7 @@ class IdentityInstance with _$IdentityInstance {
|
||||
throw IdentityException.readError;
|
||||
}
|
||||
final accountRecords = IMapOfSets.from(identity.accountRecords);
|
||||
final vcAccounts = accountRecords.get(accountKey);
|
||||
final vcAccounts = accountRecords.get(applicationId);
|
||||
|
||||
accountRecordInfo = vcAccounts.toList();
|
||||
});
|
||||
@ -104,7 +104,7 @@ class IdentityInstance with _$IdentityInstance {
|
||||
Future<AccountRecordInfo> addAccount({
|
||||
required TypedKey superRecordKey,
|
||||
required SecretKey secretKey,
|
||||
required String accountKey,
|
||||
required String applicationId,
|
||||
required Future<Uint8List> Function(TypedKey parent) createAccountCallback,
|
||||
int maxAccounts = 1,
|
||||
}) async {
|
||||
@ -143,11 +143,12 @@ class IdentityInstance with _$IdentityInstance {
|
||||
}
|
||||
final oldAccountRecords = IMapOfSets.from(oldIdentity.accountRecords);
|
||||
|
||||
if (oldAccountRecords.get(accountKey).length >= maxAccounts) {
|
||||
if (oldAccountRecords.get(applicationId).length >= maxAccounts) {
|
||||
throw IdentityException.limitExceeded;
|
||||
}
|
||||
final accountRecords =
|
||||
oldAccountRecords.add(accountKey, newAccountRecordInfo).asIMap();
|
||||
final accountRecords = oldAccountRecords
|
||||
.add(applicationId, newAccountRecordInfo)
|
||||
.asIMap();
|
||||
return oldIdentity.copyWith(accountRecords: accountRecords);
|
||||
});
|
||||
|
||||
@ -156,6 +157,61 @@ class IdentityInstance with _$IdentityInstance {
|
||||
});
|
||||
}
|
||||
|
||||
/// Removes an Account associated with super identity from the identity
|
||||
/// instance record. 'removeAccountCallback' returns the account to be
|
||||
/// removed from the list passed to it.
|
||||
Future<bool> removeAccount({
|
||||
required TypedKey superRecordKey,
|
||||
required SecretKey secretKey,
|
||||
required String applicationId,
|
||||
required Future<AccountRecordInfo?> Function(
|
||||
List<AccountRecordInfo> accountRecordInfos)
|
||||
removeAccountCallback,
|
||||
}) async {
|
||||
final pool = DHTRecordPool.instance;
|
||||
|
||||
/////// Add account with profile to DHT
|
||||
|
||||
// Open identity key for writing
|
||||
veilidLoggy.debug('Opening identity record');
|
||||
return (await pool.openRecordWrite(recordKey, writer(secretKey),
|
||||
debugName: 'IdentityInstance::addAccount::IdentityRecord',
|
||||
parent: superRecordKey))
|
||||
.scope((identityRec) async {
|
||||
try {
|
||||
// Update identity key to remove account
|
||||
veilidLoggy.debug('Updating identity to remove account');
|
||||
await identityRec.eventualUpdateJson(Identity.fromJson,
|
||||
(oldIdentity) async {
|
||||
if (oldIdentity == null) {
|
||||
throw IdentityException.readError;
|
||||
}
|
||||
final oldAccountRecords = IMapOfSets.from(oldIdentity.accountRecords);
|
||||
|
||||
// Get list of accounts associated with the application
|
||||
final vcAccounts = oldAccountRecords.get(applicationId);
|
||||
final accountRecordInfos = vcAccounts.toList();
|
||||
|
||||
// Call the callback to return what account to remove
|
||||
final toRemove = await removeAccountCallback(accountRecordInfos);
|
||||
if (toRemove == null) {
|
||||
throw IdentityException.cancelled;
|
||||
}
|
||||
final newAccountRecords =
|
||||
oldAccountRecords.remove(applicationId, toRemove).asIMap();
|
||||
|
||||
return oldIdentity.copyWith(accountRecords: newAccountRecords);
|
||||
});
|
||||
} on IdentityException catch (e) {
|
||||
if (e == IdentityException.cancelled) {
|
||||
return false;
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Internal implementation
|
||||
|
||||
|
@ -15,12 +15,14 @@ class PersistentQueue<T extends GeneratedMessage>
|
||||
required String key,
|
||||
required T Function(Uint8List) fromBuffer,
|
||||
required Future<void> Function(IList<T>) closure,
|
||||
bool deleteOnClose = true})
|
||||
bool deleteOnClose = true,
|
||||
void Function(Object, StackTrace)? onError})
|
||||
: _table = table,
|
||||
_key = key,
|
||||
_fromBuffer = fromBuffer,
|
||||
_closure = closure,
|
||||
_deleteOnClose = deleteOnClose {
|
||||
_deleteOnClose = deleteOnClose,
|
||||
_onError = onError {
|
||||
_initWait.add(_init);
|
||||
}
|
||||
|
||||
@ -61,9 +63,17 @@ class PersistentQueue<T extends GeneratedMessage>
|
||||
}));
|
||||
|
||||
// Load the queue if we have one
|
||||
await _queueMutex.protect(() async {
|
||||
_queue = await load() ?? await store(IList<T>.empty());
|
||||
});
|
||||
try {
|
||||
await _queueMutex.protect(() async {
|
||||
_queue = await load() ?? await store(IList<T>.empty());
|
||||
});
|
||||
} on Exception catch (e, st) {
|
||||
if (_onError != null) {
|
||||
_onError(e, st);
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _updateQueueInner(IList<T> newQueue) async {
|
||||
@ -132,24 +142,32 @@ class PersistentQueue<T extends GeneratedMessage>
|
||||
// }
|
||||
|
||||
Future<void> _process() async {
|
||||
// Take a copy of the current queue
|
||||
// (doesn't need queue mutex because this is a sync operation)
|
||||
final toProcess = _queue;
|
||||
final processCount = toProcess.length;
|
||||
if (processCount == 0) {
|
||||
return;
|
||||
try {
|
||||
// Take a copy of the current queue
|
||||
// (doesn't need queue mutex because this is a sync operation)
|
||||
final toProcess = _queue;
|
||||
final processCount = toProcess.length;
|
||||
if (processCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Run the processing closure
|
||||
await _closure(toProcess);
|
||||
|
||||
// If there was no exception, remove the processed items
|
||||
await _queueMutex.protect(() async {
|
||||
// Get the queue from the state again as items could
|
||||
// have been added during processing
|
||||
final newQueue = _queue.skip(processCount).toIList();
|
||||
await _updateQueueInner(newQueue);
|
||||
});
|
||||
} on Exception catch (e, sp) {
|
||||
if (_onError != null) {
|
||||
_onError(e, sp);
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// Run the processing closure
|
||||
await _closure(toProcess);
|
||||
|
||||
// If there was no exception, remove the processed items
|
||||
await _queueMutex.protect(() async {
|
||||
// Get the queue from the state again as items could
|
||||
// have been added during processing
|
||||
final newQueue = _queue.skip(processCount).toIList();
|
||||
await _updateQueueInner(newQueue);
|
||||
});
|
||||
}
|
||||
|
||||
IList<T> get queue => _queue;
|
||||
@ -190,4 +208,5 @@ class PersistentQueue<T extends GeneratedMessage>
|
||||
final StreamController<Iterable<T>> _syncAddController = StreamController();
|
||||
final StreamController<void> _queueReady = StreamController();
|
||||
final Future<void> Function(IList<T>) _closure;
|
||||
final void Function(Object, StackTrace)? _onError;
|
||||
}
|
||||
|
@ -483,10 +483,10 @@ packages:
|
||||
description:
|
||||
path: "."
|
||||
ref: main
|
||||
resolved-ref: "6712d897e25041de38fb53ec06dc7a12cc6bff7d"
|
||||
resolved-ref: "0d8ac2fcafe24eba1adff9290a9ccd41f7718480"
|
||||
url: "https://gitlab.com/veilid/flutter-chat-ui.git"
|
||||
source: git
|
||||
version: "1.6.13"
|
||||
version: "1.6.14"
|
||||
flutter_form_builder:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
Loading…
Reference in New Issue
Block a user