Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chat-NG Reactions #2498

Merged
merged 13 commits into from
Jan 21, 2025
5 changes: 5 additions & 0 deletions .changes/2498-chat-ng-reactions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[Labs] Chat-NG :

- Added the ability to do reaction on message (by long press).
- You can also see reactions details (By long press on reaction) of messages.
- Moved `Edited` indicator inside chat bubble and UX improvements.
2 changes: 1 addition & 1 deletion app/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -324,4 +324,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: d2243213672c3c48aae53c36642ba411a6be7309

COCOAPODS: 1.15.2
COCOAPODS: 1.16.2
119 changes: 119 additions & 0 deletions app/lib/features/chat_ng/actions/reaction_selection_action.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import 'dart:ui';

import 'package:acter/features/chat_ng/widgets/reactions/reaction_selector.dart';
import 'package:flutter/material.dart';

// reaction selector action on chat message
void reactionSelectionAction({
required BuildContext context,
required Widget messageWidget,
required bool isMe,
required String roomId,
required String messageId,
}) {
final RenderBox box = context.findRenderObject() as RenderBox;
final Offset position = box.localToGlobal(Offset.zero);
gtalha07 marked this conversation as resolved.
Show resolved Hide resolved
final messageSize = box.size;

showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel: '',
barrierColor: Colors.transparent,
transitionDuration: const Duration(milliseconds: 200),
pageBuilder: (context, animation, secondaryAnimation) {
return Stack(
children: [
_ReactionOverlay(
animation: animation,
child: const SizedBox.expand(),
),
Positioned(
left: position.dx,
top: position.dy,
width: messageSize.width,
child: messageWidget,
),
Positioned(
left: position.dx,
top: position.dy - 60,
child: _AnimatedReactionSelector(
animation: animation,
messageId: messageId,
child: ReactionSelector(
isMe: isMe,
messageId: messageId,
roomId: roomId,
),
),
),
],
);
},
);
}

class _ReactionOverlay extends StatelessWidget {
final Animation<double> animation;
final Widget child;

const _ReactionOverlay({
required this.animation,
required this.child,
});

@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => Navigator.pop(context),
child: AnimatedBuilder(
animation: animation,
builder: (context, child) {
return BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 8 * animation.value,
sigmaY: 8 * animation.value,
),
child: Container(
color: Colors.black.withOpacity(0.1 * animation.value),
child: child,
),
);
},
child: child,
),
);
}
}

class _AnimatedReactionSelector extends StatelessWidget {
final Animation<double> animation;
final Widget child;
final String messageId;

const _AnimatedReactionSelector({
required this.animation,
required this.child,
required this.messageId,
});

@override
Widget build(BuildContext context) {
return Hero(
tag: messageId,
child: Material(
color: Colors.transparent,
child: AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Transform.scale(
scale: Curves.easeOut.transform(animation.value),
child: child,
);
},
child: child,
),
),
);
}
}
19 changes: 19 additions & 0 deletions app/lib/features/chat_ng/actions/toggle_reaction_action.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import 'package:acter/features/chat/providers/chat_providers.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';

final _log = Logger('a3::chat::toggle_reaction');

Future<void> toggleReactionAction(
WidgetRef ref,
String roomId,
String uniqueId,
String emoji,
) async {
try {
final stream = await ref.read(timelineStreamProvider(roomId).future);
await stream.toggleReaction(uniqueId, emoji);
} catch (e, s) {
_log.severe('Reaction toggle failed', e, s);
}
}
2 changes: 1 addition & 1 deletion app/lib/features/chat_ng/pages/chat_room.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class ChatRoomNgPage extends ConsumerWidget {
lang.membersCount(members.length),
style: Theme.of(context).textTheme.bodySmall,
),
skipLoadingOnReload: false,
skipLoadingOnReload: true,
error: (e, s) {
_log.severe('Failed to load active members', e, s);
return Text(lang.errorLoadingMembersCount(e));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:acter/common/providers/chat_providers.dart';
import 'package:acter/common/providers/common_providers.dart';
import 'package:acter/common/providers/room_providers.dart';
import 'package:acter/common/utils/utils.dart';
import 'package:acter/common/widgets/html_editor/models/mention_type.dart';
import 'package:acter/features/chat_ng/models/chat_room_state/chat_room_state.dart';
import 'package:acter/features/chat_ng/models/replied_to_msg_state.dart';
Expand All @@ -21,6 +22,7 @@ const _supportedTypes = [

typedef RoomMsgId = ({String roomId, String uniqueId});
typedef MentionQuery = (String, MentionType);
typedef ReactionItem = (String, List<ReactionRecord>);

final chatStateProvider = StateNotifierProvider.family<ChatRoomMessagesNotifier,
ChatRoomState, String>(
Expand Down Expand Up @@ -139,3 +141,18 @@ final repliedToMsgProvider = AsyncNotifierProvider.autoDispose
.family<RepliedToMessageNotifier, RepliedToMsgState, RoomMsgId>(() {
return RepliedToMessageNotifier();
});

final messageReactionsProvider = StateProvider.autoDispose
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if that should be per item object or rather be bound to the roomId+eventId combination ... not sure yet.

.family<List<ReactionItem>, RoomEventItem>((ref, item) {
List<ReactionItem> reactions = [];

final reactionKeys = asDartStringList(item.reactionKeys());
for (final key in reactionKeys) {
final records = item.reactionRecords(key);
if (records != null) {
reactions.add((key, records.toList()));
}
}

return reactions;
});
44 changes: 26 additions & 18 deletions app/lib/features/chat_ng/widgets/chat_bubble.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:acter/common/extensions/acter_build_context.dart';
import 'package:acter/common/themes/acter_theme.dart';
import 'package:flutter/material.dart';
import 'package:acter/common/extensions/options.dart';
Expand All @@ -7,7 +8,7 @@ class ChatBubble extends StatelessWidget {
final Widget child;
final int? messageWidth;
final BoxDecoration decoration;
final CrossAxisAlignment bubbleAlignment;
final MainAxisAlignment bubbleAlignment;
final bool isEdited;
final Widget? repliedToBuilder;

Expand Down Expand Up @@ -43,15 +44,15 @@ class ChatBubble extends StatelessWidget {
bottomRight: Radius.circular(16),
),
),
bubbleAlignment: CrossAxisAlignment.start,
bubbleAlignment: MainAxisAlignment.start,
isEdited: isEdited,
repliedToBuilder: repliedToBuilder,
child: child,
);
}

// for user's own messages
factory ChatBubble.user({
factory ChatBubble.me({
Key? key,
required BuildContext context,
required Widget child,
Expand All @@ -73,8 +74,9 @@ class ChatBubble extends StatelessWidget {
bottomRight: Radius.circular(isNextMessageInGroup ? 16 : 4),
),
),
bubbleAlignment: CrossAxisAlignment.end,
bubbleAlignment: MainAxisAlignment.end,
repliedToBuilder: repliedToBuilder,
isEdited: isEdited,
child: DefaultTextStyle.merge(
style: theme.textTheme.bodySmall
?.copyWith(color: theme.colorScheme.onPrimary),
Expand All @@ -88,22 +90,26 @@ class ChatBubble extends StatelessWidget {
final chatTheme = Theme.of(context).chatTheme;
final size = MediaQuery.sizeOf(context);
final msgWidth = messageWidth.map((w) => w.toDouble());
final defaultWidth =
context.isLargeScreen ? size.width * 0.5 : size.width * 0.75;

return Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
child: Column(
crossAxisAlignment: bubbleAlignment,
child: Row(
mainAxisAlignment: bubbleAlignment,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const SizedBox(width: 5),
Container(
constraints: BoxConstraints(
maxWidth: msgWidth ?? size.width,
maxWidth: msgWidth ?? defaultWidth,
),
width: msgWidth,
decoration: decoration,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
horizontal: 16,
vertical: 16,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
Expand All @@ -113,19 +119,21 @@ class ChatBubble extends StatelessWidget {
const SizedBox(height: 10),
],
child,
if (isEdited) ...[
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved Edited indicator inside bubble for better UX.

const SizedBox(width: 5),
Align(
alignment: Alignment.bottomRight,
child: Text(
L10n.of(context).edited,
style: chatTheme.emptyChatPlaceholderTextStyle
.copyWith(fontSize: 12),
),
),
],
],
),
),
),
if (isEdited)
Align(
alignment: Alignment(0.9, 0.0),
child: Text(
L10n.of(context).edited,
style: chatTheme.emptyChatPlaceholderTextStyle
.copyWith(fontSize: 12),
),
),
],
),
);
Expand Down
9 changes: 4 additions & 5 deletions app/lib/features/chat_ng/widgets/events/chat_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,14 @@ class ChatEvent extends ConsumerWidget {
final options = AvatarOptions.DM(avatarInfo, size: 14);
final myId = ref.watch(myUserIdStrProvider);
final messageId = msg.uniqueId();
final isUser = myId == item.sender();
final isMe = myId == item.sender();
// TODO: render a regular timeline event
return Row(
mainAxisAlignment:
!isUser ? MainAxisAlignment.start : MainAxisAlignment.end,
!isMe ? MainAxisAlignment.start : MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
(!isNextMessageInGroup && !isUser)
(!isNextMessageInGroup && !isMe)
? Padding(
padding: const EdgeInsets.only(left: 8),
child: ActerAvatar(options: options),
Expand All @@ -89,7 +88,7 @@ class ChatEvent extends ConsumerWidget {
roomId: roomId,
messageId: messageId,
item: item,
isUser: isUser,
isMe: isMe,
isNextMessageInGroup: isNextMessageInGroup,
),
),
Expand Down
Loading
Loading