feat: marcar legenda com markdown, barra de ações nos posts do canal e limites de mensagem na modal
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-14 19:18:09 -03:00
parent 9d69e833bd
commit 43e0dc177c
12 changed files with 244 additions and 810 deletions

View File

@@ -8,11 +8,11 @@ import 'package:tatchat/config/setting_keys.dart';
import 'package:tatchat/utils/file_description.dart';
import 'package:tatchat/utils/url_launcher.dart';
import 'package:tatchat/widgets/mxc_image.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:material_ui/material_ui.dart';
import 'package:matrix/matrix.dart';
import '../../../widgets/blur_hash.dart';
import 'html_message.dart';
class ImageBubble extends StatelessWidget {
final Event event;
@@ -121,14 +121,13 @@ class ImageBubble extends StatelessWidget {
width: width,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Linkify(
text: fileDescription,
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
style: TextStyle(
color: textColor,
fontSize: AppConfig.messageFontSize,
),
options: const LinkifyOptions(humanize: false),
child: HtmlMessage(
html: event.isRichMessage
? event.formattedText
: fileDescription,
room: event.room,
textColor: textColor,
fontSize: AppConfig.messageFontSize,
linkStyle: TextStyle(
color: linkColor,
fontSize: AppConfig.messageFontSize,

View File

@@ -14,10 +14,12 @@ import 'package:tatchat/utils/adaptive_bottom_sheet.dart';
import 'package:tatchat/utils/date_time_extension.dart';
import 'package:tatchat/utils/file_description.dart';
import 'package:tatchat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:tatchat/utils/show_scaffold_dialog.dart';
import 'package:tatchat/utils/string_color.dart';
import 'package:tatchat/widgets/avatar.dart';
import 'package:tatchat/widgets/matrix.dart';
import 'package:tatchat/widgets/member_actions_popup_menu_button.dart';
import 'package:tatchat/widgets/share_scaffold_dialog.dart';
import 'package:flutter/services.dart';
import 'package:material_ui/material_ui.dart';
import 'package:matrix/matrix.dart';
@@ -247,8 +249,8 @@ class Message extends StatelessWidget {
child: Center(child: Icon(Icons.check_outlined)),
),
direction: AppSettings.swipeRightToLeftToReply.value
? SwipeDirection.endToStart
: SwipeDirection.startToEnd,
? SwipeDirection.startToEnd
: SwipeDirection.endToStart,
onSwipe: (_) => onSwipe(),
child: Container(
constraints: card
@@ -683,7 +685,7 @@ class Message extends StatelessWidget {
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
alignment: Alignment.bottomCenter,
child: !hasReactions
child: card || !hasReactions
? const SizedBox.shrink()
: Container(
alignment: card
@@ -699,6 +701,17 @@ class Message extends StatelessWidget {
child: MessageReactions(event, timeline),
),
),
if (card)
_PostActionBar(
event: event,
timeline: timeline,
hasReactions: hasReactions,
threadCount: threadChildren.length,
onAddReaction: () => onSelect(event),
onOpenThread: enterThread == null
? null
: () => enterThread(event.eventId),
),
Row(
mainAxisAlignment: card
? .start
@@ -990,7 +1003,7 @@ class Message extends StatelessWidget {
),
],
),
if (enterThread != null)
if (enterThread != null && !card)
AnimatedSize(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
@@ -1177,3 +1190,115 @@ class __AnimateInState extends State<_AnimateIn> {
);
}
}
class _PostActionBar extends StatelessWidget {
final Event event;
final Timeline timeline;
final bool hasReactions;
final int threadCount;
final VoidCallback? onAddReaction;
final VoidCallback? onOpenThread;
const _PostActionBar({
required this.event,
required this.timeline,
required this.hasReactions,
required this.threadCount,
required this.onAddReaction,
required this.onOpenThread,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.surfaceContainerHighest.withAlpha(150),
border: Border(
top: BorderSide(
color: Theme.of(context).colorScheme.outlineVariant.withAlpha(120),
),
),
),
child: Row(
children: [
_BarAction(
icon: Icons.visibility_outlined,
label: '${event.receipts.length}',
),
Expanded(
child: Row(
children: [
if (hasReactions)
Flexible(
child: Align(
alignment: Alignment.centerLeft,
child: MessageReactions(event, timeline),
),
),
_BarAction(
icon: Icons.add_reaction_outlined,
onPressed: onAddReaction,
),
],
),
),
_BarAction(
icon: Icons.chat_bubble_outline,
label: '$threadCount',
onPressed: onOpenThread,
),
_BarAction(
icon: Icons.share_outlined,
onPressed: () {
showScaffoldDialog(
context: context,
builder: (context) => ShareScaffoldDialog(
items: [ContentShareItem(event.content.copy())],
),
);
},
),
],
),
);
}
}
class _BarAction extends StatelessWidget {
final IconData icon;
final String? label;
final VoidCallback? onPressed;
const _BarAction({required this.icon, this.label, this.onPressed});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 18, color: theme.colorScheme.onSurfaceVariant),
if (label != null) ...[
const SizedBox(width: 4),
Text(
label!,
style: TextStyle(
color: theme.colorScheme.onSurfaceVariant,
fontSize: 12,
),
),
],
],
),
),
);
}
}

View File

@@ -78,29 +78,32 @@ class MessageContent extends StatelessWidget {
: BoxFit.cover;
if (w != null && h != null) {
fit = BoxFit.contain;
if (fullWidthCard &&
event.messageType == MessageTypes.Image &&
w > 0 &&
h > 0) {
return AspectRatio(
aspectRatio: w / h,
child: ImageBubble(
event,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
borderRadius: borderRadius,
timeline: timeline,
textColor: textColor,
onTap: () => showDialog(
context: context,
builder: (_) => ImageViewer(
event,
timeline: timeline,
outerContext: context,
if (fullWidthCard && event.messageType == MessageTypes.Image) {
return LayoutBuilder(
builder: (context, constraints) {
final maxWidth = constraints.maxWidth;
final cardWidth = maxWidth.isFinite && maxWidth > 0
? maxWidth
: maxSize;
final hasKnownRatio = w > 0 && h > 0;
return ImageBubble(
event,
width: cardWidth,
height: hasKnownRatio ? cardWidth * h / w : maxSize,
fit: hasKnownRatio ? BoxFit.cover : BoxFit.contain,
borderRadius: borderRadius,
timeline: timeline,
textColor: textColor,
onTap: () => showDialog(
context: context,
builder: (_) => ImageViewer(
event,
timeline: timeline,
outerContext: context,
),
),
),
),
);
},
);
}
if (w > h) {

View File

@@ -13,11 +13,11 @@ import 'package:tatchat/utils/platform_infos.dart';
import 'package:tatchat/utils/url_launcher.dart';
import 'package:tatchat/widgets/blur_hash.dart';
import 'package:tatchat/widgets/mxc_image.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:material_ui/material_ui.dart';
import 'package:matrix/matrix.dart';
import '../../image_viewer/image_viewer.dart';
import 'html_message.dart';
class EventVideoPlayer extends StatelessWidget {
final Event event;
@@ -151,14 +151,13 @@ class EventVideoPlayer extends StatelessWidget {
width: width,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Linkify(
text: fileDescription,
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
style: TextStyle(
color: textColor,
fontSize: AppConfig.messageFontSize,
),
options: const LinkifyOptions(humanize: false),
child: HtmlMessage(
html: event.isRichMessage
? event.formattedText
: fileDescription,
room: event.room,
textColor: textColor!,
fontSize: AppConfig.messageFontSize,
linkStyle: TextStyle(
color: linkColor,
fontSize: AppConfig.messageFontSize,