From 4c7069075e4185bc38220bb962bbb09b101aee86 Mon Sep 17 00:00:00 2001 From: Nicolas Braun Date: Wed, 23 Jul 2025 14:45:51 +0200 Subject: [PATCH 1/2] Add some layout doc --- astro.sidebar.ts | 14 +++ .../docs/docs/flutter/guides/add-avatars.md | 75 ++++++++++++++++ .../flutter/guides/add-date-separators.md | 82 +++++++++++++++++ .../docs/docs/flutter/guides/add-usernames.md | 79 +++++++++++++++++ .../flutter/guides/understanding-layout.md | 88 +++++++++++++++++++ 5 files changed, 338 insertions(+) create mode 100644 src/content/docs/docs/flutter/guides/add-avatars.md create mode 100644 src/content/docs/docs/flutter/guides/add-date-separators.md create mode 100644 src/content/docs/docs/flutter/guides/add-usernames.md create mode 100644 src/content/docs/docs/flutter/guides/understanding-layout.md diff --git a/astro.sidebar.ts b/astro.sidebar.ts index 3e89c78..c42a529 100644 --- a/astro.sidebar.ts +++ b/astro.sidebar.ts @@ -43,4 +43,18 @@ export const sidebar = [ { label: "More Guides", slug: "docs/flutter/guides/more-guides" }, ], }, + { + label: "Layout", + items: [ + { + label: "Understanding Layout", + slug: "docs/flutter/layout/understanding-layout", + }, + + { label: "Dynamic Theming", slug: "docs/flutter/guides/dynamic-theming" }, + { label: "Add Avatars", slug: "docs/flutter/guides/add-avatars" }, + { label: "Add Usernames", slug: "docs/flutter/guides/add-usernames" }, + { label: "Add Date Separators", slug: "docs/flutter/guides/add-date-separators" }, + ], + }, ] satisfies StarlightUserConfig["sidebar"]; diff --git a/src/content/docs/docs/flutter/guides/add-avatars.md b/src/content/docs/docs/flutter/guides/add-avatars.md new file mode 100644 index 0000000..f9862da --- /dev/null +++ b/src/content/docs/docs/flutter/guides/add-avatars.md @@ -0,0 +1,75 @@ + +--- +title: Add Avatars 👩‍🎤 +--- + +Avatars are usually displayed outside the message's bubble, therefore there are built in the `chatMessageBuilder`. + +It's up to you to define your own logic to display (or not) the avatar based on the message. + +The `Avatar` widget is exposed by the library but your are free to build any widget you like. + + +## Example usage + + +```dart + chatMessageBuilder: + ( + context, + message, + index, + animation, + child, { + bool? isRemoved, + required bool isSentByMe, + MessageGroupStatus? groupStatus, + }) { + final isSystemMessage = message.authorId == 'system'; + final isFirstInGroup = groupStatus?.isFirst ?? true; + final isLastInGroup = groupStatus?.isLast ?? true; + final shouldShowAvatar = + !isSystemMessage && isLastInGroup && isRemoved != true; + final isCurrentUser = message.authorId == _currentUser.id; + + Widget? avatar; + if (shouldShowAvatar) { + avatar = Padding( + padding: EdgeInsets.only( + left: isCurrentUser ? 8 : 0, + right: isCurrentUser ? 0 : 8, + ), + child: Avatar(userId: message.authorId), + ); + } else if (!isSystemMessage) { + avatar = const SizedBox(width: 40); + } + + return ChatMessage( + message: message, + index: index, + animation: animation, + isRemoved: isRemoved, + groupStatus: groupStatus, + leadingWidget: !isCurrentUser + ? avatar + : isSystemMessage + ? null + : const SizedBox(width: 40), + trailingWidget: isCurrentUser + ? avatar + : isSystemMessage + ? null + : const SizedBox(width: 40), + receivedMessageScaleAnimationAlignment: + (message is SystemMessage) + ? Alignment.center + : Alignment.centerLeft, + receivedMessageAlignment: (message is SystemMessage) + ? AlignmentDirectional.center + : AlignmentDirectional.centerStart, + horizontalPadding: (message is SystemMessage) ? 0 : 8, + child: child, + ); + }, +``` \ No newline at end of file diff --git a/src/content/docs/docs/flutter/guides/add-date-separators.md b/src/content/docs/docs/flutter/guides/add-date-separators.md new file mode 100644 index 0000000..186a154 --- /dev/null +++ b/src/content/docs/docs/flutter/guides/add-date-separators.md @@ -0,0 +1,82 @@ +--- +title: Add date separators 📆 +--- + +Usually date separators are displayed above the message bubbles and are centered horizontally within the chat's view. + +The best way to achieve this is to use the either the `topWidget` or `headerWidget` parameter of the `ChatMessage` (see [Understanding Layout](./understanding-layout.md)). It's up to you to defined the logic of how and when to display the separator. + +## Example + +```dart + Widget _chatMessageBuilder( + BuildContext context, + Message message, + int index, + Animation animation, + Widget child, { + bool? isRemoved, + required bool isSentByMe, + MessageGroupStatus? groupStatus, + }) { + + + return ChatMessage( + message: message, + index: index, + animation: animation, + groupStatus: groupStatus, + headerWidget: _getDisplayDateHeader(index), + child: child, + ); + } +``` + +```dart +Widget? _getDisplayDateHeader(int index) { + try { + final DateTime now = DateTime.now(); + final Message? currentMessage = _controller?.messages[index]; + if (currentMessage == null) return null; + final currentMessageDate = currentMessage.createdAt!; + + final Message? previousMessage = + index > 0 ? _controller?.messages[index - 1] : null; + const differenceThreshold = 15; + + final previousMessageDate = previousMessage?.createdAt!; + + String? dateString; + + // It's today, display the time if messages are more than X minutes appart + // We could also use the groupStatus + if (currentMessageDate.isSameDay(now)) { + if (previousMessageDate == null || + currentMessageDate.difference(previousMessageDate).inMinutes >= + differenceThreshold) { + dateString = DateFormat.jm().format(currentMessageDate.toLocal()); + } + return null; + } + // Else one header per day + if (previousMessageDate != null && + currentMessageDate.isSameDay(previousMessageDate)) { + return null; + } + + if (currentMessageDate.isSameWeek(now)) { + dateString = + DateFormat.EEEE().add_jm().format(currentMessageDate.toLocal()); + } + if (currentMessageDate.isSameYear(now)) { + dateString = DateFormat('d MMMM').format(currentMessageDate.toLocal()); + } + dateString = DateFormat.yMd().format(currentMessageDate.toLocal()); + + return YourHeaderWidget(dateString: dateString); + } catch (e) { + log.e('Error getting display date header', error: e); + return null; + } + } +``` \ No newline at end of file diff --git a/src/content/docs/docs/flutter/guides/add-usernames.md b/src/content/docs/docs/flutter/guides/add-usernames.md new file mode 100644 index 0000000..8f14abf --- /dev/null +++ b/src/content/docs/docs/flutter/guides/add-usernames.md @@ -0,0 +1,79 @@ +--- +title: Add usernames +--- + +## Above the bubble + +To add usernames abobe the message bubble, you can use `topWidget` or `headerWidget` of the `ChatMessage`. + +It's up to you to implement the logic to display or not the username, and which widget to user. The library exposes a `Username` widget. + +```dart +chatMessageBuilder: + ( + context, + message, + index, + animation, + child, { + bool? isRemoved, + required bool isSentByMe, + MessageGroupStatus? groupStatus, + }) { + final isSystemMessage = message.authorId == 'system'; + final isFirstInGroup = groupStatus?.isFirst ?? true; + final isLastInGroup = groupStatus?.isLast ?? true; + final isCurrentUser = message.authorId == _currentUser.id; + final shouldShowUsername = + !isSystemMessage && isFirstInGroup && isRemoved != true; + + return ChatMessage( + message: message, + index: index, + animation: animation, + isRemoved: isRemoved, + groupStatus: groupStatus, + topWidget: shouldShowUsername + ? Padding( + padding: EdgeInsets.only( + bottom: 4, + left: isCurrentUser ? 0 : 48, + right: isCurrentUser ? 48 : 0, + ), + child: Username(userId: message.authorId), + ) + : null, + + child: child, + ); + }, +``` + +## In the message bubble + +To display the username within the bubble the logic remains the same but we will use the `topWidget` parameter for each individual message builder. + +**Example for textMessage** + +```dart + Widget _textMessageBuilder( + BuildContext context, + TextMessage message, + int index, { + required bool isSentByMe, + MessageGroupStatus? groupStatus, + }) { + final isSystemMessage = message.authorId == 'system'; + final isFirstInGroup = groupStatus?.isFirst ?? true; + final isLastInGroup = groupStatus?.isLast ?? true; + final isCurrentUser = message.authorId == _currentUser.id; + final shouldShowUsername = + !isSystemMessage && isFirstInGroup && isRemoved != true; + + return FlyerChatTextMessage( + topWidget: shouldShowUsername ? YourWidget() : null + message: message, + index: index + ); + } + ``` \ No newline at end of file diff --git a/src/content/docs/docs/flutter/guides/understanding-layout.md b/src/content/docs/docs/flutter/guides/understanding-layout.md new file mode 100644 index 0000000..842b8e9 --- /dev/null +++ b/src/content/docs/docs/flutter/guides/understanding-layout.md @@ -0,0 +1,88 @@ +--- +title: Understanding layout 📚 +--- + +This guide explains how Flyer Chat uses the `builders` parameter of `Chat` to layout the messages. + +# ChatMessage + + +The `ChatMessage` widget build with `ChatMessageBuilder` is the foundational layout container for a single message Widget (depending on type) in the Flyer Chat UI. It is designed to be highly flexible and customizable, allowing you to insert custom widgets around the main message content and control alignment, animation, and grouping. + +--- + +## Purpose + +- **Aligns** the message based on the sender (left/right). +- **Animates** appearance and removal (fade, scale, size). +- **Manages padding** for grouped or standalone messages. +- **Handles gestures** (tap, double-tap, long-press). +- **Allows custom widgets** in key positions around the message. + +--- + +## Widget Slots + +You can provide widgets for the following slots: + +- **headerWidget**: Appears above everything (e.g., date headers) and is not animated. +- **topWidget**: Appears above the message row (e.g., username, reply preview). +- **leadingWidget**: Appears to the left/start of the message (e.g., avatar). +- **child**: The main message content (text bubble, image, etc.): Will use each message's type builder. +- **trailingWidget**: Appears to the right/end of the message +- **bottomWidget**: Appears below the message row + +--- + +## Layout Diagram + +Below is a diagram showing the placement of each widget slot: + + +| | headerWidget | | +|----------------|:---------------------------:|----------------| +| | topWidget | | +| leadingWidget | child | trailingWidget | +| | bottomWidget | | + +--- + +## Example Usages + +* [Add avatars](./add-avatars.md) +* [Add usernames](./add-usernames.md) +* [Add date separtors](./add-date-separators.md) + +# Type specific builders + +`Builder` exposes one parameter per message type allowing you to choose how it's rendered. The resulting widget will be the child widget in the `ChatMessage`. + +You can build your own widgets or use the corresponding Flyer Chat packages. + +## Example for ImageMessage + +```dart +Chat( + backgroundColor: Colors.transparent, + builders: Builders( + chatAnimatedListBuilder: (context, itemBuilder) { + return ChatAnimatedList( + itemBuilder: itemBuilder, + insertAnimationDurationResolver: (message) { + if (message is SystemMessage) return Duration.zero; + return null; + }, + ); + }, + imageMessageBuilder: + ( + context, + message, + index, { + required bool isSentByMe, + MessageGroupStatus? groupStatus, + }) => FlyerChatImageMessage(message: message, index: index), + ) +) + +``` From 2830395159687e18bfd524a1bb02ac5a0389d4a5 Mon Sep 17 00:00:00 2001 From: Nicolas Braun Date: Wed, 23 Jul 2025 14:46:03 +0200 Subject: [PATCH 2/2] Add doc for reactions --- astro.sidebar.ts | 1 + .../docs/docs/flutter/guides/add-reactions.md | 107 ++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 src/content/docs/docs/flutter/guides/add-reactions.md diff --git a/astro.sidebar.ts b/astro.sidebar.ts index c42a529..036e7cb 100644 --- a/astro.sidebar.ts +++ b/astro.sidebar.ts @@ -55,6 +55,7 @@ export const sidebar = [ { label: "Add Avatars", slug: "docs/flutter/guides/add-avatars" }, { label: "Add Usernames", slug: "docs/flutter/guides/add-usernames" }, { label: "Add Date Separators", slug: "docs/flutter/guides/add-date-separators" }, + { label: "Add Reactions", slug: "docs/flutter/guides/add-reactions" }, ], }, ] satisfies StarlightUserConfig["sidebar"]; diff --git a/src/content/docs/docs/flutter/guides/add-reactions.md b/src/content/docs/docs/flutter/guides/add-reactions.md new file mode 100644 index 0000000..fa2a356 --- /dev/null +++ b/src/content/docs/docs/flutter/guides/add-reactions.md @@ -0,0 +1,107 @@ +--- +title: Add reactions 👍 +--- + +:::tip[Reactions position] +Flyer Chat assumes reactions to be displayed under the bubble, overlapping the bubble. +Should you want to display the reactions another way it may be achievable using the the parameters of `ChatMessage` but this has not been tested by the team. +::: + + +:::danger +To give user's full flexibility in the reactions behavior he library does not automatically call the controller to perform message update when the user interacts with reactions. It's up to you to implement the logic you prefer. + +For example: +WhatsApp/Signal: open reactions' list on tap and long press. Allow only one reaction. +Slack: react on tap, open list on long press. Allow several reactions. +::: + + +Adding reactions to your messages is a two step process. + +This example uses the `flyer_chat_reactions` package to add reactions to your messages. + +## Displaying reactions under the message bubble + +Flyer Chat uses the `reactionsBuilder` on to display the reactions. +Those will appear under the message bubble, overlapping and you can fully customize what happens when a user interacts with them. + +```dart +Widget _reactionsBuilder( + BuildContext context, Message message, bool isSentByMe) { + final reactions = reactionsFromMessageReactions( + reactions: message.reactions, + currentUserId: "the_user_id", + ); + return FlyerChatReactionsRow( + reactions: reactions, + alignment: isSentByMe ? MainAxisAlignment.start : MainAxisAlignment.end, + onReactionTap: (reaction) => _handleReactionTap(message, reaction), + removeOrAddLocallyOnTap: true, // Allows to visually remove the reaction on tap so it's visually instantaneous. This will not update the controller. + onReactionLongPress: (reaction) => + // The package exposes a method to show all the reactions list + showReactionsList(context: context, reactions: reactions), + onSurplusReactionTap: () => + showReactionsList(context: context, reactions: reactions), + ); + } + + void _handleReactionTap(Message message, String reaction) { + // Implement you message update logic here + } +``` + +### Displaying the reactions dialog to add a reaction + +Once again it's up to you when to display this dialog, usually on a message long press. + +```dart + void _handleMessageLongPress( + BuildContext context, + Message message, { + int? index, + LongPressStartDetails? details, + bool isSentByMe = false, + }) async { + final currentUserId = 'user_id'; + showReactionsDialog( + context, + message, + isSentByMe: isSentByMe, + // reactions: ['😄','😂'] // You can change default reactions here + userReactions: getUserReactions(message.reactions, currentUserId), + onReactionTap: (reaction) => _handleReactionTap(message, reaction), + onMoreReactionsTap: () async { + // Called when the 'more' reactions is tapped. It's up to you to decide what to do. Usually use an emoji-picker as showcased here. + final picked = await _showEmojiPicker(); + if (picked != null) { + _handleReactionTap(message, picked); + } + }, + menuItems: _getMenuItems(message), // Display context menu items + ); + } + + Future _showEmojiPicker() { + return showModalBottomSheet( + context: context, + useSafeArea: true, + builder: (context) => EmojiPicker( + onEmojiSelected: (Category? category, Emoji emoji) { + Navigator.of(context).pop(emoji.emoji); + }, + config: Config( + height: 250, + checkPlatformCompatibility: false, + viewOrderConfig: const ViewOrderConfig(), + skinToneConfig: const SkinToneConfig(), + categoryViewConfig: const CategoryViewConfig(), + bottomActionBarConfig: const BottomActionBarConfig(enabled: false), + searchViewConfig: const SearchViewConfig(), + ), + ), + ); + } + + +```