Skip to content

Commit

Permalink
Merge pull request #2069 from gtalha07/edit-msg-jump-fix
Browse files Browse the repository at this point in the history
[Chat Input ] Edit message cursor jump fix
  • Loading branch information
gtalha07 authored Aug 19, 2024
2 parents 18ab10e + 91ff1bd commit 4b6452a
Show file tree
Hide file tree
Showing 11 changed files with 576 additions and 322 deletions.
2 changes: 2 additions & 0 deletions .changes/chat-input-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- fix bug where chat input cursor jumps to end of text in middle of edit.
- make chat send button more robust to respond to text composing state.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ enum SendingState {
@freezed
class ChatInputState with _$ChatInputState {
const factory ChatInputState({
@Default('') String message,
@Default(SelectedMessageState.none)
SelectedMessageState selectedMessageState,
@Default(SendingState.preparing) SendingState sendingState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ final _privateConstructorUsedError = UnsupportedError(

/// @nodoc
mixin _$ChatInputState {
String get message => throw _privateConstructorUsedError;
SelectedMessageState get selectedMessageState =>
throw _privateConstructorUsedError;
SendingState get sendingState => throw _privateConstructorUsedError;
Expand All @@ -37,8 +36,7 @@ abstract class $ChatInputStateCopyWith<$Res> {
_$ChatInputStateCopyWithImpl<$Res, ChatInputState>;
@useResult
$Res call(
{String message,
SelectedMessageState selectedMessageState,
{SelectedMessageState selectedMessageState,
SendingState sendingState,
bool emojiPickerVisible,
types.Message? selectedMessage,
Expand All @@ -59,7 +57,6 @@ class _$ChatInputStateCopyWithImpl<$Res, $Val extends ChatInputState>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? message = null,
Object? selectedMessageState = null,
Object? sendingState = null,
Object? emojiPickerVisible = null,
Expand All @@ -68,10 +65,6 @@ class _$ChatInputStateCopyWithImpl<$Res, $Val extends ChatInputState>
Object? editBtnVisible = null,
}) {
return _then(_value.copyWith(
message: null == message
? _value.message
: message // ignore: cast_nullable_to_non_nullable
as String,
selectedMessageState: null == selectedMessageState
? _value.selectedMessageState
: selectedMessageState // ignore: cast_nullable_to_non_nullable
Expand Down Expand Up @@ -109,8 +102,7 @@ abstract class _$$ChatInputStateImplCopyWith<$Res>
@override
@useResult
$Res call(
{String message,
SelectedMessageState selectedMessageState,
{SelectedMessageState selectedMessageState,
SendingState sendingState,
bool emojiPickerVisible,
types.Message? selectedMessage,
Expand All @@ -129,7 +121,6 @@ class __$$ChatInputStateImplCopyWithImpl<$Res>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? message = null,
Object? selectedMessageState = null,
Object? sendingState = null,
Object? emojiPickerVisible = null,
Expand All @@ -138,10 +129,6 @@ class __$$ChatInputStateImplCopyWithImpl<$Res>
Object? editBtnVisible = null,
}) {
return _then(_$ChatInputStateImpl(
message: null == message
? _value.message
: message // ignore: cast_nullable_to_non_nullable
as String,
selectedMessageState: null == selectedMessageState
? _value.selectedMessageState
: selectedMessageState // ignore: cast_nullable_to_non_nullable
Expand Down Expand Up @@ -174,18 +161,14 @@ class __$$ChatInputStateImplCopyWithImpl<$Res>
class _$ChatInputStateImpl implements _ChatInputState {
const _$ChatInputStateImpl(
{this.message = '',
this.selectedMessageState = SelectedMessageState.none,
{this.selectedMessageState = SelectedMessageState.none,
this.sendingState = SendingState.preparing,
this.emojiPickerVisible = false,
this.selectedMessage = null,
final Map<String, String> mentions = const {},
this.editBtnVisible = false})
: _mentions = mentions;

@override
@JsonKey()
final String message;
@override
@JsonKey()
final SelectedMessageState selectedMessageState;
Expand Down Expand Up @@ -213,15 +196,14 @@ class _$ChatInputStateImpl implements _ChatInputState {

@override
String toString() {
return 'ChatInputState(message: $message, selectedMessageState: $selectedMessageState, sendingState: $sendingState, emojiPickerVisible: $emojiPickerVisible, selectedMessage: $selectedMessage, mentions: $mentions, editBtnVisible: $editBtnVisible)';
return 'ChatInputState(selectedMessageState: $selectedMessageState, sendingState: $sendingState, emojiPickerVisible: $emojiPickerVisible, selectedMessage: $selectedMessage, mentions: $mentions, editBtnVisible: $editBtnVisible)';
}

@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ChatInputStateImpl &&
(identical(other.message, message) || other.message == message) &&
(identical(other.selectedMessageState, selectedMessageState) ||
other.selectedMessageState == selectedMessageState) &&
(identical(other.sendingState, sendingState) ||
Expand All @@ -238,7 +220,6 @@ class _$ChatInputStateImpl implements _ChatInputState {
@override
int get hashCode => Object.hash(
runtimeType,
message,
selectedMessageState,
sendingState,
emojiPickerVisible,
Expand All @@ -256,16 +237,13 @@ class _$ChatInputStateImpl implements _ChatInputState {

abstract class _ChatInputState implements ChatInputState {
const factory _ChatInputState(
{final String message,
final SelectedMessageState selectedMessageState,
{final SelectedMessageState selectedMessageState,
final SendingState sendingState,
final bool emojiPickerVisible,
final types.Message? selectedMessage,
final Map<String, String> mentions,
final bool editBtnVisible}) = _$ChatInputStateImpl;

@override
String get message;
@override
SelectedMessageState get selectedMessageState;
@override
Expand Down
39 changes: 0 additions & 39 deletions app/lib/features/chat/providers/notifiers/chat_input_notifier.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import 'package:acter/features/chat/utils.dart';
import 'package:acter/features/chat/models/chat_input_state/chat_input_state.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:html/parser.dart';

class ChatInputNotifier extends StateNotifier<ChatInputState> {
ChatInputNotifier() : super(const ChatInputState());
Expand All @@ -23,45 +21,10 @@ class ChatInputNotifier extends StateNotifier<ChatInputState> {
);
}

void updateMessage(String value) {
state = state.copyWith(message: value);
}

void setEditMessage(Message message) {
final Map<String, String> mentions = {};
String messageBodyText = '';

if (message is TextMessage) {
// Parse String Data to HTML document
final document = parse(message.text);

if (document.body != null) {
// Get message data
String msg = message.text.trim();

// Get list of 'A Tags' values
final aTagElementList = document.getElementsByTagName('a');

for (final aTagElement in aTagElementList) {
final userMentionMessageData =
parseUserMentionMessage(msg, aTagElement);
msg = userMentionMessageData.parsedMessage;

// Update mentions data
mentions[userMentionMessageData.displayName] =
userMentionMessageData.userName;
}

// Parse data
final messageDocument = parse(msg);
messageBodyText = messageDocument.body?.text ?? '';
}
}
state = state.copyWith(
selectedMessage: message,
selectedMessageState: SelectedMessageState.edit,
mentions: mentions,
message: messageBodyText,
);
}

Expand All @@ -83,7 +46,6 @@ class ChatInputNotifier extends StateNotifier<ChatInputState> {

void unsetSelectedMessage() {
state = state.copyWith(
message: '',
selectedMessage: null,
selectedMessageState: SelectedMessageState.none,
);
Expand All @@ -101,7 +63,6 @@ class ChatInputNotifier extends StateNotifier<ChatInputState> {
void messageSent() {
// reset the state;
state = state.copyWith(
message: '',
sendingState: SendingState.preparing,
selectedMessage: null,
selectedMessageState: SelectedMessageState.none,
Expand Down
27 changes: 27 additions & 0 deletions app/lib/features/chat/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:html/dom.dart' as html;
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:html/parser.dart';

//Check for mentioned user link
final mentionedUserLinkRegex = RegExp(
Expand Down Expand Up @@ -244,3 +245,29 @@ String prepareMsg(MsgContent? content) {
(match) => '<a href="${match.group(0)}">${match.group(0)}</a>',
);
}

String parseEditMsg(types.Message message) {
if (message is types.TextMessage) {
// Parse String Data to HTML document
final document = parse(message.text);

if (document.body != null) {
// Get message data
String msg = message.text.trim();

// Get list of 'A Tags' values
final aTagElementList = document.getElementsByTagName('a');

for (final aTagElement in aTagElementList) {
final userMentionMessageData =
parseUserMentionMessage(msg, aTagElement);
msg = userMentionMessageData.parsedMessage;
}

// Parse data
final messageDocument = parse(msg);
return messageDocument.body?.text ?? '';
}
}
return '';
}
77 changes: 43 additions & 34 deletions app/lib/features/chat/widgets/custom_input.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:acter/common/widgets/frost_effect.dart';
import 'package:acter/features/attachments/actions/select_attachment.dart';
import 'package:acter/features/chat/models/chat_input_state/chat_input_state.dart';
import 'package:acter/features/chat/providers/chat_providers.dart';
import 'package:acter/features/chat/utils.dart';
import 'package:acter/features/chat/widgets/custom_message_builder.dart';
import 'package:acter/features/chat/widgets/image_message_builder.dart';
import 'package:acter/features/chat/widgets/mention_profile_builder.dart';
Expand All @@ -31,12 +32,6 @@ import 'package:skeletonizer/skeletonizer.dart';

final _log = Logger('a3::chat::custom_input');

final _sendButtonVisible = StateProvider.family<bool, String>(
(ref, roomId) => ref.watch(
chatInputProvider.select((value) => value.message.isNotEmpty),
),
);

final _allowEdit = StateProvider.family<bool, String>(
(ref, roomId) => ref.watch(
chatInputProvider
Expand Down Expand Up @@ -199,13 +194,20 @@ class _ChatInput extends ConsumerStatefulWidget {
class __ChatInputState extends ConsumerState<_ChatInput> {
late ActerTriggerAutoCompleteTextController textController;
final FocusNode chatFocus = FocusNode();
final ValueNotifier<bool> _isInputEmptyNotifier = ValueNotifier(true);

@override
void didChangeDependencies() {
super.didChangeDependencies();
_setController();
}

@override
void dispose() {
_isInputEmptyNotifier.dispose();
super.dispose();
}

void _setController() {
final Map<String, TextStyle> triggerStyles = {
'@': TextStyle(
Expand All @@ -220,9 +222,15 @@ class __ChatInputState extends ConsumerState<_ChatInput> {
};
textController =
ActerTriggerAutoCompleteTextController(triggerStyles: triggerStyles);
textController.addListener(_updateInputState);
setState(() {});
}

// listener for handling send state
void _updateInputState() {
_isInputEmptyNotifier.value = textController.text.trim().isEmpty;
}

void handleEmojiSelected(Category? category, Emoji emoji) {
// Get cursor current position
var cursorPos = textController.selection.base.offset;
Expand Down Expand Up @@ -289,7 +297,6 @@ class __ChatInputState extends ConsumerState<_ChatInput> {
final roomId = widget.roomId;
final isEncrypted =
ref.watch(isRoomEncryptedProvider(roomId)).valueOrNull ?? false;

return Column(
children: [
if (child != null) child,
Expand Down Expand Up @@ -331,8 +338,14 @@ class __ChatInputState extends ConsumerState<_ChatInput> {
),
),
),
if (ref.watch(_sendButtonVisible(roomId)))
renderSendButton(context, roomId),
ValueListenableBuilder<bool>(
valueListenable: _isInputEmptyNotifier,
builder: (context, isEmpty, child) {
return !isEmpty
? renderSendButton(context, roomId)
: const SizedBox();
},
),
],
),
),
Expand Down Expand Up @@ -688,10 +701,27 @@ class _TextInputWidgetConsumerState extends ConsumerState<_TextInputWidget> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.controller.text = ref.read(
chatInputProvider.select((value) => value.message),
);
ref.listenManual(chatInputProvider, (prev, next) {
if (next.selectedMessageState == SelectedMessageState.edit &&
(prev?.selectedMessageState != next.selectedMessageState ||
next.selectedMessage != prev?.selectedMessage)) {
// a new message has been selected to be edited or switched from reply
// to edit, force refresh the inner text controller to reflect that
if (next.selectedMessage != null) {
widget.controller.text = parseEditMsg(next.selectedMessage!);
// frame delay to keep focus connected with keyboard.
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.chatFocus.requestFocus();
});
}
} else if (next.selectedMessageState == SelectedMessageState.replyTo &&
(next.selectedMessage != prev?.selectedMessage ||
prev?.selectedMessageState != next.selectedMessageState)) {
// frame delay to keep focus connected with keyboard..
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.chatFocus.requestFocus();
});
}
});
}

Expand Down Expand Up @@ -736,26 +766,6 @@ class _TextInputWidgetConsumerState extends ConsumerState<_TextInputWidget> {

@override
Widget build(BuildContext context) {
ref.listen(chatInputProvider, (prev, next) {
if (next.selectedMessageState == SelectedMessageState.edit &&
(prev?.selectedMessageState != next.selectedMessageState ||
next.message != prev?.message)) {
// a new message has been selected to be edited or switched from reply
// to edit, force refresh the inner text controller to reflect that
widget.controller.text = next.message;
// frame delay to keep focus connected with keyboard.
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.chatFocus.requestFocus();
});
} else if (next.selectedMessageState == SelectedMessageState.replyTo &&
(next.selectedMessage != prev?.selectedMessage ||
prev?.selectedMessageState != next.selectedMessageState)) {
// frame delay to keep focus connected with keyboard..
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.chatFocus.requestFocus();
});
}
});
return CallbackShortcuts(
bindings: <ShortcutActivator, VoidCallback>{
const SingleActivator(LogicalKeyboardKey.enter): () {
Expand Down Expand Up @@ -809,7 +819,6 @@ class _TextInputWidgetConsumerState extends ConsumerState<_TextInputWidget> {
focusNode: chatFocus,
enabled: ref.watch(_allowEdit(widget.roomId)),
onChanged: (val) {
ref.read(chatInputProvider.notifier).updateMessage(val);
if (widget.onTyping != null) {
widget.onTyping!(val.isNotEmpty);
}
Expand Down
Loading

0 comments on commit 4b6452a

Please sign in to comment.