add presence component

This commit is contained in:
Ajay Bura 2025-07-26 11:55:53 +05:30
parent 2e69106283
commit 507f41cb8b
4 changed files with 161 additions and 0 deletions

View file

@ -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, MainColor> = {
[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 (
<TooltipProvider
position="Right"
align="Center"
offset={4}
delay={200}
tooltip={
<Tooltip id={badgeLabelId}>
<Box style={{ maxWidth: toRem(250) }} alignItems="Baseline" gap="100">
<Text size="L400">{label[presence]}</Text>
{status && <Text size="T200"></Text>}
{status && <Text size="T200">{status}</Text>}
</Box>
</Tooltip>
}
>
{(triggerRef) => (
<Badge
aria-labelledby={badgeLabelId}
ref={triggerRef}
size={size}
variant={PresenceToColor[presence]}
fill={presence === Presence.Offline ? 'Soft' : 'Solid'}
radii="Pill"
/>
)}
</TooltipProvider>
);
}
type AvatarPresenceProps = {
badge: ReactNode;
variant?: ContainerColor;
};
export const AvatarPresence = as<'div', AvatarPresenceProps>(
({ as: AsAvatarPresence, badge, variant = 'Surface', children, ...props }, ref) => (
<Box as={AsAvatarPresence} className={css.AvatarPresence} {...props} ref={ref}>
{badge && (
<div
className={css.AvatarPresenceBadge}
style={{ backgroundColor: color[variant].Container }}
>
{badge}
</div>
)}
{children}
</Box>
)
);

View file

@ -0,0 +1 @@
export * from './Presence';

View file

@ -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',
});

View file

@ -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<Presence, string> =>
useMemo(
() => ({
[Presence.Online]: 'Active',
[Presence.Unavailable]: 'Busy',
[Presence.Offline]: 'Away',
}),
[]
);