Add option to view user avatar (#2462)
authorAjay Bura <32841439+ajbura@users.noreply.github.com>
Sun, 24 Aug 2025 12:36:45 +0000 (18:06 +0530)
committerGitHub <noreply@github.com>
Sun, 24 Aug 2025 12:36:45 +0000 (22:36 +1000)
src/app/components/user-avatar/UserAvatar.tsx
src/app/components/user-profile/UserHero.tsx
src/app/components/user-profile/styles.css.ts

index 98067ad431fd046015061700c9cf0209150f3102..d9de9b79fcfe1c2004f252a2cd3d312e7480afe7 100644 (file)
@@ -1,15 +1,17 @@
 import { AvatarFallback, AvatarImage, color } from 'folds';
 import React, { ReactEventHandler, ReactNode, useState } from 'react';
+import classNames from 'classnames';
 import * as css from './UserAvatar.css';
 import colorMXID from '../../../util/colorMXID';
 
 type UserAvatarProps = {
+  className?: string;
   userId: string;
   src?: string;
   alt?: string;
   renderFallback: () => ReactNode;
 };
-export function UserAvatar({ userId, src, alt, renderFallback }: UserAvatarProps) {
+export function UserAvatar({ className, userId, src, alt, renderFallback }: UserAvatarProps) {
   const [error, setError] = useState(false);
 
   const handleLoad: ReactEventHandler<HTMLImageElement> = (evt) => {
@@ -20,7 +22,7 @@ export function UserAvatar({ userId, src, alt, renderFallback }: UserAvatarProps
     return (
       <AvatarFallback
         style={{ backgroundColor: colorMXID(userId), color: color.Surface.Container }}
-        className={css.UserAvatar}
+        className={classNames(css.UserAvatar, className)}
       >
         {renderFallback()}
       </AvatarFallback>
@@ -29,7 +31,7 @@ export function UserAvatar({ userId, src, alt, renderFallback }: UserAvatarProps
 
   return (
     <AvatarImage
-      className={css.UserAvatar}
+      className={classNames(css.UserAvatar, className)}
       src={src}
       alt={alt}
       onError={() => setError(true)}
index cf4c815f1216c7297e914031c1316552769bfd4b..0e7fb74850f3739f671a36f9b2b6bf33b9150f60 100644 (file)
@@ -1,6 +1,17 @@
-import React from 'react';
-import { Avatar, Box, Icon, Icons, Text } from 'folds';
+import React, { useState } from 'react';
+import {
+  Avatar,
+  Box,
+  Icon,
+  Icons,
+  Modal,
+  Overlay,
+  OverlayBackdrop,
+  OverlayCenter,
+  Text,
+} from 'folds';
 import classNames from 'classnames';
+import FocusTrap from 'focus-trap-react';
 import * as css from './styles.css';
 import { UserAvatar } from '../user-avatar';
 import colorMXID from '../../../util/colorMXID';
@@ -8,6 +19,8 @@ import { getMxIdLocalPart } from '../../utils/matrix';
 import { BreakWord, LineClamp3 } from '../../styles/Text.css';
 import { UserPresence } from '../../hooks/useUserPresence';
 import { AvatarPresence, PresenceBadge } from '../presence';
+import { ImageViewer } from '../image-viewer';
+import { stopPropagation } from '../../utils/keyboard';
 
 type UserHeroProps = {
   userId: string;
@@ -15,6 +28,8 @@ type UserHeroProps = {
   presence?: UserPresence;
 };
 export function UserHero({ userId, avatarUrl, presence }: UserHeroProps) {
+  const [viewAvatar, setViewAvatar] = useState<string>();
+
   return (
     <Box direction="Column" className={css.UserHero}>
       <div
@@ -24,7 +39,9 @@ export function UserHero({ userId, avatarUrl, presence }: UserHeroProps) {
           filter: avatarUrl ? undefined : 'brightness(50%)',
         }}
       >
-        {avatarUrl && <img className={css.UserHeroCover} src={avatarUrl} alt={userId} />}
+        {avatarUrl && (
+          <img className={css.UserHeroCover} src={avatarUrl} alt={userId} draggable="false" />
+        )}
       </div>
       <div className={css.UserHeroAvatarContainer}>
         <AvatarPresence
@@ -33,8 +50,14 @@ export function UserHero({ userId, avatarUrl, presence }: UserHeroProps) {
             presence && <PresenceBadge presence={presence.presence} status={presence.status} />
           }
         >
-          <Avatar className={css.UserHeroAvatar} size="500">
+          <Avatar
+            as={avatarUrl ? 'button' : 'div'}
+            onClick={avatarUrl ? () => setViewAvatar(avatarUrl) : undefined}
+            className={css.UserHeroAvatar}
+            size="500"
+          >
             <UserAvatar
+              className={css.UserHeroAvatarImg}
               userId={userId}
               src={avatarUrl}
               alt={userId}
@@ -42,6 +65,28 @@ export function UserHero({ userId, avatarUrl, presence }: UserHeroProps) {
             />
           </Avatar>
         </AvatarPresence>
+        {viewAvatar && (
+          <Overlay open backdrop={<OverlayBackdrop />}>
+            <OverlayCenter>
+              <FocusTrap
+                focusTrapOptions={{
+                  initialFocus: false,
+                  onDeactivate: () => setViewAvatar(undefined),
+                  clickOutsideDeactivates: true,
+                  escapeDeactivates: stopPropagation,
+                }}
+              >
+                <Modal size="500" onContextMenu={(evt: any) => evt.stopPropagation()}>
+                  <ImageViewer
+                    src={viewAvatar}
+                    alt={userId}
+                    requestClose={() => setViewAvatar(undefined)}
+                  />
+                </Modal>
+              </FocusTrap>
+            </OverlayCenter>
+          </Overlay>
+        )}
       </div>
     </Box>
   );
index ad6d5a95b83aee7de4a9fc9fd34238639642c341..62272a2e86249386b2d1a5c966f5891d919f3549 100644 (file)
@@ -39,4 +39,16 @@ export const UserAvatarContainer = style({
 });
 export const UserHeroAvatar = style({
   outline: `${config.borderWidth.B600} solid ${color.Surface.Container}`,
+  selectors: {
+    'button&': {
+      cursor: 'pointer',
+    },
+  },
+});
+export const UserHeroAvatarImg = style({
+  selectors: {
+    [`button${UserHeroAvatar}:hover &`]: {
+      filter: 'brightness(0.5)',
+    },
+  },
 });