largura imagem mural, comentarios
This commit is contained in:
@@ -16,6 +16,7 @@ import 'package:tatchat/pages/chat/sticker_picker_dialog.dart';
|
|||||||
import 'package:tatchat/pages/image_viewer/video_player.dart';
|
import 'package:tatchat/pages/image_viewer/video_player.dart';
|
||||||
import 'package:tatchat/utils/adaptive_bottom_sheet.dart';
|
import 'package:tatchat/utils/adaptive_bottom_sheet.dart';
|
||||||
import 'package:tatchat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
import 'package:tatchat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||||
|
import 'package:tatchat/utils/show_scaffold_dialog.dart';
|
||||||
import 'package:tatchat/utils/stream_extension.dart';
|
import 'package:tatchat/utils/stream_extension.dart';
|
||||||
import 'package:tatchat/utils/url_launcher.dart';
|
import 'package:tatchat/utils/url_launcher.dart';
|
||||||
import 'package:tatchat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
|
import 'package:tatchat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
|
||||||
@@ -24,6 +25,7 @@ import 'package:tatchat/widgets/adaptive_dialogs/user_dialog.dart';
|
|||||||
import 'package:tatchat/widgets/avatar.dart';
|
import 'package:tatchat/widgets/avatar.dart';
|
||||||
import 'package:tatchat/widgets/matrix.dart';
|
import 'package:tatchat/widgets/matrix.dart';
|
||||||
import 'package:tatchat/widgets/mxc_image.dart';
|
import 'package:tatchat/widgets/mxc_image.dart';
|
||||||
|
import 'package:tatchat/widgets/share_scaffold_dialog.dart';
|
||||||
|
|
||||||
const _muralMediaTypes = {
|
const _muralMediaTypes = {
|
||||||
MessageTypes.Image,
|
MessageTypes.Image,
|
||||||
@@ -654,9 +656,16 @@ class _PostViewer extends StatefulWidget {
|
|||||||
State<_PostViewer> createState() => _PostViewerState();
|
State<_PostViewer> createState() => _PostViewerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _CommentNode {
|
||||||
|
final Event event;
|
||||||
|
final List<_CommentNode> replies;
|
||||||
|
_CommentNode(this.event) : replies = [];
|
||||||
|
}
|
||||||
|
|
||||||
class _PostViewerState extends State<_PostViewer> {
|
class _PostViewerState extends State<_PostViewer> {
|
||||||
final TextEditingController _commentController = TextEditingController();
|
final TextEditingController _commentController = TextEditingController();
|
||||||
bool _sending = false;
|
bool _sending = false;
|
||||||
|
Event? _replyingTo;
|
||||||
|
|
||||||
Event get event => widget.event;
|
Event get event => widget.event;
|
||||||
|
|
||||||
@@ -702,6 +711,123 @@ class _PostViewerState extends State<_PostViewer> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _forwardPost() async {
|
||||||
|
await showScaffoldDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ShareScaffoldDialog(
|
||||||
|
items: [ContentShareItem(event.content.copy())],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _toggleCommentReaction(Event comment, String key) async {
|
||||||
|
final reactions = comment.aggregatedEvents(
|
||||||
|
widget.timeline,
|
||||||
|
RelationshipTypes.reaction,
|
||||||
|
);
|
||||||
|
for (final r in reactions) {
|
||||||
|
if (r.senderId != widget.room.client.userID) continue;
|
||||||
|
final rKey = r.content
|
||||||
|
.tryGetMap<String, Object?>('m.relates_to')
|
||||||
|
?.tryGet<String>('key');
|
||||||
|
if (rKey == key) {
|
||||||
|
await r.redactEvent();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await widget.room.sendReaction(comment.eventId, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showCommentReactionPicker(Event comment) async {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
await showModalBottomSheet<void>(
|
||||||
|
context: context,
|
||||||
|
useSafeArea: true,
|
||||||
|
backgroundColor: theme.colorScheme.surface,
|
||||||
|
builder: (context) => SizedBox(
|
||||||
|
height: MediaQuery.sizeOf(context).height * 0.5,
|
||||||
|
child: EmojiPicker(
|
||||||
|
onEmojiSelected: (_, emoji) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
_toggleCommentReaction(comment, emoji.emoji);
|
||||||
|
},
|
||||||
|
config: Config(
|
||||||
|
locale: Localizations.localeOf(context),
|
||||||
|
emojiViewConfig: EmojiViewConfig(
|
||||||
|
backgroundColor: theme.colorScheme.onInverseSurface,
|
||||||
|
),
|
||||||
|
bottomActionBarConfig: const BottomActionBarConfig(enabled: false),
|
||||||
|
categoryViewConfig: CategoryViewConfig(
|
||||||
|
backspaceColor: theme.colorScheme.primary,
|
||||||
|
iconColor: theme.colorScheme.primary.withAlpha(128),
|
||||||
|
iconColorSelected: theme.colorScheme.primary,
|
||||||
|
indicatorColor: theme.colorScheme.primary,
|
||||||
|
backgroundColor: theme.colorScheme.surface,
|
||||||
|
),
|
||||||
|
skinToneConfig: const SkinToneConfig(indicatorColor: Colors.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<_CommentNode> _buildCommentTree() {
|
||||||
|
final allComments = widget.timeline.events
|
||||||
|
.where(
|
||||||
|
(e) =>
|
||||||
|
!e.redacted &&
|
||||||
|
e.relationshipType == RelationshipTypes.thread &&
|
||||||
|
e.relationshipEventId == event.eventId,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
allComments.sort(
|
||||||
|
(a, b) => a.originServerTs.compareTo(b.originServerTs),
|
||||||
|
);
|
||||||
|
|
||||||
|
final nodeMap = <String, _CommentNode>{};
|
||||||
|
final roots = <_CommentNode>[];
|
||||||
|
|
||||||
|
for (final comment in allComments) {
|
||||||
|
nodeMap[comment.eventId] = _CommentNode(comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
String? resolveRoot(String? id) {
|
||||||
|
var current = id;
|
||||||
|
final visited = <String>{};
|
||||||
|
while (current != null &&
|
||||||
|
current != event.eventId &&
|
||||||
|
nodeMap.containsKey(current)) {
|
||||||
|
if (!visited.add(current)) break;
|
||||||
|
final node = nodeMap[current]!;
|
||||||
|
final parentId =
|
||||||
|
node.event.inReplyToEventId(includingFallback: false);
|
||||||
|
if (parentId == null ||
|
||||||
|
parentId == event.eventId ||
|
||||||
|
!nodeMap.containsKey(parentId)) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
current = parentId;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final comment in allComments) {
|
||||||
|
final replyToId = comment.inReplyToEventId(includingFallback: false);
|
||||||
|
final node = nodeMap[comment.eventId]!;
|
||||||
|
if (replyToId != null &&
|
||||||
|
replyToId != event.eventId &&
|
||||||
|
nodeMap.containsKey(replyToId)) {
|
||||||
|
final rootId = resolveRoot(replyToId);
|
||||||
|
if (rootId != null && rootId != comment.eventId) {
|
||||||
|
nodeMap[rootId]!.replies.add(node);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
roots.add(node);
|
||||||
|
}
|
||||||
|
return roots;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _editCaption() async {
|
Future<void> _editCaption() async {
|
||||||
final l10n = L10n.of(context);
|
final l10n = L10n.of(context);
|
||||||
final currentBody = event.getDisplayEvent(widget.timeline).body.trim();
|
final currentBody = event.getDisplayEvent(widget.timeline).body.trim();
|
||||||
@@ -766,13 +892,62 @@ class _PostViewerState extends State<_PostViewer> {
|
|||||||
).showSnackBar(SnackBar(content: Text(l10n.contentHasBeenReported)));
|
).showSnackBar(SnackBar(content: Text(l10n.contentHasBeenReported)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _deleteComment(Event comment) async {
|
||||||
|
final l10n = L10n.of(context);
|
||||||
|
final delete = await showOkCancelAlertDialog(
|
||||||
|
context: context,
|
||||||
|
title: l10n.areYouSure,
|
||||||
|
message: l10n.areYouSureDeletePost,
|
||||||
|
okLabel: l10n.delete,
|
||||||
|
cancelLabel: l10n.cancel,
|
||||||
|
);
|
||||||
|
if (delete != OkCancelResult.ok || !mounted) return;
|
||||||
|
try {
|
||||||
|
await comment.redactEvent();
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(L10n.of(context).oopsSomethingWentWrong)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _reportComment(Event comment) async {
|
||||||
|
final l10n = L10n.of(context);
|
||||||
|
final reason = await showTextInputDialog(
|
||||||
|
context: context,
|
||||||
|
title: l10n.whyDoYouWantToReportThis,
|
||||||
|
okLabel: l10n.report,
|
||||||
|
cancelLabel: l10n.cancel,
|
||||||
|
hintText: l10n.reason,
|
||||||
|
);
|
||||||
|
if (reason == null || reason.isEmpty || !mounted) return;
|
||||||
|
await widget.room.client.reportEvent(
|
||||||
|
widget.room.id,
|
||||||
|
comment.eventId,
|
||||||
|
reason: reason,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text(l10n.contentHasBeenReported)));
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _sendComment() async {
|
Future<void> _sendComment() async {
|
||||||
final text = _commentController.text.trim();
|
final text = _commentController.text.trim();
|
||||||
if (text.isEmpty || _sending) return;
|
if (text.isEmpty || _sending) return;
|
||||||
_commentController.clear();
|
_commentController.clear();
|
||||||
setState(() => _sending = true);
|
final replyTo = _replyingTo;
|
||||||
|
setState(() {
|
||||||
|
_sending = true;
|
||||||
|
_replyingTo = null;
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
await widget.room.sendTextEvent(text, threadRootEventId: event.eventId);
|
await widget.room.sendTextEvent(
|
||||||
|
text,
|
||||||
|
threadRootEventId: event.eventId,
|
||||||
|
inReplyTo: replyTo,
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _sending = false);
|
if (mounted) setState(() => _sending = false);
|
||||||
}
|
}
|
||||||
@@ -858,19 +1033,6 @@ class _PostViewerState extends State<_PostViewer> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Event> _comments() {
|
|
||||||
final comments = widget.timeline.events
|
|
||||||
.where(
|
|
||||||
(e) =>
|
|
||||||
!e.redacted &&
|
|
||||||
e.relationshipType == RelationshipTypes.thread &&
|
|
||||||
e.relationshipEventId == event.eventId,
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
comments.sort((a, b) => a.originServerTs.compareTo(b.originServerTs));
|
|
||||||
return comments;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _openProfile(Event comment) {
|
void _openProfile(Event comment) {
|
||||||
final sender = comment.senderFromMemoryOrFallback;
|
final sender = comment.senderFromMemoryOrFallback;
|
||||||
UserDialog.show(
|
UserDialog.show(
|
||||||
@@ -889,12 +1051,18 @@ class _PostViewerState extends State<_PostViewer> {
|
|||||||
final l10n = L10n.of(context);
|
final l10n = L10n.of(context);
|
||||||
final isVideo = event.messageType == MessageTypes.Video;
|
final isVideo = event.messageType == MessageTypes.Video;
|
||||||
|
|
||||||
|
final imageInfo = event.content.tryGetMap<String, Object?>('info');
|
||||||
|
final imgW = imageInfo?.tryGet<num>('w')?.toDouble();
|
||||||
|
final imgH = imageInfo?.tryGet<num>('h')?.toDouble();
|
||||||
|
final hasImageDimensions =
|
||||||
|
imgW != null && imgH != null && imgW > 0 && imgH > 0;
|
||||||
|
|
||||||
return StreamBuilder(
|
return StreamBuilder(
|
||||||
stream: widget.room.client.onSync.stream
|
stream: widget.room.client.onSync.stream
|
||||||
.where((syncUpdate) => syncUpdate.hasRoomUpdate)
|
.where((syncUpdate) => syncUpdate.hasRoomUpdate)
|
||||||
.rateLimit(const Duration(seconds: 1)),
|
.rateLimit(const Duration(seconds: 1)),
|
||||||
builder: (context, snapshots) {
|
builder: (context, snapshots) {
|
||||||
final comments = _comments();
|
final commentTree = _buildCommentTree();
|
||||||
final caption = event.getDisplayEvent(widget.timeline).body.trim();
|
final caption = event.getDisplayEvent(widget.timeline).body.trim();
|
||||||
final liked = _myReaction() != null;
|
final liked = _myReaction() != null;
|
||||||
|
|
||||||
@@ -939,20 +1107,32 @@ class _PostViewerState extends State<_PostViewer> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 5,
|
child: ListView(
|
||||||
child: Container(
|
padding: EdgeInsets.zero,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
color: theme.colorScheme.surfaceContainerHighest,
|
color: theme.colorScheme.surfaceContainerHighest,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: isVideo
|
child: isVideo
|
||||||
? EventVideoPlayer(event)
|
? EventVideoPlayer(event)
|
||||||
|
: hasImageDimensions
|
||||||
|
? AspectRatio(
|
||||||
|
aspectRatio: imgW / imgH,
|
||||||
|
child: MxcImage(
|
||||||
|
key: ValueKey(event.eventId),
|
||||||
|
event: event,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
isThumbnail: false,
|
||||||
|
),
|
||||||
|
)
|
||||||
: MxcImage(
|
: MxcImage(
|
||||||
key: ValueKey(event.eventId),
|
key: ValueKey(event.eventId),
|
||||||
event: event,
|
event: event,
|
||||||
|
width: double.infinity,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
isThumbnail: false,
|
isThumbnail: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -976,6 +1156,11 @@ class _PostViewerState extends State<_PostViewer> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: l10n.forward,
|
||||||
|
icon: Icon(Icons.adaptive.share_outlined),
|
||||||
|
onPressed: _forwardPost,
|
||||||
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
PopupMenuButton<String>(
|
PopupMenuButton<String>(
|
||||||
tooltip: l10n.more,
|
tooltip: l10n.more,
|
||||||
@@ -1029,11 +1214,8 @@ class _PostViewerState extends State<_PostViewer> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
Expanded(
|
if (commentTree.isEmpty)
|
||||||
flex: 3,
|
Padding(
|
||||||
child: comments.isEmpty
|
|
||||||
? Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Text(
|
child: Text(
|
||||||
l10n.noCommentsYet,
|
l10n.noCommentsYet,
|
||||||
@@ -1042,76 +1224,55 @@ class _PostViewerState extends State<_PostViewer> {
|
|||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
)
|
|
||||||
: ListView.builder(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
||||||
itemCount: comments.length,
|
|
||||||
itemBuilder: (context, i) {
|
|
||||||
final comment = comments[i];
|
|
||||||
final isSticker =
|
|
||||||
comment.messageType == MessageTypes.Sticker;
|
|
||||||
final sender = comment.senderFromMemoryOrFallback;
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
vertical: 6,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () => _openProfile(comment),
|
|
||||||
child: Avatar(
|
|
||||||
mxContent: sender.avatarUrl,
|
|
||||||
name: sender.calcDisplayname(),
|
|
||||||
size: 32,
|
|
||||||
client: widget.room.client,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment:
|
|
||||||
CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () => _openProfile(comment),
|
|
||||||
child: Text(
|
|
||||||
sender.calcDisplayname(),
|
|
||||||
style: theme.textTheme.labelMedium
|
|
||||||
?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 2),
|
|
||||||
if (isSticker)
|
|
||||||
MxcImage(
|
|
||||||
key: ValueKey(comment.eventId),
|
|
||||||
event: comment,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
isThumbnail: false,
|
|
||||||
width: double.infinity,
|
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
Text(
|
...commentTree.map((node) => _CommentTile(
|
||||||
comment
|
node: node,
|
||||||
.getDisplayEvent(widget.timeline)
|
depth: 0,
|
||||||
.calcLocalizedBodyFallback(
|
timeline: widget.timeline,
|
||||||
MatrixLocals(l10n),
|
currentUserId: widget.room.client.userID,
|
||||||
withSenderNamePrefix: false,
|
onReply: (comment) => setState(
|
||||||
hideReply: true,
|
() => _replyingTo = comment,
|
||||||
),
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
),
|
),
|
||||||
|
onToggleReaction: _toggleCommentReaction,
|
||||||
|
onShowReactionPicker: _showCommentReactionPicker,
|
||||||
|
onOpenProfile: _openProfile,
|
||||||
|
onDelete: _deleteComment,
|
||||||
|
onReport: _reportComment,
|
||||||
|
)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
if (_replyingTo != null)
|
||||||
|
Container(
|
||||||
|
color: theme.colorScheme.secondaryContainer.withValues(
|
||||||
|
alpha: 0.5,
|
||||||
),
|
),
|
||||||
);
|
padding: const EdgeInsets.fromLTRB(16, 6, 8, 6),
|
||||||
},
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.reply,
|
||||||
|
size: 16,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'${l10n.reply} ${_replyingTo!.senderFromMemoryOrFallback.calcDisplayname()}',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
icon: const Icon(Icons.close, size: 18),
|
||||||
|
onPressed: () => setState(() => _replyingTo = null),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
@@ -1134,7 +1295,9 @@ class _PostViewerState extends State<_PostViewer> {
|
|||||||
textInputAction: TextInputAction.send,
|
textInputAction: TextInputAction.send,
|
||||||
onSubmitted: (_) => _sendComment(),
|
onSubmitted: (_) => _sendComment(),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: l10n.optionalMessage,
|
hintText: _replyingTo != null
|
||||||
|
? '${l10n.reply} ${_replyingTo!.senderFromMemoryOrFallback.calcDisplayname()}'
|
||||||
|
: l10n.optionalMessage,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
),
|
),
|
||||||
@@ -1155,3 +1318,248 @@ class _PostViewerState extends State<_PostViewer> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _CommentTile extends StatelessWidget {
|
||||||
|
final _CommentNode node;
|
||||||
|
final int depth;
|
||||||
|
final Timeline timeline;
|
||||||
|
final String? currentUserId;
|
||||||
|
final ValueChanged<Event> onReply;
|
||||||
|
final void Function(Event comment, String key) onToggleReaction;
|
||||||
|
final ValueChanged<Event> onShowReactionPicker;
|
||||||
|
final ValueChanged<Event> onOpenProfile;
|
||||||
|
final ValueChanged<Event> onDelete;
|
||||||
|
final ValueChanged<Event> onReport;
|
||||||
|
|
||||||
|
const _CommentTile({
|
||||||
|
required this.node,
|
||||||
|
required this.depth,
|
||||||
|
required this.timeline,
|
||||||
|
required this.currentUserId,
|
||||||
|
required this.onReply,
|
||||||
|
required this.onToggleReaction,
|
||||||
|
required this.onShowReactionPicker,
|
||||||
|
required this.onOpenProfile,
|
||||||
|
required this.onDelete,
|
||||||
|
required this.onReport,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final l10n = L10n.of(context);
|
||||||
|
final comment = node.event;
|
||||||
|
final isSticker = comment.messageType == MessageTypes.Sticker;
|
||||||
|
final sender = comment.senderFromMemoryOrFallback;
|
||||||
|
final indent = depth * 32.0;
|
||||||
|
|
||||||
|
final reactions = comment.aggregatedEvents(
|
||||||
|
timeline,
|
||||||
|
RelationshipTypes.reaction,
|
||||||
|
);
|
||||||
|
|
||||||
|
final reactionCounts = <String, int>{};
|
||||||
|
for (final r in reactions) {
|
||||||
|
final key = r.content
|
||||||
|
.tryGetMap<String, Object?>('m.relates_to')
|
||||||
|
?.tryGet<String>('key');
|
||||||
|
if (key != null) {
|
||||||
|
reactionCounts[key] = (reactionCounts[key] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final myUserId = comment.room.client.userID;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: 16.0 + indent,
|
||||||
|
right: 16,
|
||||||
|
top: depth == 0 ? 8 : 2,
|
||||||
|
bottom: 2,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => onOpenProfile(comment),
|
||||||
|
child: Avatar(
|
||||||
|
mxContent: sender.avatarUrl,
|
||||||
|
name: sender.calcDisplayname(),
|
||||||
|
size: depth == 0 ? 32 : 24,
|
||||||
|
client: comment.room.client,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => onOpenProfile(comment),
|
||||||
|
child: Text(
|
||||||
|
sender.calcDisplayname(),
|
||||||
|
style: theme.textTheme.labelMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
if (isSticker)
|
||||||
|
MxcImage(
|
||||||
|
key: ValueKey(comment.eventId),
|
||||||
|
event: comment,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
isThumbnail: false,
|
||||||
|
width: 200,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Text(
|
||||||
|
comment
|
||||||
|
.getDisplayEvent(timeline)
|
||||||
|
.calcLocalizedBodyFallback(
|
||||||
|
MatrixLocals(l10n),
|
||||||
|
withSenderNamePrefix: false,
|
||||||
|
hideReply: true,
|
||||||
|
),
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (depth == 0)
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => onReply(comment),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||||
|
child: Text(
|
||||||
|
l10n.reply,
|
||||||
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (depth == 0) const SizedBox(width: 12),
|
||||||
|
...reactionCounts.entries.map((entry) {
|
||||||
|
final isMyReaction = reactions.any(
|
||||||
|
(r) =>
|
||||||
|
r.senderId == myUserId &&
|
||||||
|
r.content
|
||||||
|
.tryGetMap<String, Object?>(
|
||||||
|
'm.relates_to',
|
||||||
|
)
|
||||||
|
?.tryGet<String>('key') ==
|
||||||
|
entry.key,
|
||||||
|
);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 4),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () =>
|
||||||
|
onToggleReaction(comment, entry.key),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 6,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isMyReaction
|
||||||
|
? theme.colorScheme.primaryContainer
|
||||||
|
: theme
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: isMyReaction
|
||||||
|
? Border.all(
|
||||||
|
color:
|
||||||
|
theme.colorScheme.primary,
|
||||||
|
width: 1,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'${entry.key} ${entry.value}',
|
||||||
|
style: theme.textTheme.labelSmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => onShowReactionPicker(comment),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(2),
|
||||||
|
child: Icon(
|
||||||
|
Icons.add_reaction_outlined,
|
||||||
|
size: 18,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
PopupMenuButton<String>(
|
||||||
|
tooltip: l10n.more,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
color: theme.colorScheme.surface,
|
||||||
|
onSelected: (action) async {
|
||||||
|
switch (action) {
|
||||||
|
case 'delete':
|
||||||
|
onDelete(comment);
|
||||||
|
case 'report':
|
||||||
|
onReport(comment);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
if (comment.senderId == currentUserId)
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'delete',
|
||||||
|
child: ListTile(
|
||||||
|
leading: const Icon(Icons.delete_outline),
|
||||||
|
title: Text(l10n.deleteMessage),
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'report',
|
||||||
|
child: ListTile(
|
||||||
|
leading: const Icon(Icons.flag_outlined),
|
||||||
|
title: Text(l10n.reportMessage),
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
icon: Icon(
|
||||||
|
Icons.more_vert,
|
||||||
|
size: 18,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
...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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake)
|
|||||||
# https://github.com/flutter/flutter/issues/57146.
|
# https://github.com/flutter/flutter/issues/57146.
|
||||||
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
|
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
|
||||||
|
|
||||||
|
# Set fallback configurations for older versions of the flutter tool.
|
||||||
|
if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
|
||||||
|
set(FLUTTER_TARGET_PLATFORM "windows-x64")
|
||||||
|
endif()
|
||||||
|
|
||||||
# === Flutter Library ===
|
# === Flutter Library ===
|
||||||
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
|
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
|
||||||
|
|
||||||
@@ -92,7 +97,7 @@ add_custom_command(
|
|||||||
COMMAND ${CMAKE_COMMAND} -E env
|
COMMAND ${CMAKE_COMMAND} -E env
|
||||||
${FLUTTER_TOOL_ENVIRONMENT}
|
${FLUTTER_TOOL_ENVIRONMENT}
|
||||||
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
|
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
|
||||||
windows-x64 $<CONFIG>
|
${FLUTTER_TARGET_PLATFORM} $<CONFIG>
|
||||||
VERBATIM
|
VERBATIM
|
||||||
)
|
)
|
||||||
add_custom_target(flutter_assemble DEPENDS
|
add_custom_target(flutter_assemble DEPENDS
|
||||||
|
|||||||
@@ -20,6 +20,13 @@ add_executable(${BINARY_NAME} WIN32
|
|||||||
# that need different build settings.
|
# that need different build settings.
|
||||||
apply_standard_settings(${BINARY_NAME})
|
apply_standard_settings(${BINARY_NAME})
|
||||||
|
|
||||||
|
# Add preprocessor definitions for the build version.
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
|
||||||
|
|
||||||
# Disable Windows macros that collide with C++ standard library functions.
|
# Disable Windows macros that collide with C++ standard library functions.
|
||||||
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
|
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
|
||||||
|
|
||||||
|
|||||||
@@ -60,14 +60,14 @@ IDI_APP_ICON ICON "resources\\app_icon.ico"
|
|||||||
// Version
|
// Version
|
||||||
//
|
//
|
||||||
|
|
||||||
#ifdef FLUTTER_BUILD_NUMBER
|
#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
|
||||||
#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER
|
#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
|
||||||
#else
|
#else
|
||||||
#define VERSION_AS_NUMBER 1,0,0
|
#define VERSION_AS_NUMBER 1,0,0,0
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef FLUTTER_BUILD_NAME
|
#if defined(FLUTTER_VERSION)
|
||||||
#define VERSION_AS_STRING #FLUTTER_BUILD_NAME
|
#define VERSION_AS_STRING FLUTTER_VERSION
|
||||||
#else
|
#else
|
||||||
#define VERSION_AS_STRING "1.0.0"
|
#define VERSION_AS_STRING "1.0.0"
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
Reference in New Issue
Block a user