Skip to content

Commit

Permalink
Fix last read activity marker in thread
Browse files Browse the repository at this point in the history
  • Loading branch information
Godefroy committed Jun 28, 2024
1 parent 55e1e73 commit 023e5d4
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 123 deletions.
229 changes: 113 additions & 116 deletions packages/webapp/src/features/thread/components/ThreadActivities.tsx
Original file line number Diff line number Diff line change
@@ -1,146 +1,143 @@
import { MeetingContext } from '@/meeting/contexts/MeetingContext'
import useCurrentMember from '@/member/hooks/useCurrentMember'
import {
Alert,
AlertDescription,
AlertTitle,
Box,
StackProps,
StyleProps,
VStack,
forwardRef,
} from '@chakra-ui/react'
import { ThreadMemberStatusFragment, Thread_Activity_Type_Enum } from '@gql'
import { Thread_Activity_Type_Enum } from '@gql'
import { ThreadActivityMeetingNoteFragment } from '@rolebase/shared/model/thread_activity'
import { isSameDay } from 'date-fns'
import React, {
forwardRef,
useContext,
useEffect,
useMemo,
useState,
} from 'react'
import React, { useContext, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ThreadIcon } from 'src/icons'
import { ThreadContext } from '../contexts/ThreadContext'
import ThreadActivity from './ThreadActivity'
import ThreadDaySeparator from './ThreadDaySeparator'

interface Props extends StackProps {
memberStatus?: ThreadMemberStatusFragment
}

export const activityMeetingNoteTmpId = 'tmp'

const ThreadActivities = forwardRef<HTMLDivElement, Props>(
({ memberStatus, ...stackProps }, ref) => {
const { t } = useTranslation()
const { thread, activities } = useContext(ThreadContext)!
const meetingState = useContext(MeetingContext)
const currentMember = useCurrentMember()
export default forwardRef(function ThreadActivities(
styleProps: StyleProps,
ref
) {
const { t } = useTranslation()
const { thread, activities, memberStatus } = useContext(ThreadContext)!
const meetingState = useContext(MeetingContext)

// Temporary meeting note
const tmpMeetingNoteActivity = useMemo(() => {
if (
!thread ||
!activities ||
!meetingState?.meeting ||
activities.some(
(a) =>
a.type === Thread_Activity_Type_Enum.MeetingNote &&
a.refMeeting?.id === meetingState.meeting?.id
)
) {
return undefined
}
// Temporary meeting note
const tmpMeetingNoteActivity = useMemo(() => {
if (
!thread ||
!activities ||
!meetingState?.meeting ||
activities.some(
(a) =>
a.type === Thread_Activity_Type_Enum.MeetingNote &&
a.refMeeting?.id === meetingState.meeting?.id
)
) {
return undefined
}

// Add temporary meeting note to activities
return {
id: activityMeetingNoteTmpId,
threadId: thread.id,
userId: '',
createdAt: new Date().toISOString(),
type: Thread_Activity_Type_Enum.MeetingNote,
refMeeting: meetingState.meeting,
reactions: [],
data: {
notes: '',
},
} as ThreadActivityMeetingNoteFragment
}, [thread, activities, meetingState?.meeting?.id])
// Add temporary meeting note to activities
return {
id: activityMeetingNoteTmpId,
threadId: thread.id,
userId: '',
createdAt: new Date().toISOString(),
type: Thread_Activity_Type_Enum.MeetingNote,
refMeeting: meetingState.meeting,
reactions: [],
data: {
notes: '',
},
} as ThreadActivityMeetingNoteFragment
}, [thread?.id, activities, meetingState?.meeting?.id])

// Previous status to show a mark
const [lastReadActivityId, setLastReadActivityId] = useState<
string | undefined
>()
// Show read mark on last read activity:
// - at first load
// - when it changes for an activity that is not the last one
const [lastReadActivityId, setLastReadActivityId] = useState<
string | undefined
>()

// Update read mark
useEffect(() => {
if (!thread || !activities || !currentMember) {
return
}
const lastActivityId = activities[activities.length - 1]?.id || null
const lastReadActivityId = memberStatus?.lastReadActivityId
// Update read mark at first load
useEffect(() => {
if (!thread || !activities || thread.id !== activities[0].threadId) {
return
}
const lastActivityId = activities[activities.length - 1]?.id
const readActivityId = memberStatus?.lastReadActivityId

if (lastReadActivityId && lastReadActivityId !== lastActivityId) {
setLastReadActivityId(lastReadActivityId)
}
}, [
memberStatus,
// Member status may be provided after other deps,
// so we need to check the existence of their values
!thread,
!activities,
!currentMember,
])
if (readActivityId && readActivityId !== lastActivityId) {
// Set mark to last read activity when opening thread
setLastReadActivityId(readActivityId)
} else {
// Reset
setLastReadActivityId(undefined)
}
}, [thread?.id, activities?.[0].threadId])

return (
<VStack spacing={0} mb={2} align="stretch" ref={ref} {...stackProps}>
{activities &&
activities.map((activity, i) => (
<React.Fragment key={`activity_${activity.id}`}>
{(i === 0 ||
!isSameDay(
new Date(activity.createdAt),
new Date(activities[i - 1].createdAt)
)) && <ThreadDaySeparator date={activity.createdAt} />}
// Update read mark when last read activity changes
useEffect(() => {
if (!memberStatus || !activities) return
const readActivityId = memberStatus.lastReadActivityId
if (!readActivityId) return
const activityExists = activities.some((a) => a.id === readActivityId)
const lastActivityId = activities[activities.length - 1]?.id
if (activityExists && readActivityId !== lastActivityId) {
setLastReadActivityId(readActivityId)
}
}, [memberStatus?.lastReadActivityId])

<ThreadActivity activity={activity} />
return (
<VStack spacing={0} mb={2} align="stretch" ref={ref} {...styleProps}>
{activities &&
activities.map((activity, i) => (
<React.Fragment key={`activity_${activity.id}`}>
{(i === 0 ||
!isSameDay(
new Date(activity.createdAt),
new Date(activities[i - 1].createdAt)
)) && <ThreadDaySeparator date={activity.createdAt} />}

{lastReadActivityId === activity.id && (
<Box h="3px" w="100%" bg="red.200" _dark={{ bg: 'red.800' }} />
)}
</React.Fragment>
))}
<ThreadActivity activity={activity} />

{activities?.length === 0 &&
!tmpMeetingNoteActivity &&
!thread?.archived && (
<Alert
status="success"
variant="subtle"
flexDirection="column"
alignItems="center"
justifyContent="center"
textAlign="center"
height="200px"
>
<ThreadIcon size={40} />
<AlertTitle mt={4} mb={1} fontSize="lg">
{t('ThreadActivities.emptyTitle')}
</AlertTitle>
<AlertDescription maxWidth="sm">
{t('ThreadActivities.emptyDescription')}
</AlertDescription>
</Alert>
)}
{lastReadActivityId === activity.id && (
<Box h="3px" w="100%" bg="red.200" _dark={{ bg: 'red.800' }} />
)}
</React.Fragment>
))}

{tmpMeetingNoteActivity && (
<ThreadActivity activity={tmpMeetingNoteActivity} />
{activities?.length === 0 &&
!tmpMeetingNoteActivity &&
!thread?.archived && (
<Alert
status="success"
variant="subtle"
flexDirection="column"
alignItems="center"
justifyContent="center"
textAlign="center"
height="200px"
>
<ThreadIcon size={40} />
<AlertTitle mt={4} mb={1} fontSize="lg">
{t('ThreadActivities.emptyTitle')}
</AlertTitle>
<AlertDescription maxWidth="sm">
{t('ThreadActivities.emptyDescription')}
</AlertDescription>
</Alert>
)}
</VStack>
)
}
)

ThreadActivities.displayName = 'ThreadActivities'

export default ThreadActivities
{tmpMeetingNoteActivity && (
<ThreadActivity activity={tmpMeetingNoteActivity} />
)}
</VStack>
)
})
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ export default function ThreadContent({

const {
thread,
memberStatus,
path,
loading,
error,
Expand Down Expand Up @@ -170,7 +169,7 @@ export default function ThreadContent({
}
>
{loading && <Loading active center />}
<ThreadActivities memberStatus={memberStatus} />
<ThreadActivities />
</ScrollableLayout>

{editModal.isOpen && (
Expand Down
8 changes: 3 additions & 5 deletions packages/webapp/src/features/thread/hooks/useThreadState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export default function useThreadState(threadId: string): ThreadState {
// Scroll to next unread activity when activities are loaded
useEffect(() => {
const activityId = memberStatus?.lastReadActivityId
if (!activityId || !activities || !thread) return
if (!activityId || !activities) return
const activityIndex = activities.findIndex((a) => a.id === activityId)
if (activityIndex === -1) return
const nextActivityId = activities[activityIndex + 1]?.id
Expand All @@ -136,9 +136,7 @@ export default function useThreadState(threadId: string): ThreadState {

// Update member status when there is a new activity
useEffect(() => {
if (!activities || !thread || !currentMember) {
return
}
if (!activities || !thread || !currentMember) return

// Already up to date?
const lastActivityId = activities[activities.length - 1]?.id || null
Expand All @@ -157,7 +155,7 @@ export default function useThreadState(threadId: string): ThreadState {
},
},
})
}, [activities])
}, [!thread, activities])

const threadLogsActivity = useMemo(() => {
if (!thread || !threadLogs) {
Expand Down

0 comments on commit 023e5d4

Please sign in to comment.