Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,12 @@ import { ChatRoomState } from './types'
export const INITIAL_CHAT_ROOM_STATE: ChatRoomState = {
id: undefined,
participants: undefined,
leftPanelContent: 0,
singleChatProfileDetails: {
pk: undefined,
name: undefined,
username: undefined,
imageUrl: undefined,
biography: undefined,
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ const ChatRoomProvider: FC<PropsWithChildren> = ({ children }) => {
if (!storeRef.current) {
storeRef.current = create<UseChatRoom>((set) => ({
...INITIAL_CHAT_ROOM_STATE,

setChatRoom: (state: ChatRoomState) => set(state),
resetChatRoom: () => set({ ...INITIAL_CHAT_ROOM_STATE }),
setLeftPanelContent: (content: number) => set({ leftPanelContent: content }),
setSingleChatProfileDetails: (details) => set({ singleChatProfileDetails: { ...details } }),
}))
}
return <ChatRoomContext.Provider value={storeRef.current}>{children}</ChatRoomContext.Provider>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
export type ChatRoomState = {
id?: string
participants?: (string | null | undefined)[]
leftPanelContent?: number
singleChatProfileDetails?: {
pk: number | undefined
name: string | null | undefined
username: string | null | undefined
imageUrl: string | null | undefined
biography?: string | null | undefined
}
Comment on lines +5 to +11
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix type inconsistency between state and setter.

There's a mismatch between the state type and the setter parameter:

  • State allows name: string | null | undefined (lines 7-9)
  • Setter parameter only allows name: string | undefined (line 23)

This inconsistency can cause issues:

  • Type errors at call sites when passing potentially-null values
  • Confusion about whether null is a valid value
  • The state could still receive null via setChatRoom, bypassing the setter's type restriction

Apply this diff to align the types:

   setSingleChatProfileDetails: (details: {
     pk: number | undefined
-    name: string | undefined
-    username: string | undefined
-    imageUrl: string | undefined
+    name: string | null | undefined
+    username: string | null | undefined
+    imageUrl: string | null | undefined
     biography?: string | undefined
   }) => void

Or, if nulls should never be allowed, update the state type to remove | null.

Also applies to: 21-27

🤖 Prompt for AI Agents
In packages/components/modules/messages/common/context/ChatRoomProvider/types.ts
around lines 5-11 (and similarly for lines 21-27), the state type for
singleChatProfileDetails permits null for fields like
name/username/imageUrl/biography but the setter parameter does not, causing a
type mismatch; resolve by making the setter parameter types mirror the state
(add | null to name, username, imageUrl, biography and any other nullable
fields) OR remove | null from the state type if null should never be
allowed—ensure both the state and setter signatures use the exact same nullable
union types.

}

type ChatRoomFunctions = {
Expand All @@ -9,6 +17,14 @@ type ChatRoomFunctions = {
replace?: boolean | undefined,
) => void
resetChatRoom: () => void
setLeftPanelContent: (content: number) => void
Copy link
Collaborator

Choose a reason for hiding this comment

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

lets use:
export type LeftPanelContentValues = ValueOf<typeof LEFT_PANEL_CONTENT> (move this type over to here)
instead of number

setSingleChatProfileDetails: (details: {
pk: number | undefined
name: string | undefined
username: string | undefined
imageUrl: string | undefined
biography?: string | undefined
}) => void
}

export type UseChatRoom = ChatRoomState & ChatRoomFunctions
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@ export const RoomTitleFragment = graphql`
node {
profile {
id
pk
name
image(width: 100, height: 100) {
image(width: 128, height: 128) {
url
}
biography
urlPath {
path
}
}
role
}
Expand Down
17 changes: 13 additions & 4 deletions packages/components/modules/messages/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@ export const useGroupNameAndAvatar = (
headerRef as GroupTitleFragment$key,
)
return {
pk: undefined,
path: undefined,
biography: undefined,
title: header?.title,
avatar: header?.image?.url,
}
}

const useRoomNameAndAvatar = (headerRef: RoomTitleFragment$key | null | undefined) => {
export const useSingleChatDetails = (headerRef: RoomTitleFragment$key | null | undefined) => {
const { currentProfile } = useCurrentProfile()
const header = useFragment<RoomTitleFragment$key>(RoomTitleFragment, headerRef)
if (!header?.participants) {
Expand All @@ -39,22 +42,28 @@ const useRoomNameAndAvatar = (headerRef: RoomTitleFragment$key | null | undefine
)
if (otherParticipant === undefined) {
return {
pk: undefined,
title: 'Deleted User',
avatar: undefined,
path: undefined,
biography: undefined,
}
}

return {
pk: otherParticipant?.node?.profile?.pk,
title: otherParticipant?.node?.profile?.name,
avatar: otherParticipant?.node?.profile?.image?.url,
path: otherParticipant?.node?.profile?.urlPath?.path,
biography: otherParticipant?.node?.profile?.biography,
}
}

export const useNameAndAvatar = (roomHeader: TitleFragment$data) => {
const roomNameAndAvatar = useRoomNameAndAvatar(roomHeader)
export const useChatroomDetails = (roomHeader: TitleFragment$data) => {
const singleChatDetails = useSingleChatDetails(roomHeader)
const groupNameAndAvatar = useGroupNameAndAvatar(roomHeader)
if (roomHeader.isGroup) return groupNameAndAvatar
return roomNameAndAvatar
return singleChatDetails
}

export const getParticipantCountString = (participantCount: number | null | undefined) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
TitleFragment,
UnreadMessagesCountFragment,
useArchiveChatRoomMutation,
useNameAndAvatar,
useChatroomDetails,
useUnreadChatMutation,
} from '../../../common'
import { StyledChatCard } from './styled'
Expand Down Expand Up @@ -52,7 +52,7 @@ const ChatRoomItem: FC<ChatRoomItemProps> = ({
const chatCardRef = useRef<HTMLDivElement>(null)

const { currentProfile } = useCurrentProfile()
const { title, avatar } = useNameAndAvatar(headerFragment)
const { title, avatar } = useChatroomDetails(headerFragment)

const { lastMessageTime } = lastMessageFragment
const lastMessage = lastMessageFragment.lastMessage?.content
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,17 @@ const ChatRoomOptions: FC<ChatRoomOptionsProps> = ({
onArchiveClicked,
onDetailsClicked,
onLeaveClicked,
onContactDetailsClicked,
}) => (
<MenuList>
<MenuItem onClick={onArchiveClicked} disabled={isArchiveMutationInFlight}>
<Typography variant="body2">{isArchived ? 'Unarchive Chat' : 'Archive Chat'}</Typography>
</MenuItem>
{!isGroup && (
<MenuItem onClick={onContactDetailsClicked}>
<Typography variant="body2">Contact Details</Typography>
</MenuItem>
)}
{isGroup ? (
<>
<MenuItem onClick={onDetailsClicked}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export interface ChatRoomOptionsProps {
onArchiveClicked: () => void
onDetailsClicked: () => void
onLeaveClicked: () => void
onContactDetailsClicked: () => void
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC, useState } from 'react'
import { FC, useEffect, useState } from 'react'

import { useCurrentProfile } from '@baseapp-frontend/authentication'
import { AvatarWithPlaceholder } from '@baseapp-frontend/design-system/components/web/avatars'
Expand All @@ -20,10 +20,11 @@ import {
getParticipantCountString,
useArchiveChatRoomMutation,
useChatRoom,
useChatroomDetails,
useCheckIsAdmin,
useNameAndAvatar,
} from '../../../common'
import { RoomTitleFragment } from '../../../common/graphql/fragments/RoomTitle'
import { LEFT_PANEL_CONTENT } from '../../ChatRoomsComponent/constants'
import LeaveGroupDialog from '../../__shared__/LeaveGroupDialog'
import ChatRoomOptions from './ChatRoomOptions'
import { BackButtonContainer, ChatHeaderContainer, ChatTitleContainer } from './styled'
Expand All @@ -37,14 +38,28 @@ const ChatRoomHeader: FC<ChatRoomHeaderProps> = ({
roomId,
}) => {
const roomHeader = useFragment(TitleFragment, roomTitleRef)

const [open, setOpen] = useState(false)
const { currentProfile } = useCurrentProfile()

const isUpToMd = useResponsive('up', 'md')
const { resetChatRoom } = useChatRoom()
const { resetChatRoom, setSingleChatProfileDetails, setLeftPanelContent } = useChatRoom()

const { isGroup } = roomHeader
const { title, avatar } = useNameAndAvatar(roomHeader)
const { title, avatar, path, pk, biography } = useChatroomDetails(roomHeader)

useEffect(() => {
if (!isGroup) {
setSingleChatProfileDetails({
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think that the best approach here would be the ProfileSummary itself calling its own Fragment to get the data it needs and then register this fragment under GroupDetailsQuery defined at ChatRoomsComponent.
By doing that we dont need to store anything in this state provider and we keep in sync with the relay philosophy in which we specify the data requirements of a component by using fragments

pk: pk ?? undefined,
name: title ?? '',
username: path ?? '',
imageUrl: avatar ?? '',
biography: biography ?? '',
})
}
}, [pk, title, path, avatar, biography])
Comment on lines +51 to +61
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix missing dependencies in useEffect.

The effect is missing isGroup and setSingleChatProfileDetails in its dependency array. This violates the exhaustive-deps rule and can cause:

  • Stale closures if setSingleChatProfileDetails changes
  • The effect not re-running when isGroup toggles
  • Unpredictable behavior when room type changes

Apply this diff to fix the dependencies:

   useEffect(() => {
     if (!isGroup) {
       setSingleChatProfileDetails({
         pk: pk ?? undefined,
         name: title ?? '',
         username: path ?? '',
         imageUrl: avatar ?? '',
         biography: biography ?? '',
       })
     }
-  }, [pk, title, path, avatar, biography])
+  }, [pk, title, path, avatar, biography, isGroup, setSingleChatProfileDetails])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (!isGroup) {
setSingleChatProfileDetails({
pk: pk ?? undefined,
name: title ?? '',
username: path ?? '',
imageUrl: avatar ?? '',
biography: biography ?? '',
})
}
}, [pk, title, path, avatar, biography])
useEffect(() => {
if (!isGroup) {
setSingleChatProfileDetails({
pk: pk ?? undefined,
name: title ?? '',
username: path ?? '',
imageUrl: avatar ?? '',
biography: biography ?? '',
})
}
}, [pk, title, path, avatar, biography, isGroup, setSingleChatProfileDetails])
🤖 Prompt for AI Agents
In packages/components/modules/messages/web/ChatRoom/ChatRoomHeader/index.tsx
around lines 51 to 61, the useEffect dependency array is incomplete: add isGroup
and setSingleChatProfileDetails to the dependency list so the effect re-runs
when room type or the setter changes and to satisfy exhaustive-deps; update the
dependency array accordingly to include [pk, title, path, avatar, biography,
isGroup, setSingleChatProfileDetails].


const { participants } = useFragment<RoomTitleFragment$key>(RoomTitleFragment, roomHeader)
const { isSoleAdmin } = useCheckIsAdmin(participants as MembersListFragment$data['participants'])
const members = getParticipantCountString(participantsCount)
Expand Down Expand Up @@ -148,6 +163,10 @@ const ChatRoomHeader: FC<ChatRoomHeaderProps> = ({
popover.onClose()
setOpen(true)
}}
onContactDetailsClicked={() => {
popover.onClose()
setLeftPanelContent(LEFT_PANEL_CONTENT.profileSummary)
}}
/>
</Popover>
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export const LEFT_PANEL_CONTENT = {
createGroupChat: 2,
editGroupChat: 3,
groupDetails: 4,
profileSummary: 5,
} as const
Copy link
Collaborator

Choose a reason for hiding this comment

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

lets move this closer to the useChatRoom/ChatRoomProvider context, since we moved this logic there

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { FC, useState } from 'react'
import { FC } from 'react'

import { useResponsive } from '@baseapp-frontend/design-system/hooks/web'

Expand All @@ -13,10 +13,11 @@ import ChatRoom from '../ChatRoom'
import DefaultGroupChatCreate from '../GroupChatCreate'
import DefaultGroupChatDetails from '../GroupChatDetails'
import DefaultGroupChatEdit from '../GroupChatEdit'
import DefaultProfileSummary from '../ProfileSummary'
import DefaultSingleChatCreate from '../SingleChatCreate'
import { LEFT_PANEL_CONTENT } from './constants'
import { ChatRoomContainer, ChatRoomsContainer, ChatRoomsListContainer } from './styled'
import { ChatRoomsComponentProps, LeftPanelContentValues } from './types'
import { ChatRoomsComponentProps } from './types'

const ChatRoomsComponent: FC<ChatRoomsComponentProps> = ({
chatRoomsQueryData,
Expand All @@ -31,15 +32,14 @@ const ChatRoomsComponent: FC<ChatRoomsComponentProps> = ({
GroupChatEditComponentProps = {},
SingleChatCreateComponent = DefaultSingleChatCreate,
SingleChatCreateComponentProps = {},
ProfileSummaryComponent = DefaultProfileSummary,
}) => {
const isUpToMd = useResponsive('up', 'md')
const [leftPanelContent, setLeftPanelContent] = useState<LeftPanelContentValues>(
Copy link
Collaborator

Choose a reason for hiding this comment

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

nice move! 🎉

LEFT_PANEL_CONTENT.chatRoomList,
)

const [groupDetailsQueryRef, loadGroupDetailsQuery] =
useQueryLoader<GroupDetailsQueryType>(GroupDetailsQuery)
const { id: selectedRoom } = useChatRoom()

const { id: selectedRoom, leftPanelContent, setLeftPanelContent } = useChatRoom()

const displayGroupDetails = () => {
if (selectedRoom) {
Expand Down Expand Up @@ -99,6 +99,12 @@ const ChatRoomsComponent: FC<ChatRoomsComponentProps> = ({
{...SingleChatCreateComponentProps}
/>
)
case LEFT_PANEL_CONTENT.profileSummary:
return (
<ProfileSummaryComponent
onBackButtonClicked={() => setLeftPanelContent(LEFT_PANEL_CONTENT.chatRoomList)}
/>
)
default:
return (
<AllChatRoomsListComponent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { AllChatRoomsListProps } from '../AllChatRoomsList/types'
import { GroupChatCreateProps } from '../GroupChatCreate/types'
import { GroupChatDetailsProps } from '../GroupChatDetails/types'
import { GroupChatEditProps } from '../GroupChatEdit/types'
import { ProfileSummaryProps } from '../ProfileSummary/types'
import { SingleChatCreateProps } from '../SingleChatCreate/types'
import { LEFT_PANEL_CONTENT } from './constants'

Expand All @@ -32,4 +33,6 @@ export interface ChatRoomsComponentProps {
GroupChatEditComponentProps?: Partial<GroupChatEditProps>
SingleChatCreateComponent?: FC<SingleChatCreateProps>
SingleChatCreateComponentProps?: Partial<SingleChatCreateProps>
ProfileSummaryComponent?: FC<ProfileSummaryProps>
ProfileSummaryComponentProps?: Partial<ProfileSummaryProps>
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const mockChatRoomStore = create<UseChatRoom>((set) => ({
id: 'room-123',
setChatRoom: (newState) => set(newState),
resetChatRoom: () => set({ id: '' }),
setLeftPanelContent: (_content) => {},
setSingleChatProfileDetails: (_details) => {},
}))

const meta: Meta<typeof MessageListWithQuery> = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { FC } from 'react'

import { CircledAvatar } from '@baseapp-frontend/design-system/components/web/avatars'
import { IconButton } from '@baseapp-frontend/design-system/components/web/buttons'
import {
NewGroupIcon,
ProfileNoCircleIcon,
} from '@baseapp-frontend/design-system/components/web/icons'
import { TypographyWithEllipsis } from '@baseapp-frontend/design-system/components/web/typographies'

import { Box, Divider, Typography } from '@mui/material'

import {
ButtonContainer,
HeaderContainer,
Subheader,
SubheaderContainer,
TitleContainer,
} from './styled'
import { BodyProps } from './types'

const Body: FC<BodyProps> = ({ avatar, avatarSize = 144, biography, username, name, pk }) => {
const formattedUsername = username ? username.replace(/^\/+/, '') : ''
const profilePath = username ?? `/profile/${pk}`
Comment on lines +23 to +24
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix: Handle undefined pk in profilePath fallback.

If both username and pk are undefined/null, profilePath will be /profile/undefined, causing a broken link.

-  const formattedUsername = username ? username.replace(/^\/+/, '') : ''
-  const profilePath = username ?? `/profile/${pk}`
+  const formattedUsername = username ? username.replace(/^\/+/, '') : ''
+  const profilePath = username ?? (pk ? `/profile/${pk}` : undefined)

Then add a guard to disable the "Go to profile" button when profilePath is undefined:

   <IconButton
     size="small"
     aria-label="go to profile"
     onClick={() => window.open(profilePath, '_blank')}
     sx={{ maxWidth: 'fit-content', gap: '8px' }}
+    disabled={!profilePath}
   >

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/components/modules/messages/web/ProfileSummary/Body/index.tsx around
lines 17 to 18, the fallback builds `/profile/${pk}` even when pk is undefined
which yields `/profile/undefined`; change the logic so profilePath is undefined
if neither username nor pk exist (e.g., set profilePath to the cleaned username
if present, otherwise to `/profile/${pk}` only when pk is defined, else
undefined). After that, guard the "Go to profile" button by disabling or hiding
it when profilePath is undefined so it cannot render a broken link.

return (
<Box sx={{ display: 'grid', gridTemplateRows: 'auto 1fr' }}>
<HeaderContainer>
<CircledAvatar src={avatar} width={avatarSize} height={avatarSize} hasError={false} />
<TitleContainer>
<TypographyWithEllipsis variant="subtitle1" color="text.primary">
{name}
</TypographyWithEllipsis>
<TypographyWithEllipsis variant="body2" color="text.secondary">
{`@${formattedUsername}`}
</TypographyWithEllipsis>
Comment on lines +33 to +35
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Minor: Display empty state when username is missing.

When username is empty or undefined, the component displays @ which looks incomplete. Consider showing a placeholder or omitting the username row entirely.

-          <TypographyWithEllipsis variant="body2" color="text.secondary">
-            {`@${formattedUsername}`}
-          </TypographyWithEllipsis>
+          {formattedUsername && (
+            <TypographyWithEllipsis variant="body2" color="text.secondary">
+              {`@${formattedUsername}`}
+            </TypographyWithEllipsis>
+          )}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<TypographyWithEllipsis variant="body2" color="text.secondary">
{`@${formattedUsername}`}
</TypographyWithEllipsis>
{formattedUsername && (
<TypographyWithEllipsis variant="body2" color="text.secondary">
{`@${formattedUsername}`}
</TypographyWithEllipsis>
)}
🤖 Prompt for AI Agents
In packages/components/modules/messages/web/ProfileSummary/Body/index.tsx around
lines 27 to 29, the code renders `@${formattedUsername}` unconditionally which
yields a lone "@" when username is missing; update the render to handle
empty/undefined usernames by either omitting the entire TypographyWithEllipsis
row when formattedUsername is falsy, or rendering a clear placeholder (e.g.,
"Username unavailable" or "—") instead of `@`. Implement the check around the
JSX so the UI shows a sensible empty state rather than just "@".

</TitleContainer>
</HeaderContainer>
<SubheaderContainer>
<ButtonContainer>
<IconButton
size="small"
aria-label="go to profile"
onClick={() => window.open(profilePath, '_blank')}
sx={{ maxWidth: 'fit-content', gap: '8px' }}
>
<ProfileNoCircleIcon sx={{ fontSize: '18px' }} />
<Typography variant="subtitle2" color="text.primary">
Go to profile
</Typography>
</IconButton>

<IconButton
size="small"
aria-label="edit group chat"
sx={{ maxWidth: 'fit-content', gap: '8px' }}
>
<NewGroupIcon sx={{ fontSize: '18px', color: 'text.primary' }} />
<Typography variant="subtitle2" color="text.primary">
Add contact to a group
</Typography>
</IconButton>
</ButtonContainer>
<Subheader>
<Typography variant="subtitle2" color="text.primary">
About
</Typography>
</Subheader>
<Divider />
<Subheader>
<Typography variant="caption" color="text.secondary">
{biography?.split('\n').map((line, index) => (
<span key={index}>
{line}
{index < biography.split('\n').length - 1 && <br />}
</span>
))}
</Typography>
</Subheader>
</SubheaderContainer>
</Box>
)
}

export default Body
Loading
Loading