diff --git a/src/app/components/presence/PresenceAvatar.tsx b/src/app/components/presence/PresenceAvatar.tsx new file mode 100644 index 0000000..fca5045 --- /dev/null +++ b/src/app/components/presence/PresenceAvatar.tsx @@ -0,0 +1,35 @@ +import React, { ReactNode } from 'react'; +import classNames from 'classnames'; +import * as css from './styles.css'; +import { Presence } from '../../hooks/useUserPresence'; + +type PresenceAvatarProps = { + /** The presence state to display */ + presence?: Presence; + /** The avatar element to wrap */ + children: ReactNode; + /** Additional className */ + className?: string; +}; + +/** + * Wraps an avatar with a left-side presence indicator border. + * Shows green for online, yellow/orange for unavailable/away, grey for offline. + */ +export function PresenceAvatar({ presence, children, className }: PresenceAvatarProps) { + if (!presence) { + return <>{children}; + } + + return ( +
+ {children} +
+ ); +} diff --git a/src/app/components/presence/index.ts b/src/app/components/presence/index.ts index 88fcdf7..8ac922c 100644 --- a/src/app/components/presence/index.ts +++ b/src/app/components/presence/index.ts @@ -1 +1,2 @@ export * from './Presence'; +export * from './PresenceAvatar'; diff --git a/src/app/components/presence/styles.css.ts b/src/app/components/presence/styles.css.ts index 12ea7f1..fd28178 100644 --- a/src/app/components/presence/styles.css.ts +++ b/src/app/components/presence/styles.css.ts @@ -1,5 +1,5 @@ import { style } from '@vanilla-extract/css'; -import { config } from 'folds'; +import { config, color } from 'folds'; export const AvatarPresence = style({ display: 'flex', @@ -20,3 +20,41 @@ export const AvatarPresenceBadge = style({ borderRadius: config.radii.Pill, overflow: 'hidden', }); + +export const PresenceAvatarContainer = style({ + display: 'flex', + position: 'relative', + flexShrink: 0, + alignSelf: 'flex-start', + '::before': { + content: '""', + position: 'absolute', + left: 0, + top: '10%', + height: '80%', + width: '3px', + borderTopRightRadius: '2px', + borderBottomRightRadius: '2px', + zIndex: 1, + }, +}); + +export const PresenceIndicator = style({}); + +export const PresenceOnline = style({ + '::before': { + backgroundColor: color.Success.Main, + }, +}); + +export const PresenceUnavailable = style({ + '::before': { + backgroundColor: color.Warning.Main, + }, +}); + +export const PresenceOffline = style({ + '::before': { + backgroundColor: color.Secondary.Main, + }, +}); diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index 9324e1c..443b6a3 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -79,6 +79,7 @@ import { MemberPowerTag, StateEvent } from '../../../../types/matrix/room'; import { PowerIcon } from '../../../components/power'; import colorMXID from '../../../../util/colorMXID'; import { getPowerTagIconSrc } from '../../../hooks/useMemberPowerTag'; +import { Presence, useUserPresence } from '../../../hooks/useUserPresence'; export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void; @@ -723,6 +724,7 @@ export const Message = as<'div', MessageProps>( const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const senderId = mEvent.getSender() ?? ''; + const senderPresence = useUserPresence(senderId); const [hover, setHover] = useState(false); const { hoverProps } = useHover({ onHoverChange: setHover }); @@ -790,12 +792,20 @@ export const Message = as<'div', MessageProps>( ); + const presenceClass = senderPresence?.presence === Presence.Online + ? css.MessageAvatarOnline + : senderPresence?.presence === Presence.Unavailable + ? css.MessageAvatarUnavailable + : senderPresence?.presence === Presence.Offline + ? css.MessageAvatarOffline + : undefined; + const avatarJSX = !collapse && messageLayout !== MessageLayout.Compact && (