Skip to content

Commit

Permalink
load and save composer draft for chat room
Browse files Browse the repository at this point in the history
  • Loading branch information
gtalha07 committed Aug 19, 2024
1 parent 4b6452a commit 3ad62c6
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 10 deletions.
9 changes: 9 additions & 0 deletions app/lib/features/chat/providers/chat_providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ final chatStateProvider =
(ref, roomId) => ChatRoomNotifier(ref: ref, roomId: roomId),
);

final chatComposerDraftProvider = FutureProvider.autoDispose
.family<ComposeDraft?, String>((ref, roomId) async {
final chat = await ref.watch(chatProvider(roomId).future);
if (chat == null) {
return null;
}
return (await chat.msgDraft().then((val) => val.draft()));
});

final chatTopic =
FutureProvider.autoDispose.family<String?, String>((ref, roomId) async {
final c = await ref.watch(chatProvider(roomId).future);
Expand Down
98 changes: 88 additions & 10 deletions app/lib/features/chat/widgets/custom_input.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'dart:async';
import 'dart:io';

import 'package:acter/common/models/types.dart';
import 'package:acter/common/providers/chat_providers.dart';
import 'package:acter/common/providers/room_providers.dart';
import 'package:acter/common/themes/app_theme.dart';
import 'package:acter/common/widgets/emoji_picker_widget.dart';
Expand Down Expand Up @@ -192,18 +194,33 @@ class _ChatInput extends ConsumerStatefulWidget {
}

class __ChatInputState extends ConsumerState<_ChatInput> {
late ActerTriggerAutoCompleteTextController textController;
ActerTriggerAutoCompleteTextController textController =
ActerTriggerAutoCompleteTextController();
final FocusNode chatFocus = FocusNode();
final ValueNotifier<bool> _isInputEmptyNotifier = ValueNotifier(true);

@override
void didChangeDependencies() {
super.didChangeDependencies();
_setController();
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_setController();
loadDraft();
});
}

// @override
// void didUpdateWidget(covariant _ChatInput oldWidget) {
// super.didUpdateWidget(oldWidget);
// // useful lifecycle method for desktop UI where room selection is done in side-view.
// // re-builds when roomId update occurs and loads the room composer draft state.
// if (widget.roomId != oldWidget.roomId) {
// loadDraft();
// }
// }

@override
void dispose() {
textController.dispose();
_isInputEmptyNotifier.dispose();
super.dispose();
}
Expand All @@ -223,7 +240,30 @@ class __ChatInputState extends ConsumerState<_ChatInput> {
textController =
ActerTriggerAutoCompleteTextController(triggerStyles: triggerStyles);
textController.addListener(_updateInputState);
setState(() {});
}

// composer draft load state handler
Future<void> loadDraft() async {
final draft =
await ref.read(chatComposerDraftProvider(widget.roomId).future);
if (draft != null) {
if (draft.eventId() != null) {
final eventId = draft.eventId()!;
final draftType = draft.draftType();
final inputNotifier = ref.read(chatInputProvider.notifier);
inputNotifier.unsetSelectedMessage();
final m = ref
.read(chatMessagesProvider(widget.roomId))
.firstWhere((x) => x.id == eventId);
if (draftType == 'edit') {
inputNotifier.setEditMessage(m);
} else if (draftType == 'reply') {
inputNotifier.setReplyToMessage(m);
}
}
textController.text = draft.htmlText() ?? draft.plainText();
_log.info('compose draft loaded for room: ${widget.roomId}');
}
}

// listener for handling send state
Expand Down Expand Up @@ -698,6 +738,7 @@ class _TextInputWidget extends ConsumerStatefulWidget {
}

class _TextInputWidgetConsumerState extends ConsumerState<_TextInputWidget> {
Timer? _debounceTimer;
@override
void initState() {
super.initState();
Expand All @@ -709,6 +750,8 @@ class _TextInputWidgetConsumerState extends ConsumerState<_TextInputWidget> {
// to edit, force refresh the inner text controller to reflect that
if (next.selectedMessage != null) {
widget.controller.text = parseEditMsg(next.selectedMessage!);
// save edit UI state also by pointing reference event id.
saveDraft(widget.controller.text, next.selectedMessage!.id);
// frame delay to keep focus connected with keyboard.
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.chatFocus.requestFocus();
Expand All @@ -717,6 +760,8 @@ class _TextInputWidgetConsumerState extends ConsumerState<_TextInputWidget> {
} else if (next.selectedMessageState == SelectedMessageState.replyTo &&
(next.selectedMessage != prev?.selectedMessage ||
prev?.selectedMessageState != next.selectedMessageState)) {
// save reply UI state also by pointing reference event id.
saveDraft(widget.controller.text, next.selectedMessage!.id);
// frame delay to keep focus connected with keyboard..
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.chatFocus.requestFocus();
Expand Down Expand Up @@ -764,6 +809,43 @@ class _TextInputWidgetConsumerState extends ConsumerState<_TextInputWidget> {
}
}

// save composer draft object handler
Future<void> saveDraft(String text, String? eventId) async {
// get the convo object to initiate draft
final chat = await ref.read(chatProvider(widget.roomId).future);
bool res = false;
if (chat != null) {
if (eventId != null) {
final selectedMessageState =
ref.read(chatInputProvider).selectedMessageState;
if (selectedMessageState == SelectedMessageState.edit) {
/// FIXME: how html can be passed down here?
res = await chat.saveMsgDraft(text, text, 'edit', eventId);
} else if (selectedMessageState == SelectedMessageState.replyTo) {
/// FIXME: how html can be passed down here?
res = await chat.saveMsgDraft(text, text, 'reply', eventId);
}
}
res = await chat.saveMsgDraft(text, text, 'new', null);
_log.info('compose message state stored for room? $res|${widget.roomId}');
}
}

// chat text field onChanged callback
void _onTextChanged(String text) {
// send typing notice
if (widget.onTyping != null) {
widget.onTyping!(text.isNotEmpty);
}

// Debounce the save operation to avoid excessive writes
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(milliseconds: 300), () {
// save composing draft
saveDraft(text, ref.read(chatInputProvider).selectedMessage?.id);
});
}

@override
Widget build(BuildContext context) {
return CallbackShortcuts(
Expand Down Expand Up @@ -818,11 +900,7 @@ class _TextInputWidgetConsumerState extends ConsumerState<_TextInputWidget> {
controller: widget.controller,
focusNode: chatFocus,
enabled: ref.watch(_allowEdit(widget.roomId)),
onChanged: (val) {
if (widget.onTyping != null) {
widget.onTyping!(val.isNotEmpty);
}
},
onChanged: _onTextChanged,
onSubmitted: (_) => widget.onSendButtonPressed(),
style: Theme.of(context).textTheme.bodySmall,
decoration: InputDecoration(
Expand Down

0 comments on commit 3ad62c6

Please sign in to comment.