diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md
index fd4c117e..602a5da5 100644
--- a/packages/components/CHANGELOG.md
+++ b/packages/components/CHANGELOG.md
@@ -1,8 +1,15 @@
# @baseapp-frontend/components
+## 1.0.35
+
+### Patch Changes
+
+- create MessageList, messageItem, ChatRoom and MessagesGroup storybook
+
## 1.0.34
### Patch Changes
+
- Content Feed creation page and Dropzone Improvements
- Updated dependencies
- @baseapp-frontend/design-system@1.0.15
diff --git a/packages/components/modules/messages/web/ChatRoom/__storybook__/ChatRoom.mdx b/packages/components/modules/messages/web/ChatRoom/__storybook__/ChatRoom.mdx
new file mode 100644
index 00000000..b66cf53d
--- /dev/null
+++ b/packages/components/modules/messages/web/ChatRoom/__storybook__/ChatRoom.mdx
@@ -0,0 +1,46 @@
+import { Meta } from '@storybook/addon-docs'
+
+
+
+# Component Documentation
+
+## ChatRoom
+
+- **Purpose**: Central component for rendering a complete chat experience, including header, message list, and input form.
+- **Expected Behavior**: Loads chat room data via Relay, shows messages, allows message sending, and provides access to group or conversation-specific actions.
+
+## Use Cases
+
+- **Current Usage**: Used in the messaging module as the main interface for individual or group chats.
+- **Potential Usage**: Could be adapted for support ticket threads, comment discussions, or community channels.
+
+## Props
+
+- **roomId** (`string`): The unique identifier for the chat room to fetch and render.
+- **MessagesList** (`component`, optional): Custom component to render messages. Defaults to `DefaultMessagesList`.
+- **MessagesListProps** (`object`, optional): Extra props passed to `MessagesList`.
+- **SendMessage** (`component`, optional): Custom component to render the message input. Defaults to `DefaultSendMessage`.
+- **SendMessageProps** (`object`, optional): Extra props passed to `SendMessage`.
+- **onDisplayGroupDetailsClicked** (`function`): Callback triggered when user clicks on group chat header to view group info.
+
+## Notes
+
+- **Related Components**:
+ - `MessagesList` – Renders the message timeline inside the chat.
+ - `SendMessage` – Handles input and sending of messages.
+ - `ChatRoomHeader` – Displays chat participants, title, and group options.
+
+## Example Usage
+
+```tsx
+const MyComponent = () => {
+ return (
+ console.log('Group info opened')}
+ />
+ )
+}
+
+export default MyComponent
+```
diff --git a/packages/components/modules/messages/web/ChatRoom/__storybook__/ChatRoomWithQuery/index.tsx b/packages/components/modules/messages/web/ChatRoom/__storybook__/ChatRoomWithQuery/index.tsx
new file mode 100644
index 00000000..c22e0427
--- /dev/null
+++ b/packages/components/modules/messages/web/ChatRoom/__storybook__/ChatRoomWithQuery/index.tsx
@@ -0,0 +1,34 @@
+import { graphql, useLazyLoadQuery } from 'react-relay'
+
+import SuspendedChatRoom from '../..'
+import { ChatRoomWithQuery as Query } from '../../../../../../__generated__/ChatRoomWithQuery.graphql'
+import { MessagesListFragment$key } from '../../../../../../__generated__/MessagesListFragment.graphql'
+
+const ChatRoomWithQuery = () => {
+ const data = useLazyLoadQuery(
+ graphql`
+ query ChatRoomWithQuery @relay_test_operation {
+ chatRoom(id: "room-123") {
+ id
+ isArchived
+ participantsCount
+ ...TitleFragment
+ ...MessagesListFragment
+ }
+ }
+ `,
+ {},
+ )
+
+ if (!data.chatRoom) return Room not found
+
+ return (
+ alert('Group details clicked')}
+ MessagesListProps={{ roomRef: data.chatRoom as MessagesListFragment$key }}
+ />
+ )
+}
+
+export default ChatRoomWithQuery
diff --git a/packages/components/modules/messages/web/ChatRoom/__storybook__/mockResolvers.ts b/packages/components/modules/messages/web/ChatRoom/__storybook__/mockResolvers.ts
new file mode 100644
index 00000000..24a88a83
--- /dev/null
+++ b/packages/components/modules/messages/web/ChatRoom/__storybook__/mockResolvers.ts
@@ -0,0 +1,265 @@
+export const mockResolverGroup = {
+ ChatRoom: () => ({
+ id: 'room-123',
+ isArchived: false,
+ participantsCount: 3,
+ isGroup: true,
+ participants: {
+ edges: [
+ {
+ node: {
+ profile: {
+ id: 'UHJvZmlsZToxNzM=',
+ name: 'John',
+ image: null,
+ },
+ role: 'MEMBER',
+ id: 'Q2hhdFJvb21QYXJ0aWNpcGFudDoxNA==',
+ },
+ },
+ {
+ node: {
+ profile: {
+ id: 'UHJvZmlsZToxNTM=',
+ name: 'Priscilla',
+ image: null,
+ },
+ role: 'ADMIN',
+ id: 'Q2hhdFJvb21QYXJ0aWNpcGFudDoxNg==',
+ },
+ },
+ ],
+ },
+ image: null,
+ title: 'TSL',
+ unreadMessages: {
+ count: 0,
+ markedUnread: false,
+ id: 'VW5yZWFkTWVzc2FnZUNvdW50OjIw',
+ },
+ allMessages: {
+ totalCount: 6,
+ edges: [
+ {
+ node: {
+ id: 'TWVzc2FnZToxOQ==DKS',
+ created: '2025-03-25T03:42:31.004711+00:00',
+ profile: {
+ id: 'UHJvZmlsZToxNzsdM=',
+ name: 'Alex',
+ image: null,
+ },
+ isRead: true,
+ messageType: 'USER_MESSAGE',
+ content: 'Hey guys!',
+ deleted: false,
+ extraData: null,
+ inReplyTo: null,
+ pk: 20,
+ verb: 'SENT_MESSAGE',
+ __typename: 'Message',
+ },
+ cursor: 'YXJyYXljb25uZWN0zxaW9uOjA=',
+ },
+ {
+ node: {
+ id: 'TWVzc2FnZToxOQ==',
+ created: '2025-03-24T03:42:31.004711+00:00',
+ profile: {
+ id: 'UHJvZmlsZToxNzM=',
+ name: 'John',
+ image: null,
+ },
+ isRead: true,
+ messageType: 'USER_MESSAGE',
+ content: 'Hello Priscilla!',
+ deleted: false,
+ extraData: null,
+ inReplyTo: null,
+ pk: 19,
+ verb: 'SENT_MESSAGE',
+ __typename: 'Message',
+ },
+ cursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ },
+ {
+ node: {
+ id: 'TWVzc2FnZToxNQ==',
+ created: '2025-03-24T01:23:19.810162+00:00',
+ profile: {
+ id: 'UHJvZmlsZToxNTM=',
+ name: 'Priscilla',
+ image: null,
+ },
+ isRead: true,
+ messageType: 'USER_MESSAGE',
+ content: 'Hello everyone',
+ deleted: false,
+ extraData: null,
+ inReplyTo: null,
+ pk: 15,
+ verb: 'SENT_MESSAGE',
+ __typename: 'Message',
+ },
+ cursor: 'YXJyYXljb25uZWN0aW9uOjQ=',
+ },
+ {
+ node: {
+ id: 'TWVzc2FnZToxNA==',
+ created: '2025-03-24T01:23:15.065540+00:00',
+ profile: null,
+ isRead: null,
+ messageType: 'SYSTEM_GENERATED',
+ content: 'Priscilla created group "TSL"',
+ deleted: false,
+ extraData: null,
+ inReplyTo: null,
+ pk: 14,
+ verb: 'SENT_MESSAGE',
+ __typename: 'Message',
+ },
+ cursor: 'YXJyYXljb25uZWN0aW9uOjU=',
+ },
+ ],
+ pageInfo: {
+ hasNextPage: false,
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjU=',
+ },
+ },
+ }),
+}
+
+export const mockResolverPrivate = {
+ ChatRoom: () => ({
+ id: 'room-123',
+ isArchived: false,
+ participantsCount: 2,
+ isGroup: false,
+ participants: {
+ edges: [
+ {
+ node: {
+ profile: {
+ id: 'UHJvZmlsZToxNzM=',
+ name: 'John',
+ image: null,
+ },
+ role: 'ADMIN',
+ id: 'Q2hhdFJvb21QYXJ0aWNpcGFudDoxMw==',
+ },
+ },
+ {
+ node: {
+ profile: {
+ id: 'UHJvZmlsZToxNTM=',
+ name: 'Priscilla',
+ image: null,
+ },
+ role: 'MEMBER',
+ id: 'Q2hhdFJvb21QYXJ0aWNpcGFudDoxMg==',
+ },
+ },
+ ],
+ },
+ image: null,
+ title: null,
+ unreadMessages: {
+ count: 0,
+ markedUnread: false,
+ id: 'VW5yZWFkTWVzc2FnZUNvdW50OjE5',
+ },
+ allMessages: {
+ totalCount: 4,
+ edges: [
+ {
+ node: {
+ id: 'TWVzc2FnZToyMQ==',
+ created: '2025-03-27T02:31:22.786185+00:00',
+ profile: {
+ id: 'UHJvZmlsZToxNTM=',
+ name: 'Priscilla',
+ image: null,
+ },
+ isRead: true,
+ messageType: 'USER_MESSAGE',
+ content: 'fine and you?',
+ deleted: false,
+ extraData: null,
+ inReplyTo: null,
+ pk: 21,
+ verb: 'SENT_MESSAGE',
+ __typename: 'Message',
+ },
+ cursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ },
+ {
+ node: {
+ id: 'TWVzc2FnZToyMA==',
+ created: '2025-03-27T02:30:01.484497+00:00',
+ profile: {
+ id: 'UHJvZmlsZToxNzM=',
+ name: 'John',
+ image: null,
+ },
+ isRead: true,
+ messageType: 'USER_MESSAGE',
+ content: 'how are you?',
+ deleted: false,
+ extraData: null,
+ inReplyTo: null,
+ pk: 20,
+ verb: 'SENT_MESSAGE',
+ __typename: 'Message',
+ },
+ cursor: 'YXJyYXljb25uZWN0aW9uOjE=',
+ },
+ {
+ node: {
+ id: 'TWVzc2FnZToxMw==',
+ created: '2025-03-24T01:15:25.749043+00:00',
+ profile: {
+ id: 'UHJvZmlsZToxNTM=',
+ name: 'Priscilla',
+ image: null,
+ },
+ isRead: true,
+ messageType: 'USER_MESSAGE',
+ content: 'This message was deleted',
+ deleted: true,
+ extraData: null,
+ inReplyTo: null,
+ pk: 13,
+ verb: 'SENT_MESSAGE',
+ __typename: 'Message',
+ },
+ cursor: 'YXJyYXljb25uZWN0aW9uOjI=',
+ },
+ {
+ node: {
+ id: 'TWVzc2FnZToxMg==',
+ created: '2025-03-24T01:14:45.029307+00:00',
+ profile: {
+ id: 'UHJvZmlsZToxNzM=',
+ name: 'John',
+ image: null,
+ },
+ isRead: true,
+ messageType: 'USER_MESSAGE',
+ content: 'Hello, Priscilla!',
+ deleted: false,
+ extraData: null,
+ inReplyTo: null,
+ pk: 12,
+ verb: 'SENT_MESSAGE',
+ __typename: 'Message',
+ },
+ cursor: 'YXJyYXljb25uZWN0aW9uOjM=',
+ },
+ ],
+ pageInfo: {
+ hasNextPage: false,
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjM=',
+ },
+ },
+ }),
+}
diff --git a/packages/components/modules/messages/web/ChatRoom/__storybook__/stories.tsx b/packages/components/modules/messages/web/ChatRoom/__storybook__/stories.tsx
new file mode 100644
index 00000000..b876a00a
--- /dev/null
+++ b/packages/components/modules/messages/web/ChatRoom/__storybook__/stories.tsx
@@ -0,0 +1,50 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import { CommentReplyProvider } from '../../../../comments/common'
+import { ChatRoomProvider } from '../../../common'
+import ChatRoomWithQuery from './ChatRoomWithQuery'
+import { mockResolverGroup, mockResolverPrivate } from './mockResolvers'
+
+const meta: Meta = {
+ title: '@baseapp-frontend | components/Messages/ChatRoom',
+ component: ChatRoomWithQuery,
+ decorators: [
+ (Story) => (
+
+
+
+
+
+
+
+ ),
+ ],
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const GroupChat: Story = {
+ name: 'Group Chat',
+ parameters: {
+ mockResolvers: mockResolverGroup,
+ initialProfile: {
+ id: 'UHJvZmlsZToxNzM=',
+ name: 'John',
+ image: '',
+ },
+ },
+}
+
+export const PrivateChat: Story = {
+ name: 'Private Chat',
+ parameters: {
+ mockResolvers: mockResolverPrivate,
+ initialProfile: {
+ id: 'UHJvZmlsZToxNzM=',
+ name: 'John',
+ image: '',
+ },
+ },
+}
diff --git a/packages/components/modules/messages/web/MessagesList/MessagesGroup/UserMessage/MessageItem/__storybook__/MessageItem.mdx b/packages/components/modules/messages/web/MessagesList/MessagesGroup/UserMessage/MessageItem/__storybook__/MessageItem.mdx
new file mode 100644
index 00000000..eb390d91
--- /dev/null
+++ b/packages/components/modules/messages/web/MessagesList/MessagesGroup/UserMessage/MessageItem/__storybook__/MessageItem.mdx
@@ -0,0 +1,37 @@
+import { Meta } from '@storybook/addon-docs'
+
+
+
+# Component Documentation
+
+## MessageItem
+
+- **Purpose**: Displays an individual chat message within a conversation, supporting editing, deletion, and context actions (copy/download).
+- **Expected Behavior**: Shows the message content with conditional styling depending on ownership and deletion state. Enables editing and deletion for the current user’s own messages. Supports contextual menu for actions like copy or download.
+
+## Use Cases
+
+- **Current Usage**: Used in chat interfaces where messages from different users are shown in sequence.
+- **Potential Usage**: Could be adapted to any messaging UI that needs per-message interaction like forums, comments, or support chats.
+
+## Props
+
+- **messageRef** (`MessageFragment`): A Relay fragment reference containing the message data.
+- **isFirstGroupedMessage** (`boolean`): If the message is the first in a group (used to apply spacing/avatars).
+- **isGroup** (`boolean`, optional): Whether the message is in a group conversation.
+
+## Notes
+
+- **Related Components**:
+ - `ActionsOverlay` – Wraps the message with a contextual action menu
+ - `MessageUpdate` – Renders the editable message input
+
+## Example Usage
+
+```tsx
+const MyComponent = () => {
+ return
+}
+
+export default MyComponent
+```
diff --git a/packages/components/modules/messages/web/MessagesList/MessagesGroup/UserMessage/MessageItem/__storybook__/MessageItemWithQuery/index.tsx b/packages/components/modules/messages/web/MessagesList/MessagesGroup/UserMessage/MessageItem/__storybook__/MessageItemWithQuery/index.tsx
new file mode 100644
index 00000000..84e8d551
--- /dev/null
+++ b/packages/components/modules/messages/web/MessagesList/MessagesGroup/UserMessage/MessageItem/__storybook__/MessageItemWithQuery/index.tsx
@@ -0,0 +1,38 @@
+import { graphql, useLazyLoadQuery } from 'react-relay'
+
+import MessageItem from '../..'
+import { MessageItemFragment$key } from '../../../../../../../../../__generated__/MessageItemFragment.graphql'
+import { MessageItemWithQuery as Query } from '../../../../../../../../../__generated__/MessageItemWithQuery.graphql'
+
+interface Props {
+ isGroup?: boolean
+ isFirstGroupedMessage?: boolean
+}
+
+const MessageItemWithQuery = ({ isGroup = false, isFirstGroupedMessage = true }: Props) => {
+ const data = useLazyLoadQuery(
+ graphql`
+ query MessageItemWithQuery @relay_test_operation {
+ node(id: "msg-1") {
+ ... on Message {
+ ...MessageItemFragment
+ profile {
+ id
+ }
+ }
+ }
+ }
+ `,
+ {},
+ )
+
+ return (
+
+ )
+}
+
+export default MessageItemWithQuery
diff --git a/packages/components/modules/messages/web/MessagesList/MessagesGroup/UserMessage/MessageItem/__storybook__/mockResolvers.ts b/packages/components/modules/messages/web/MessagesList/MessagesGroup/UserMessage/MessageItem/__storybook__/mockResolvers.ts
new file mode 100644
index 00000000..7c39568f
--- /dev/null
+++ b/packages/components/modules/messages/web/MessagesList/MessagesGroup/UserMessage/MessageItem/__storybook__/mockResolvers.ts
@@ -0,0 +1,15 @@
+export const mockResolvers = {
+ Node: () => ({
+ __typename: 'Message',
+ id: 'msg-1',
+ content: 'This is a mock message',
+ created: new Date().toISOString(),
+ deleted: false,
+ extraData: null,
+ inReplyTo: { id: 'msg-0' },
+ isRead: true,
+ pk: 12345,
+ profile: { id: 'profile-123' },
+ verb: 'SENT_MESSAGE',
+ }),
+}
diff --git a/packages/components/modules/messages/web/MessagesList/MessagesGroup/UserMessage/MessageItem/__storybook__/stories.tsx b/packages/components/modules/messages/web/MessagesList/MessagesGroup/UserMessage/MessageItem/__storybook__/stories.tsx
new file mode 100644
index 00000000..ed81bcd0
--- /dev/null
+++ b/packages/components/modules/messages/web/MessagesList/MessagesGroup/UserMessage/MessageItem/__storybook__/stories.tsx
@@ -0,0 +1,96 @@
+import { Meta, StoryObj } from '@storybook/react'
+
+import MessageItemWithQuery from './MessageItemWithQuery'
+import { mockResolvers } from './mockResolvers'
+
+const meta: Meta = {
+ title: '@baseapp-frontend | components/Messages/MessageItem',
+ component: MessageItemWithQuery,
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Default: Story = {
+ name: 'Default Message',
+ parameters: {
+ mockResolvers,
+ },
+}
+
+export const DeletedMessage: Story = {
+ name: 'Deleted Message',
+ parameters: {
+ mockResolvers: {
+ Node: () => ({
+ ...mockResolvers.Node(),
+ deleted: true,
+ content: '[This message was removed]',
+ }),
+ },
+ },
+}
+
+export const GroupedMessage: Story = {
+ name: 'Grouped Message (not first)',
+ args: {
+ isGroup: true,
+ isFirstGroupedMessage: false,
+ },
+ parameters: {
+ mockResolvers: {
+ Node: () => ({
+ ...mockResolvers.Node(),
+ content: 'Not first grouped message',
+ }),
+ },
+ },
+}
+
+export const OwnMessage: Story = {
+ name: 'Own Message',
+ args: {
+ isFirstGroupedMessage: false,
+ },
+ parameters: {
+ mockResolvers: {
+ Node: () => ({
+ ...mockResolvers.Node(),
+ content: 'Default Message',
+ }),
+ },
+ initialProfile: {
+ id: 'profile-123',
+ name: 'Profile Name',
+ image: '',
+ urlPath: 'profile',
+ },
+ },
+}
+
+export const OwnDeletedMessage: Story = {
+ name: 'Own Deleted Message',
+ parameters: {
+ mockResolvers: {
+ Node: () => ({
+ ...mockResolvers.Node(),
+ deleted: true,
+ content: '[This message was removed]',
+ }),
+ },
+ initialProfile: {
+ id: 'profile-123',
+ name: 'Profile Name',
+ image: '',
+ urlPath: 'profile',
+ },
+ },
+}
diff --git a/packages/components/modules/messages/web/MessagesList/MessagesGroup/__storybook__/MessagesGroup.mdx b/packages/components/modules/messages/web/MessagesList/MessagesGroup/__storybook__/MessagesGroup.mdx
new file mode 100644
index 00000000..62091335
--- /dev/null
+++ b/packages/components/modules/messages/web/MessagesList/MessagesGroup/__storybook__/MessagesGroup.mdx
@@ -0,0 +1,55 @@
+import { Meta } from '@storybook/addon-docs'
+
+
+
+# Component Documentation
+
+## MessagesGroup
+
+- **Purpose**: Responsible for rendering grouped chat messages by type (user/system), date, and read status.
+- **Expected Behavior**: Displays message blocks separated by date headers and an "unread" divider when applicable. Differentiates between system and user messages using customizable components.
+
+## Use Cases
+
+- **Current Usage**: Used inside `MessagesList` to group and format each message node.
+- **Potential Usage**: Any message-driven timeline (e.g. comments, notifications) where temporal grouping or visual separation is useful.
+
+## Props
+
+- **allMessages** (`MessageNode[]`): Complete list of message nodes to render in context.
+- **message** (`MessageNode`): The specific message node to be rendered by this group.
+- **messageIndex** (`number`): The index of the current message in the full message list.
+- **isGroup** (`boolean`, optional): Whether the room is a group chat. Affects visual display (like names/avatars).
+- **allMessagesLastIndex** (`number`): Index of the last message, for determining date headers.
+- **firstUnreadMessageId** (`string | null`, optional): Used to insert an "Unread" divider at the correct position.
+- **hasNext** (`boolean`): Whether there are more messages available to paginate. Used in rendering date headers correctly.
+- **SystemMessage** (`component`, optional): Custom renderer for system messages. Defaults to `DefaultSystemMessage`.
+- **SystemMessageProps** (`object`, optional): Additional props for the `SystemMessage` component.
+- **UserMessage** (`component`, optional): Custom renderer for user messages. Defaults to `DefaultUserMessage`.
+- **UserMessageProps** (`object`, optional): Additional props for the `UserMessage` component.
+
+## Notes
+
+- **Related Components**:
+ - `MessagesList` – Parent list component that uses `MessagesGroup` to render each message.
+ - `UserMessage` – Handles rendering of user-submitted messages with avatar/name logic.
+ - `SystemMessage` – Handles rendering of system-generated messages like "User joined".
+
+## Example Usage
+
+```tsx
+const MyComponent = () => {
+ return (
+
+ )
+}
+
+export default MyComponent
+```
diff --git a/packages/components/modules/messages/web/MessagesList/MessagesGroup/__storybook__/MessagesGroupWithQuery/index.tsx b/packages/components/modules/messages/web/MessagesList/MessagesGroup/__storybook__/MessagesGroupWithQuery/index.tsx
new file mode 100644
index 00000000..9aad1fec
--- /dev/null
+++ b/packages/components/modules/messages/web/MessagesList/MessagesGroup/__storybook__/MessagesGroupWithQuery/index.tsx
@@ -0,0 +1,64 @@
+import { graphql, useLazyLoadQuery } from 'react-relay'
+
+import MessagesGroup from '../..'
+import { MessagesGroupWithQuery as Query } from '../../../../../../../__generated__/MessagesGroupWithQuery.graphql'
+import { MESSAGE_TYPE } from '../../../../../common'
+
+const MessagesGroupWithQuery = () => {
+ const data = useLazyLoadQuery(
+ graphql`
+ query MessagesGroupWithQuery @relay_test_operation {
+ node(id: "msg-1") {
+ __typename
+ ... on Message {
+ id
+ messageType
+ created
+ profile {
+ id
+ name
+ image(width: 32, height: 32) {
+ url
+ }
+ }
+ isRead
+ ...MessageItemFragment
+ }
+ }
+ }
+ `,
+ {},
+ )
+
+ // eslint-disable-next-line no-underscore-dangle
+ if (!data.node || data.node.__typename !== 'Message') return null
+
+ const allMessages = [
+ {
+ ...data.node,
+ id: 'msg-0',
+ messageType: MESSAGE_TYPE.user,
+ created: new Date().toISOString(),
+ profile: {
+ id: 'profile-456',
+ name: 'Alice',
+ image: { url: '' },
+ },
+ isRead: true,
+ },
+ data.node,
+ ]
+
+ return (
+
+ )
+}
+
+export default MessagesGroupWithQuery
diff --git a/packages/components/modules/messages/web/MessagesList/MessagesGroup/__storybook__/mockResolvers.ts b/packages/components/modules/messages/web/MessagesList/MessagesGroup/__storybook__/mockResolvers.ts
new file mode 100644
index 00000000..aa65ee02
--- /dev/null
+++ b/packages/components/modules/messages/web/MessagesList/MessagesGroup/__storybook__/mockResolvers.ts
@@ -0,0 +1,22 @@
+export const mockResolvers = {
+ Node: () => ({
+ __typename: 'Message',
+ id: 'msg-1',
+ content: 'Hello',
+ created: new Date().toISOString(),
+ deleted: false,
+ extraData: null,
+ inReplyTo: null,
+ isRead: false,
+ pk: 1,
+ verb: 'SENT_MESSAGE',
+ profile: {
+ id: 'profile-123',
+ name: 'Você',
+ image: {
+ url: '',
+ },
+ },
+ messageType: 'USER_MESSAGE',
+ }),
+}
diff --git a/packages/components/modules/messages/web/MessagesList/MessagesGroup/__storybook__/stories.tsx b/packages/components/modules/messages/web/MessagesList/MessagesGroup/__storybook__/stories.tsx
new file mode 100644
index 00000000..1ec49ece
--- /dev/null
+++ b/packages/components/modules/messages/web/MessagesList/MessagesGroup/__storybook__/stories.tsx
@@ -0,0 +1,67 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import MessagesGroupWithQuery from './MessagesGroupWithQuery'
+import { mockResolvers } from './mockResolvers'
+
+const meta: Meta = {
+ title: '@baseapp-frontend | components/Messages/MessagesGroup',
+ component: MessagesGroupWithQuery,
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Default: Story = {
+ name: 'Own Message',
+ parameters: {
+ mockResolvers,
+ initialProfile: {
+ id: 'profile-123',
+ name: 'Profile',
+ },
+ },
+}
+
+export const FromOtherUser: Story = {
+ name: 'Group Member Message',
+ parameters: {
+ mockResolvers: {
+ Node: () => ({
+ ...mockResolvers.Node(),
+ content: 'Hello, how are you?',
+ profile: {
+ id: 'profile-456',
+ name: 'Alice',
+ image: { url: '' },
+ },
+ messageType: 'USER_MESSAGE',
+ }),
+ },
+ initialProfile: {
+ id: 'profile-123',
+ name: 'Profile',
+ },
+ },
+}
+
+export const SystemMessage: Story = {
+ name: 'System Message',
+ parameters: {
+ mockResolvers: {
+ Node: () => ({
+ ...mockResolvers.Node(),
+ content: 'David created group "TSL"',
+ messageType: 'SYSTEM_GENERATED',
+ profile: null,
+ }),
+ },
+ },
+}
diff --git a/packages/components/modules/messages/web/MessagesList/__storybook__/MessageList.mdx b/packages/components/modules/messages/web/MessagesList/__storybook__/MessageList.mdx
new file mode 100644
index 00000000..bdd16ac3
--- /dev/null
+++ b/packages/components/modules/messages/web/MessagesList/__storybook__/MessageList.mdx
@@ -0,0 +1,40 @@
+import { Meta } from '@storybook/addon-docs'
+
+
+
+# Component Documentation
+
+## MessagesList
+
+- **Purpose**: Displays a scrollable list of chat messages with grouping.
+- **Expected Behavior**: Loads chat messages in reverse chronological order, supports infinite scroll, groups messages by sender and day, and automatically marks messages as read when viewed.
+
+## Use Cases
+
+- **Current Usage**: Embedded in `ChatRoom` to show the full conversation.
+- **Potential Usage**: Can be adapted for threaded comment systems, activity feeds, or any scrollable message-based UI.
+
+## Props
+
+- **roomRef** (`MessagesListFragment$key`): Relay fragment reference for the current chat room data.
+- **VirtuosoProps** (object, optional): Props passed down to the `react-virtuoso` virtual scroller.
+- **MessagesGroup** (component, optional): Custom message grouping component. Defaults to `DefaultMessagesGroup`.
+- **MessagesGroupProps** (object, optional): Additional props for the message group renderer.
+
+## Notes
+
+- **Related Components**:
+ - `MessagesGroup` – Responsible for rendering grouped message blocks.
+ - `ChatRoom` – The parent component that uses this to display chat history.
+ - `LoadingState` – Loading spinner shown when paginating messages.
+ - `Virtuoso` – Virtualized list renderer for efficient scrolling.
+
+## Example Usage
+
+```tsx
+const MyComponent = () => {
+ return
+}
+
+export default MyComponent
+```
diff --git a/packages/components/modules/messages/web/MessagesList/__storybook__/MessageListWithQuery/index.tsx b/packages/components/modules/messages/web/MessagesList/__storybook__/MessageListWithQuery/index.tsx
new file mode 100644
index 00000000..144b6644
--- /dev/null
+++ b/packages/components/modules/messages/web/MessagesList/__storybook__/MessageListWithQuery/index.tsx
@@ -0,0 +1,22 @@
+import { graphql, useLazyLoadQuery } from 'react-relay'
+
+import MessagesList from '../..'
+import { MessageListWithQuery as Query } from '../../../../../../__generated__/MessageListWithQuery.graphql'
+import { MessagesListFragment$key } from '../../../../../../__generated__/MessagesListFragment.graphql'
+
+const MessageListWithQuery = () => {
+ const data = useLazyLoadQuery(
+ graphql`
+ query MessageListWithQuery @relay_test_operation {
+ chatRoom(id: "room-123") {
+ ...MessagesListFragment
+ }
+ }
+ `,
+ {},
+ )
+
+ return
+}
+
+export default MessageListWithQuery
diff --git a/packages/components/modules/messages/web/MessagesList/__storybook__/mockResolvers.ts b/packages/components/modules/messages/web/MessagesList/__storybook__/mockResolvers.ts
new file mode 100644
index 00000000..cf5d9223
--- /dev/null
+++ b/packages/components/modules/messages/web/MessagesList/__storybook__/mockResolvers.ts
@@ -0,0 +1,79 @@
+export const mockResolvers = {
+ ChatRoom: () => ({
+ id: 'room-123',
+ isGroup: true,
+ unreadMessages: {
+ count: 1,
+ markedUnread: false,
+ },
+ allMessages: {
+ totalCount: 3,
+ edges: [
+ {
+ node: {
+ id: 'msg-3',
+ created: new Date().toISOString(),
+ profile: {
+ id: 'profile-456',
+ name: 'Alice',
+ image: {
+ url: '',
+ },
+ },
+ isRead: false,
+ messageType: 'USER_MESSAGE',
+ content: 'Me too! How was your day?',
+ deleted: false,
+ extraData: null,
+ inReplyTo: null,
+ verb: 'SENT_MESSAGE',
+ },
+ },
+ {
+ node: {
+ id: 'msg-2',
+ created: new Date().toISOString(),
+ profile: {
+ id: 'profile-123',
+ name: 'Você',
+ image: {
+ url: '',
+ },
+ },
+ isRead: true,
+ messageType: 'USER_MESSAGE',
+ content: "I'm fine! And you?",
+ deleted: false,
+ extraData: null,
+ inReplyTo: null,
+ verb: 'SENT_MESSAGE',
+ },
+ },
+ {
+ node: {
+ id: 'msg-1',
+ created: new Date().toISOString(),
+ profile: {
+ id: 'profile-456',
+ name: 'Alice',
+ image: {
+ url: '',
+ },
+ },
+ isRead: true,
+ messageType: 'USER_MESSAGE',
+ content: 'Hey, how are you?',
+ deleted: false,
+ extraData: null,
+ inReplyTo: null,
+ verb: 'SENT_MESSAGE',
+ },
+ },
+ ],
+ pageInfo: {
+ hasNextPage: false,
+ endCursor: null,
+ },
+ },
+ }),
+}
diff --git a/packages/components/modules/messages/web/MessagesList/__storybook__/stories.tsx b/packages/components/modules/messages/web/MessagesList/__storybook__/stories.tsx
new file mode 100644
index 00000000..d183fc12
--- /dev/null
+++ b/packages/components/modules/messages/web/MessagesList/__storybook__/stories.tsx
@@ -0,0 +1,47 @@
+import type { Meta, StoryObj } from '@storybook/react'
+import { create } from 'zustand'
+
+import { UseChatRoom } from '../../../common'
+import { ChatRoomContext } from '../../../common/context/ChatRoomProvider'
+import MessageListWithQuery from './MessageListWithQuery'
+import { mockResolvers } from './mockResolvers'
+
+const mockChatRoomStore = create((set) => ({
+ id: 'room-123',
+ setChatRoom: (newState) => set(newState),
+ resetChatRoom: () => set({ id: '' }),
+}))
+
+const meta: Meta = {
+ title: '@baseapp-frontend | components/Messages/MessageList',
+ component: MessageListWithQuery,
+ decorators: [
+ (Story) => (
+
+
+
+
+
+ ),
+ ],
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Default: Story = {
+ name: 'MessagesList (default)',
+ parameters: {
+ mockResolvers,
+ initialProfile: {
+ id: 'profile-123',
+ name: 'Profile Name',
+ image: '',
+ urlPath: 'profile',
+ },
+ chatRoom: {
+ id: 'room-123',
+ },
+ },
+}
diff --git a/packages/components/package.json b/packages/components/package.json
index 51af24e1..622f69cb 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -1,7 +1,7 @@
{
"name": "@baseapp-frontend/components",
"description": "BaseApp components modules such as comments, notifications, messages, and more.",
- "version": "1.0.34",
+ "version": "1.0.35",
"sideEffects": false,
"scripts": {
"babel:transpile": "babel modules -d tmp-babel --extensions .ts,.tsx --ignore '**/__tests__/**','**/__storybook__/**'",