From 507f41cb8b3748f87bb9f860b26be93d3a348f43 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 26 Jul 2025 11:55:53 +0530 Subject: [PATCH] add presence component --- src/app/components/presence/Presence.tsx | 80 +++++++++++++++++++++++ src/app/components/presence/index.ts | 1 + src/app/components/presence/styles.css.ts | 22 +++++++ src/app/hooks/useUserPresence.ts | 58 ++++++++++++++++ 4 files changed, 161 insertions(+) create mode 100644 src/app/components/presence/Presence.tsx create mode 100644 src/app/components/presence/index.ts create mode 100644 src/app/components/presence/styles.css.ts create mode 100644 src/app/hooks/useUserPresence.ts diff --git a/src/app/components/presence/Presence.tsx b/src/app/components/presence/Presence.tsx new file mode 100644 index 00000000..108852f2 --- /dev/null +++ b/src/app/components/presence/Presence.tsx @@ -0,0 +1,80 @@ +import { + as, + Badge, + Box, + color, + ContainerColor, + MainColor, + Text, + Tooltip, + TooltipProvider, + toRem, +} from 'folds'; +import React, { ReactNode, useId } from 'react'; +import * as css from './styles.css'; +import { Presence, usePresenceLabel } from '../../hooks/useUserPresence'; + +const PresenceToColor: Record = { + [Presence.Online]: 'Success', + [Presence.Unavailable]: 'Warning', + [Presence.Offline]: 'Secondary', +}; + +type PresenceBadgeProps = { + presence: Presence; + status?: string; + size?: '200' | '300' | '400' | '500'; +}; +export function PresenceBadge({ presence, status, size }: PresenceBadgeProps) { + const label = usePresenceLabel(); + const badgeLabelId = useId(); + + return ( + + + {label[presence]} + {status && } + {status && {status}} + + + } + > + {(triggerRef) => ( + + )} + + ); +} + +type AvatarPresenceProps = { + badge: ReactNode; + variant?: ContainerColor; +}; +export const AvatarPresence = as<'div', AvatarPresenceProps>( + ({ as: AsAvatarPresence, badge, variant = 'Surface', children, ...props }, ref) => ( + + {badge && ( +
+ {badge} +
+ )} + {children} +
+ ) +); diff --git a/src/app/components/presence/index.ts b/src/app/components/presence/index.ts new file mode 100644 index 00000000..88fcdf78 --- /dev/null +++ b/src/app/components/presence/index.ts @@ -0,0 +1 @@ +export * from './Presence'; diff --git a/src/app/components/presence/styles.css.ts b/src/app/components/presence/styles.css.ts new file mode 100644 index 00000000..12ea7f18 --- /dev/null +++ b/src/app/components/presence/styles.css.ts @@ -0,0 +1,22 @@ +import { style } from '@vanilla-extract/css'; +import { config } from 'folds'; + +export const AvatarPresence = style({ + display: 'flex', + position: 'relative', + flexShrink: 0, +}); + +export const AvatarPresenceBadge = style({ + position: 'absolute', + bottom: 0, + right: 0, + transform: 'translate(25%, 25%)', + zIndex: 1, + + display: 'flex', + padding: config.borderWidth.B600, + backgroundColor: 'inherit', + borderRadius: config.radii.Pill, + overflow: 'hidden', +}); diff --git a/src/app/hooks/useUserPresence.ts b/src/app/hooks/useUserPresence.ts new file mode 100644 index 00000000..5137950e --- /dev/null +++ b/src/app/hooks/useUserPresence.ts @@ -0,0 +1,58 @@ +import { useEffect, useMemo, useState } from 'react'; +import { User, UserEvent, UserEventHandlerMap } from 'matrix-js-sdk'; +import { useMatrixClient } from './useMatrixClient'; + +export enum Presence { + Online = 'online', + Unavailable = 'unavailable', + Offline = 'offline', +} + +export type UserPresence = { + presence: Presence; + status?: string; + active: boolean; + lastActiveTs?: number; +}; + +const getUserPresence = (user: User): UserPresence => ({ + presence: user.presence as Presence, + status: user.presenceStatusMsg, + active: user.currentlyActive, + lastActiveTs: user.getLastActiveTs(), +}); + +export const useUserPresence = (userId: string): UserPresence | undefined => { + const mx = useMatrixClient(); + const user = mx.getUser(userId); + + const [presence, setPresence] = useState(() => (user ? getUserPresence(user) : undefined)); + + useEffect(() => { + const updatePresence: UserEventHandlerMap[UserEvent.Presence] = (event, u) => { + if (u.userId === user?.userId) { + setPresence(getUserPresence(user)); + } + }; + user?.on(UserEvent.Presence, updatePresence); + user?.on(UserEvent.CurrentlyActive, updatePresence); + user?.on(UserEvent.LastPresenceTs, updatePresence); + return () => { + user?.removeListener(UserEvent.Presence, updatePresence); + user?.removeListener(UserEvent.CurrentlyActive, updatePresence); + user?.removeListener(UserEvent.LastPresenceTs, updatePresence); + }; + }, [user]); + + return presence; +}; + +export const usePresenceLabel = (): Record => + useMemo( + () => ({ + [Presence.Online]: 'Active', + [Presence.Unavailable]: 'Busy', + [Presence.Offline]: 'Away', + }), + [] + );