-
Notifications
You must be signed in to change notification settings - Fork 13.6k
feat: presence sync engine UI #40469
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
base: feat/presence-sync-engine
Are you sure you want to change the base?
Changes from all commits
6ec8222
8b42279
db09757
f147684
2b52ad0
76e13c5
a1ad4eb
cd15e10
ac5c615
c6ebe7e
3b3d9f1
d48abcd
06416c2
a7d77c3
0241991
0c56e76
a3afa34
6a27461
5c84072
0fe8dc1
9c87407
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,43 @@ | ||||||
| import { Box } from '@rocket.chat/fuselage'; | ||||||
| import { useTooltipClose, useTooltipOpen } from '@rocket.chat/ui-contexts'; | ||||||
| import type { ReactElement } from 'react'; | ||||||
| import { useRef, useCallback } from 'react'; | ||||||
|
|
||||||
| import { useExpirationText } from './useExpirationText'; | ||||||
| import MarkdownText from '../MarkdownText'; | ||||||
|
|
||||||
| type UserStatusTextProps = { | ||||||
| statusText?: string; | ||||||
| statusExpiresAt?: Date | string; | ||||||
| showExpiration?: boolean; | ||||||
| }; | ||||||
|
|
||||||
| const UserStatusText = ({ statusText, statusExpiresAt, showExpiration: showExpirationProp }: UserStatusTextProps): ReactElement | null => { | ||||||
| const expirationText = useExpirationText(statusExpiresAt); | ||||||
| const hasValidExpiration = expirationText != null; | ||||||
| const showExpiration = showExpirationProp ?? hasValidExpiration; | ||||||
|
|
||||||
| const ref = useRef<HTMLDivElement>(null); | ||||||
| const openTooltip = useTooltipOpen(); | ||||||
| const closeTooltip = useTooltipClose(); | ||||||
|
|
||||||
| const handleMouseEnter = useCallback(() => { | ||||||
| if (!ref.current || !hasValidExpiration) { | ||||||
| return; | ||||||
| } | ||||||
| openTooltip(<Box fontScale='p2'>{expirationText}</Box>, ref.current); | ||||||
| }, [hasValidExpiration, expirationText, openTooltip]); | ||||||
|
|
||||||
| if (!statusText) { | ||||||
| return null; | ||||||
| } | ||||||
|
|
||||||
| return ( | ||||||
| <Box ref={ref} data-tooltip='' onMouseEnter={handleMouseEnter} onMouseLeave={closeTooltip}> | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| <MarkdownText content={statusText} parseEmoji={true} variant='inlineWithoutBreaks' withTruncatedText /> | ||||||
| {showExpiration && hasValidExpiration && <Box color='secondary-info'>{expirationText}</Box>} | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
This way we don't need to check for |
||||||
| </Box> | ||||||
| ); | ||||||
| }; | ||||||
|
|
||||||
| export default UserStatusText; | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| export { default as UserStatusText } from './UserStatusText'; | ||
| export { useStatusTooltip } from './useStatusTooltip'; | ||
|
|
||
| export { useExpirationText, parseExpiresAt } from './useExpirationText'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| import { useLanguage } from '@rocket.chat/ui-contexts'; | ||
| import { useMemo } from 'react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
|
|
||
| import { useFormatDate } from '../../hooks/useFormatDate'; | ||
| import { useFormatTime } from '../../hooks/useFormatTime'; | ||
|
|
||
| // Handles Date, ISO string, and EJSON { $date } (from DDP streamer which does raw JSON.parse without EJSON deserialization) | ||
| export function parseExpiresAt(value?: unknown): Date | undefined { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why exporting this helper? Doesn't seems to be used anywhere else. |
||
| if (!value) { | ||
| return undefined; | ||
| } | ||
|
|
||
| if (value instanceof Date) { | ||
| return Number.isNaN(value.getTime()) ? undefined : value; | ||
| } | ||
|
|
||
| if (typeof value === 'object' && '$date' in (value as Record<string, unknown>)) { | ||
| const date = new Date((value as { $date: number }).$date); | ||
| return Number.isNaN(date.getTime()) ? undefined : date; | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| if (typeof value === 'string') { | ||
| const date = new Date(value); | ||
| return Number.isNaN(date.getTime()) ? undefined : date; | ||
| } | ||
|
|
||
| return undefined; | ||
| } | ||
|
|
||
| function isSameDay(a: Date, b: Date): boolean { | ||
| return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); | ||
| } | ||
|
|
||
| export function useExpirationText(statusExpiresAt?: Date | string): string | undefined { | ||
| const { t } = useTranslation(); | ||
| const language = useLanguage(); | ||
| const formatTime = useFormatTime(); | ||
| const formatDate = useFormatDate(); | ||
|
|
||
| return useMemo(() => { | ||
| const expiresAt = parseExpiresAt(statusExpiresAt); | ||
| if (!expiresAt || expiresAt.getTime() <= Date.now()) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const now = new Date(); | ||
| if (isSameDay(expiresAt, now)) { | ||
| return `${t('Until')} ${formatTime(expiresAt)}`; | ||
| } | ||
|
|
||
| if (expiresAt.getFullYear() === now.getFullYear()) { | ||
| return `${t('Until')} ${new Intl.DateTimeFormat(language, { month: 'long', day: 'numeric' }).format(expiresAt)}`; | ||
| } | ||
|
|
||
| return `${t('Until')} ${formatDate(expiresAt)}`; | ||
| }, [statusExpiresAt, t, language, formatTime, formatDate]); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import { Box, Icon } from '@rocket.chat/fuselage'; | ||
| import { useTooltipOpen, useTooltipClose } from '@rocket.chat/ui-contexts'; | ||
| import type { MouseEvent } from 'react'; | ||
| import { useCallback } from 'react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
|
|
||
| import { useExpirationText } from './useExpirationText'; | ||
|
|
||
| const STATUS_LABEL_KEYS: Record<string, string> = { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not use the already existing |
||
| online: 'Online', | ||
| away: 'Away', | ||
| busy: 'Busy', | ||
| offline: 'Offline', | ||
| }; | ||
|
|
||
| export function useStatusTooltip(statusText?: string, statusExpiresAt?: Date | string, status?: string) { | ||
| const { t } = useTranslation(); | ||
| const expirationText = useExpirationText(statusExpiresAt); | ||
| const openTooltip = useTooltipOpen(); | ||
| const closeTooltip = useTooltipClose(); | ||
|
|
||
| const handleMouseEnter = useCallback( | ||
| (e: MouseEvent<HTMLElement>) => { | ||
| if (!statusText) { | ||
| return; | ||
| } | ||
| const statusLabelKey = status ? STATUS_LABEL_KEYS[status] : undefined; | ||
| const headline = statusLabelKey ? `${t(statusLabelKey)} - ${statusText}` : statusText; | ||
| openTooltip( | ||
| <Box> | ||
| <Box fontScale='p2'>{headline}</Box> | ||
| {expirationText && ( | ||
| <Box color='secondary-info' display='flex' alignItems='center'> | ||
| <Icon name='clock' size='x16' mie={4} /> | ||
| {expirationText} | ||
| </Box> | ||
| )} | ||
| </Box>, | ||
| e.currentTarget, | ||
| ); | ||
| }, | ||
| [statusText, status, expirationText, openTooltip, t], | ||
| ); | ||
|
|
||
| return { | ||
| hasStatusText: !!statusText, | ||
| handleMouseEnter, | ||
| handleMouseLeave: closeTooltip, | ||
| }; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.