feat: suporte a tabelas markdown nas legendas, remoção de borda de imagens e separação de comentários no mural
Some checks failed
Main Deploy Workflow / deploy_web (push) Has been cancelled
Main Deploy Workflow / deploy_playstore_internal (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2026-09-15 17:24:18 -03:00
parent 43e0dc177c
commit ab8637bf6a
6 changed files with 157 additions and 32 deletions

View File

@@ -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",

View File

@@ -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<ChatPageWithRoom>
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<ChatPageWithRoom>
inputFocus.requestFocus();
}
Future<void> _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<String, Object?>.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);
}

View File

@@ -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<String?> showEditMediaCaptionDialog(
BuildContext context, {
required String initialCaption,
}) => showDialog<String>(
context: context,
builder: (context) => EditMediaCaptionDialog(initialCaption: initialCaption),
);
class EditMediaCaptionDialog extends StatefulWidget {
final String initialCaption;
const EditMediaCaptionDialog({super.key, required this.initialCaption});
@override
State<EditMediaCaptionDialog> createState() => _EditMediaCaptionDialogState();
}
class _EditMediaCaptionDialogState extends State<EditMediaCaptionDialog> {
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),
),
],
);
}
}

View File

@@ -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,

View File

@@ -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<SendFileDialog> {
? null
: () {
final extra = <String, Object>{'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;

View File

@@ -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,
),
],
],
],
),
);