From ab8637bf6a52cdea964301a7554b7e81a87d7405 Mon Sep 17 00:00:00 2001 From: Welton Moura Date: Tue, 15 Sep 2026 17:24:18 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20suporte=20a=20tabelas=20markdown=20nas?= =?UTF-8?q?=20legendas,=20remo=C3=A7=C3=A3o=20de=20borda=20de=20imagens=20?= =?UTF-8?q?e=20separa=C3=A7=C3=A3o=20de=20coment=C3=A1rios=20no=20mural?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/l10n/intl_pt_BR.arb | 2 +- lib/pages/chat/chat.dart | 40 +++++++++ lib/pages/chat/edit_media_caption_dialog.dart | 86 +++++++++++++++++++ lib/pages/chat/events/image_bubble.dart | 11 +-- lib/pages/chat/send_file_dialog.dart | 7 +- lib/pages/mural/mural_chat_page.dart | 43 ++++++---- 6 files changed, 157 insertions(+), 32 deletions(-) create mode 100644 lib/pages/chat/edit_media_caption_dialog.dart diff --git a/lib/l10n/intl_pt_BR.arb b/lib/l10n/intl_pt_BR.arb index 15695ab..4e9db76 100644 --- a/lib/l10n/intl_pt_BR.arb +++ b/lib/l10n/intl_pt_BR.arb @@ -1986,7 +1986,7 @@ "blockedUsers": "Usuários bloqueados", "passwordIsWrong": "A senha digitada está incorreta", "pleaseEnterYourCurrentPassword": "Digite sua senha atual", - "groupCanBeFoundViaSearch": "Grupos podem ser encontrados por pesquisa", + "groupCanBeFoundViaSearch": "Sala pode ser encontrada na pesquisa", "noUsersFoundWithQuery": "Infelizmente, não foi encontrado usuário com \"{query}\". Verifique se digitou corretamente.", "@noUsersFoundWithQuery": { "type": "String", diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 1ad454e..affdf88 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -10,11 +10,14 @@ import 'package:collection/collection.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:html/parser.dart' show parseFragment; +import 'package:markdown/markdown.dart' show ExtensionSet, markdownToHtml; import 'package:tatchat/config/setting_keys.dart'; import 'package:tatchat/config/themes.dart'; import 'package:tatchat/l10n/l10n.dart'; import 'package:tatchat/pages/chat/chat_view.dart'; import 'package:tatchat/pages/chat/event_info_dialog.dart'; +import 'package:tatchat/pages/chat/edit_media_caption_dialog.dart'; import 'package:tatchat/pages/chat/start_poll_bottom_sheet.dart'; import 'package:tatchat/pages/chat/trust_user_key_dialog.dart'; import 'package:tatchat/pages/chat/utils/web_file_to_x_file.dart'; @@ -1314,6 +1317,16 @@ class ChatController extends State final timeline = this.timeline; if (timeline == null) return; + if (const { + MessageTypes.Image, + MessageTypes.Video, + MessageTypes.Audio, + MessageTypes.File, + }.contains(event.messageType)) { + _startEditingMediaCaption(event, clearSelection: clearSelection); + return; + } + final client = currentRoomBundle.firstWhere( (c) => c?.userID == event.senderId, orElse: () => null, @@ -1336,6 +1349,33 @@ class ChatController extends State inputFocus.requestFocus(); } + Future _startEditingMediaCaption( + Event event, { + bool clearSelection = false, + }) async { + final timeline = this.timeline; + if (timeline == null) return; + if (clearSelection) setState(() => selectedEvents.clear()); + final caption = event.getDisplayEvent(timeline).body; + final result = await showEditMediaCaptionDialog( + context, + initialCaption: caption, + ); + if (result == null) return; + final html = markdownToHtml( + result, + extensionSet: ExtensionSet.gitHubFlavored, + ); + final content = Map.of(event.content) + ..remove('m.relates_to') + ..['body'] = result; + if (parseFragment(html).text != result) { + content['format'] = 'org.matrix.custom.html'; + content['formatted_body'] = html; + } + await room.sendEvent(content, editEventId: event.eventId); + } + void editSelectedEventAction() { _startEditingEvent(selectedEvents.first, clearSelection: true); } diff --git a/lib/pages/chat/edit_media_caption_dialog.dart b/lib/pages/chat/edit_media_caption_dialog.dart new file mode 100644 index 0000000..773613c --- /dev/null +++ b/lib/pages/chat/edit_media_caption_dialog.dart @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2019-Present Christian Kußowski +// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +import 'package:material_ui/material_ui.dart'; +import 'package:tatchat/config/setting_keys.dart'; +import 'package:tatchat/l10n/l10n.dart'; +import 'package:tatchat/utils/platform_infos.dart'; +import 'package:tatchat/widgets/adaptive_dialogs/adaptive_dialog_action.dart'; +import 'package:tatchat/widgets/adaptive_dialogs/dialog_text_field.dart'; + +Future showEditMediaCaptionDialog( + BuildContext context, { + required String initialCaption, +}) => showDialog( + context: context, + builder: (context) => EditMediaCaptionDialog(initialCaption: initialCaption), +); + +class EditMediaCaptionDialog extends StatefulWidget { + final String initialCaption; + + const EditMediaCaptionDialog({super.key, required this.initialCaption}); + + @override + State createState() => _EditMediaCaptionDialogState(); +} + +class _EditMediaCaptionDialogState extends State { + late final TextEditingController _controller = TextEditingController( + text: widget.initialCaption, + ); + late final FocusNode _focusNode = FocusNode(); + + @override + void dispose() { + _focusNode.dispose(); + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final isMobile = PlatformInfos.isMobile; + final l10n = L10n.of(context); + return AlertDialog.adaptive( + title: Text(l10n.editCaption), + insetPadding: isMobile + ? const EdgeInsets.all(8.0) + : const EdgeInsets.symmetric(horizontal: 40, vertical: 24), + constraints: isMobile + ? const BoxConstraints( + maxWidth: double.infinity, + maxHeight: double.infinity, + ) + : null, + content: SizedBox( + width: 512, + height: isMobile ? MediaQuery.sizeOf(context).height * 0.7 : null, + child: DialogTextField( + controller: _controller, + focusNode: _focusNode, + hintText: l10n.captionHint, + labelText: l10n.optionalMessage, + keyboardType: TextInputType.multiline, + minLines: isMobile ? 8 : 3, + maxLines: isMobile ? null : 8, + maxLength: AppSettings.textMessageMaxLength.value, + counterText: '', + ), + ), + actions: [ + AdaptiveDialogAction( + onPressed: () => Navigator.of(context).pop(), + child: Text(l10n.cancel), + ), + AdaptiveDialogAction( + autofocus: true, + onPressed: () => Navigator.of(context).pop(_controller.text.trim()), + child: Text(l10n.save), + ), + ], + ); + } +} diff --git a/lib/pages/chat/events/image_bubble.dart b/lib/pages/chat/events/image_bubble.dart index 42b7b24..db2b23f 100644 --- a/lib/pages/chat/events/image_bubble.dart +++ b/lib/pages/chat/events/image_bubble.dart @@ -48,8 +48,6 @@ class ImageBubble extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - var borderRadius = this.borderRadius ?? BorderRadius.circular(AppConfig.borderRadius); @@ -70,14 +68,7 @@ class ImageBubble extends StatelessWidget { Material( color: Colors.transparent, clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: borderRadius, - side: BorderSide( - color: event.messageType == MessageTypes.Sticker - ? Colors.transparent - : theme.dividerColor, - ), - ), + shape: RoundedRectangleBorder(borderRadius: borderRadius), child: InkWell( onTap: onTap, borderRadius: borderRadius, diff --git a/lib/pages/chat/send_file_dialog.dart b/lib/pages/chat/send_file_dialog.dart index 9d62204..426c59d 100644 --- a/lib/pages/chat/send_file_dialog.dart +++ b/lib/pages/chat/send_file_dialog.dart @@ -7,7 +7,7 @@ import 'package:async/async.dart' show Result; import 'package:cross_file/cross_file.dart'; import 'package:cupertino_ui/cupertino_ui.dart'; import 'package:html/parser.dart' show parseFragment; -import 'package:markdown/markdown.dart' show markdownToHtml; +import 'package:markdown/markdown.dart' show ExtensionSet, markdownToHtml; import 'package:tatchat/config/app_config.dart'; import 'package:tatchat/config/setting_keys.dart'; import 'package:tatchat/l10n/l10n.dart'; @@ -133,7 +133,10 @@ class SendFileDialogState extends State { ? null : () { final extra = {'body': label}; - final html = markdownToHtml(label); + final html = markdownToHtml( + label, + extensionSet: ExtensionSet.gitHubFlavored, + ); if (parseFragment(html).text != label) { extra['format'] = 'org.matrix.custom.html'; extra['formatted_body'] = html; diff --git a/lib/pages/mural/mural_chat_page.dart b/lib/pages/mural/mural_chat_page.dart index 6d09018..415aeeb 100644 --- a/lib/pages/mural/mural_chat_page.dart +++ b/lib/pages/mural/mural_chat_page.dart @@ -1280,10 +1280,11 @@ class _PostViewerState extends State<_PostViewer> { ), ), ) - else - ...commentTree.map( - (node) => _CommentTile( - node: node, + else ...[ + for (var i = 0; i < commentTree.length; i++) ...[ + if (i > 0) const Divider(height: 1), + _CommentTile( + node: commentTree[i], depth: 0, timeline: widget.timeline, currentUserId: widget.room.client.userID, @@ -1295,7 +1296,8 @@ class _PostViewerState extends State<_PostViewer> { onDelete: _deleteComment, onReport: _reportComment, ), - ), + ], + ], ], ), ), @@ -1598,20 +1600,23 @@ class _CommentTile extends StatelessWidget { ), ], ), - ...node.replies.map( - (reply) => _CommentTile( - node: reply, - depth: depth + 1, - timeline: timeline, - currentUserId: currentUserId, - onReply: onReply, - onToggleReaction: onToggleReaction, - onShowReactionPicker: onShowReactionPicker, - onOpenProfile: onOpenProfile, - onDelete: onDelete, - onReport: onReport, - ), - ), + ...[ + for (var i = 0; i < node.replies.length; i++) ...[ + if (i > 0) const Divider(height: 1), + _CommentTile( + node: node.replies[i], + depth: depth + 1, + timeline: timeline, + currentUserId: currentUserId, + onReply: onReply, + onToggleReaction: onToggleReaction, + onShowReactionPicker: onShowReactionPicker, + onOpenProfile: onOpenProfile, + onDelete: onDelete, + onReport: onReport, + ), + ], + ], ], ), );