From 0d65a134fdcd8fddf15c70a9cc2f72ba720d6b03 Mon Sep 17 00:00:00 2001 From: byGalax Date: Tue, 12 May 2026 22:58:22 +0200 Subject: [PATCH] feat(settings): click own avatar in profile preview to view fullscreen Lightbox was previously a file-private component inside AttachmentImage (used for enlarging chat image attachments). Extracted to a standalone components/Lightbox.tsx so other surfaces can reuse the same dialog without duplicating Esc/backdrop/body-overflow plumbing. In SettingsPage's profile live-preview, the round avatar overlapping the banner is now wrapped in a transparent button that opens the Lightbox with the full-resolution avatar URL on click. Cursor switches to zoom-in. Disabled when the user only has the initial-letter placeholder (nothing meaningful to enlarge). Native button chrome (border, padding, button-face background) is reset to keep the avatar circle's appearance unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/components/AttachmentImage.tsx | 44 ++-------------- apps/desktop/src/components/Lightbox.tsx | 51 +++++++++++++++++++ apps/desktop/src/pages/SettingsPage.tsx | 39 ++++++++++++-- 3 files changed, 89 insertions(+), 45 deletions(-) create mode 100644 apps/desktop/src/components/Lightbox.tsx diff --git a/apps/desktop/src/components/AttachmentImage.tsx b/apps/desktop/src/components/AttachmentImage.tsx index 6f3400c..ded19e0 100644 --- a/apps/desktop/src/components/AttachmentImage.tsx +++ b/apps/desktop/src/components/AttachmentImage.tsx @@ -3,7 +3,8 @@ import { useEffect, useState } from 'react'; import { getCachedAttachment, putCachedAttachment } from '../lib/attachmentCache'; import { supabase } from '../lib/supabase'; -import { AlertIcon, SpinnerIcon, XIcon } from './icons'; +import { AlertIcon, SpinnerIcon } from './icons'; +import { Lightbox } from './Lightbox'; interface Props { handle: AttachmentHandle; @@ -136,42 +137,5 @@ export function AttachmentImage({ handle }: Props) { ); } -function Lightbox({ url, onClose }: { url: string; onClose: () => void }) { - useEffect(() => { - function onKey(e: KeyboardEvent) { - if (e.key === 'Escape') onClose(); - } - document.addEventListener('keydown', onKey); - const prevOverflow = document.body.style.overflow; - document.body.style.overflow = 'hidden'; - return () => { - document.removeEventListener('keydown', onKey); - document.body.style.overflow = prevOverflow; - }; - }, [onClose]); - - return ( -
- - attachment full e.stopPropagation()} - className="max-h-[92vh] max-w-[92vw] rounded-xl object-contain shadow-2xl" - /> -
- ); -} +// Lightbox extracted to ./Lightbox.tsx so the settings avatar preview and +// any future surface can reuse the same dialog without duplication. diff --git a/apps/desktop/src/components/Lightbox.tsx b/apps/desktop/src/components/Lightbox.tsx new file mode 100644 index 0000000..9c7a07f --- /dev/null +++ b/apps/desktop/src/components/Lightbox.tsx @@ -0,0 +1,51 @@ +import { useEffect } from 'react'; + +import { XIcon } from './icons'; + +// Fullscreen image viewer. Backdrop click + Esc close. Originally lived +// inside AttachmentImage.tsx as a file-private component; extracted here +// so other surfaces (settings avatar preview, future profile popover, +// etc.) can reuse the exact same dialog without duplicating the chrome. +// +// The image itself stops click propagation so a click on the picture +// keeps the lightbox open — only the backdrop or the explicit close +// button dismisses. +export function Lightbox({ url, onClose }: { url: string; onClose: () => void }) { + useEffect(() => { + function onKey(e: KeyboardEvent) { + if (e.key === 'Escape') onClose(); + } + document.addEventListener('keydown', onKey); + const prevOverflow = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + return () => { + document.removeEventListener('keydown', onKey); + document.body.style.overflow = prevOverflow; + }; + }, [onClose]); + + return ( +
+ + e.stopPropagation()} + className="max-h-[92vh] max-w-[92vw] rounded-xl object-contain shadow-2xl" + /> +
+ ); +} diff --git a/apps/desktop/src/pages/SettingsPage.tsx b/apps/desktop/src/pages/SettingsPage.tsx index 22c173c..e951d52 100644 --- a/apps/desktop/src/pages/SettingsPage.tsx +++ b/apps/desktop/src/pages/SettingsPage.tsx @@ -33,6 +33,7 @@ import { uploadBannerBlob, } from '../lib/bannerUpload'; import { ImageCropDialog } from '../components/ImageCropDialog'; +import { Lightbox } from '../components/Lightbox'; import { devLocalSecretStore } from '../lib/secretStore'; import { getPttSettings, @@ -799,6 +800,9 @@ function ProfileVisualsControls({ patchProfile, busy }: AvatarControlsProps) { // ratios. const [cropFile, setCropFile] = useState(null); const [cropKind, setCropKind] = useState<'avatar' | 'banner' | null>(null); + // Lightbox toggle for the avatar live-preview. Clicking the in-page + // avatar opens a fullscreen view; clicking outside / Esc dismisses. + const [avatarPreviewOpen, setAvatarPreviewOpen] = useState(false); const userId = profile?.userId; const avatarUrl = profile?.avatarUrl ?? null; @@ -930,11 +934,33 @@ function ProfileVisualsControls({ patchProfile, busy }: AvatarControlsProps) { className="relative z-10 flex items-end gap-3 px-4 pb-3" style={{ marginTop: '-2rem' }} > - +
{displayName ?? '—'} @@ -1061,6 +1087,9 @@ function ProfileVisualsControls({ patchProfile, busy }: AvatarControlsProps) { }} onClose={closeCropDialog} /> + {avatarPreviewOpen && avatarUrl && ( + setAvatarPreviewOpen(false)} /> + )}
); }