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 && (