Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 13 additions & 89 deletions lib/ui/views/main_page/chat/widgets/message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Copyright 2025 BitCodersNN

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bbcode/flutter_bbcode.dart';
import 'package:unn_mobile/core/constants/date_pattern.dart';
import 'package:unn_mobile/core/misc/custom_bb_tags.dart';
Expand All @@ -22,6 +21,8 @@ import 'package:unn_mobile/core/viewmodels/main_page/feed/attached_file_view_mod
import 'package:unn_mobile/ui/views/base_view.dart';
import 'package:unn_mobile/ui/views/main_page/feed/widgets/attached_file.dart';
import 'package:unn_mobile/ui/views/main_page/feed/widgets/reaction_bubble.dart';
import 'package:unn_mobile/ui/widgets/context_menu/context_menu_factory.dart';
import 'package:unn_mobile/ui/widgets/context_menu/context_menu_helper.dart';

const systemMessageSeparator =
'------------------------------------------------------\n';
Expand Down Expand Up @@ -231,7 +232,17 @@ class _MessageWidgetState extends State<MessageWidget> {
return BaseView<MessageReactionViewModel>(
model: MessageReactionViewModel.cached(widget.message.messageId),
builder: (context, model, _) => GestureDetector(
onLongPress: () => _showContextMenu(model),
onLongPress: () => ContextMenuHelper.showContextMenu(
context: context,
model: model,
actionsBuilder: () => createMessageActions(
context: context,
model: model,
widget: widget,
),
onOpen: () => setState(() => _isHighlighted = true),
onClose: () => setState(() => _isHighlighted = false),
),
child: _buildMessageContent(context, model, theme),
),
onModelReady: (model) => model.init(
Expand All @@ -241,93 +252,6 @@ class _MessageWidgetState extends State<MessageWidget> {
);
}

void _showContextMenu(MessageReactionViewModel model) {
setState(() => _isHighlighted = true);
triggerHaptic(HapticIntensity.medium);

final renderBox = context.findRenderObject()! as RenderBox;
final offset = renderBox.localToGlobal(Offset.zero);

showMenu<String>(
context: context,
position: RelativeRect.fromLTRB(
offset.dx,
offset.dy + renderBox.size.height,
offset.dx + renderBox.size.width,
offset.dy + renderBox.size.height + 1,
),
items: _buildMenuItems(model),
).then((value) {
setState(() => _isHighlighted = false);
_handleMenuSelection(value);
});
}

List<PopupMenuEntry<String>> _buildMenuItems(
MessageReactionViewModel model,
) =>
[
PopupMenuItem(
enabled: false,
child: Scrollbar(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: ReactionType.values
.map(
(reaction) => GestureDetector(
onTap: () => _handleReactionTap(reaction, model),
child: Padding(
padding: const EdgeInsets.all(4.0),
child: CircleAvatar(
radius: 16,
backgroundImage: AssetImage(reaction.assetName),
),
),
),
)
.toList(),
),
),
),
),
const PopupMenuItem(
value: 'copy',
child: Text('Скопировать текст'),
),
const PopupMenuItem(
value: 'reply',
child: Text('Ответить'),
),
];

void _handleReactionTap(
ReactionType reaction,
MessageReactionViewModel model,
) {
triggerHaptic(HapticIntensity.selection);
if (model.currentReaction != reaction) {
model.toggleReaction(reaction);
}
Navigator.pop(context);
}

void _handleMenuSelection(String? value) async {
if (value == null) {
return;
}

switch (value) {
case 'copy':
await Clipboard.setData(ClipboardData(text: widget.message.text));
break;
case 'reply':
widget.chatModel.replyMessage = widget.message;
break;
}
}

Widget _buildMessageContent(
BuildContext context,
MessageReactionViewModel model,
Expand Down
32 changes: 15 additions & 17 deletions lib/ui/views/main_page/feed/widgets/comments_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,25 +51,23 @@ class CommentsPage extends StatelessWidget {
),
),
),
for (final comment in model.comments)
Column(
children: [
const Padding(
padding: EdgeInsets.only(
left: 18,
bottom: 10,
right: 18,
),
child: Divider(
thickness: 0.3,
color: Color(0xFF989EA9),
),
Column(
children: [
const Padding(
padding: EdgeInsets.symmetric(
horizontal: 18,
vertical: 10,
),
FeedCommentView(
viewModel: comment,
child: Divider(
height: 1,
thickness: 0.3,
color: Color(0xFF989EA9),
),
],
),
),
for (final comment in model.comments)
FeedCommentView(viewModel: comment),
],
),
],
),
),
Expand Down
72 changes: 43 additions & 29 deletions lib/ui/views/main_page/feed/widgets/feed_comment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import 'package:unn_mobile/ui/views/main_page/feed/widgets/attached_file.dart';
import 'package:unn_mobile/ui/views/main_page/feed/widgets/packed_post_images.dart';
import 'package:unn_mobile/ui/views/main_page/feed/widgets/post_html_widget.dart';
import 'package:unn_mobile/ui/views/main_page/feed/widgets/reaction_bubble.dart';
import 'package:unn_mobile/ui/widgets/context_menu/context_menu_factory.dart';
import 'package:unn_mobile/ui/widgets/context_menu/context_menu_helper.dart';
import 'package:unn_mobile/ui/widgets/shimmer.dart';
import 'package:unn_mobile/ui/widgets/shimmer_loading.dart';

Expand All @@ -26,38 +28,50 @@ class FeedCommentView extends StatelessWidget {
@override
Widget build(BuildContext context) => BaseView<FeedCommentViewModel>(
model: viewModel,
builder: (context, model, child) => Shimmer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_CommentHeader(
dateTime: model.comment.dateTime,
viewModel: model.profileViewModel,
hide: model.isBusy,
),
Padding(
padding: const EdgeInsets.only(
left: 16,
bottom: 10,
right: 10,
top: 8,
builder: (context, model, child) => GestureDetector(
onLongPress: () => ContextMenuHelper.showContextMenu(
context: context,
model: model,
actionsBuilder: () => createCommentActions(
context: context,
model: model,
),
),
child: Shimmer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_CommentHeader(
dateTime: model.comment.dateTime,
viewModel: model.profileViewModel,
hide: model.isBusy,
),
Padding(
padding: const EdgeInsets.only(
left: 16,
bottom: 10,
right: 10,
top: 8,
),
child: model.renderMessage
? PostHtmlWidget(text: model.message)
: const SizedBox(),
),
child: model.renderMessage
? PostHtmlWidget(text: model.message)
: const SizedBox(),
),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 20.0, vertical: 8.0),
child: PackedPostImages(attachedImages: model.attachedImages),
),
for (final file in model.attachedFileViewModels)
Padding(
padding: const EdgeInsets.only(left: 16),
child: AttachedFile(viewModel: file),
padding: const EdgeInsets.symmetric(
horizontal: 20.0,
vertical: 0.0,
),
child: PackedPostImages(attachedImages: model.attachedImages),
),
_ReactionView(model: model.reactionViewModel, context: context),
],
for (final file in model.attachedFileViewModels)
Padding(
padding: const EdgeInsets.only(left: 16),
child: AttachedFile(viewModel: file),
),
_ReactionView(model: model.reactionViewModel, context: context),
],
),
),
),
);
Expand Down
13 changes: 13 additions & 0 deletions lib/ui/views/main_page/feed/widgets/feed_post.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import 'package:unn_mobile/ui/views/main_page/feed/widgets/attached_file.dart';
import 'package:unn_mobile/ui/views/main_page/feed/widgets/packed_post_images.dart';
import 'package:unn_mobile/ui/views/main_page/feed/widgets/post_html_widget.dart';
import 'package:unn_mobile/ui/views/main_page/main_page_routing.dart';
import 'package:unn_mobile/ui/widgets/context_menu/context_menu_factory.dart';
import 'package:unn_mobile/ui/widgets/context_menu/context_menu_helper.dart';
import 'package:unn_mobile/ui/widgets/height_limiter.dart';
import 'package:unn_mobile/ui/widgets/shimmer.dart';
import 'package:unn_mobile/ui/widgets/shimmer_loading.dart';
Expand Down Expand Up @@ -71,6 +73,17 @@ class _FeedPostState extends State<FeedPost> {
.putInCache(model.blogData.id, model);
_openPostCommentsPage(context, model);
},
onLongPress: () => ContextMenuHelper.showContextMenu(
context: context,
model: model,
actionsBuilder: () => createPostActions(
context: context,
model: model,
onShare: _sharePost,
),
onOpen: () => setState(() {}),
onClose: () => setState(() {}),
),
child: Shimmer(
child: AnimatedContainer(
duration: const Duration(milliseconds: 100),
Expand Down
91 changes: 91 additions & 0 deletions lib/ui/widgets/context_menu/context_menu_action.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2025 BitCodersNN

import 'package:flutter/material.dart';
import 'package:unn_mobile/core/misc/haptic_utils.dart';
import 'package:unn_mobile/core/models/feed/rating_list.dart';
import 'package:unn_mobile/core/viewmodels/main_page/common/reaction_view_model_base.dart';

class ContextMenuAction {
final PopupMenuEntry<void> entry;
final VoidCallback? onTap;

const ContextMenuAction({
required this.entry,
this.onTap,
});

factory ContextMenuAction.text({
required String label,
VoidCallback? onTap,
Widget? leadingIcon,
}) =>
ContextMenuAction(
entry: PopupMenuItem<void>(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (leadingIcon != null) ...[
leadingIcon,
const SizedBox(width: 12),
],
Text(label),
],
),
),
onTap: onTap,
);

factory ContextMenuAction.reaction({
required BuildContext context,
required ReactionViewModelBase reactionViewModel,
}) =>
ContextMenuAction(
entry: PopupMenuItem<void>(
enabled: false,
child: SizedBox(
width: 280,
child: Scrollbar(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (final reaction in ReactionType.values)
GestureDetector(
onTap: () {
triggerHaptic(HapticIntensity.selection);
if (reactionViewModel.currentReaction != reaction) {
reactionViewModel.toggleReaction(reaction);
}
Navigator.pop(context);
},
child: Padding(
padding: const EdgeInsets.all(4.0),
child: CircleAvatar(
radius: 16,
backgroundImage: AssetImage(reaction.assetName),
),
),
),
],
),
),
),
),
),
);

factory ContextMenuAction.custom({
required Widget child,
VoidCallback? onTap,
}) =>
ContextMenuAction(
entry: PopupMenuItem<void>(
enabled: false,
child: child,
),
onTap: onTap,
);
}
Loading