mirror of
				https://github.com/cinnyapp/cinny.git
				synced 2025-11-04 14:30:29 +03:00 
			
		
		
		
	add presence component
This commit is contained in:
		
							parent
							
								
									2e69106283
								
							
						
					
					
						commit
						507f41cb8b
					
				
					 4 changed files with 161 additions and 0 deletions
				
			
		
							
								
								
									
										80
									
								
								src/app/components/presence/Presence.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/app/components/presence/Presence.tsx
									
										
									
									
									
										Normal 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>
 | 
			
		||||
  )
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										1
									
								
								src/app/components/presence/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/app/components/presence/index.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
export * from './Presence';
 | 
			
		||||
							
								
								
									
										22
									
								
								src/app/components/presence/styles.css.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/app/components/presence/styles.css.ts
									
										
									
									
									
										Normal 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',
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										58
									
								
								src/app/hooks/useUserPresence.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/app/hooks/useUserPresence.ts
									
										
									
									
									
										Normal 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',
 | 
			
		||||
    }),
 | 
			
		||||
    []
 | 
			
		||||
  );
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue