From c0f9baf6a61833e77cb763ff292532bb6c0431cb Mon Sep 17 00:00:00 2001 From: Shea Date: Mon, 22 Jun 2026 21:45:16 +0300 Subject: [PATCH] add easy copy Signed-off-by: Shea --- .changeset/feat_add_easy_copy_user.md | 5 +++ src/app/components/user-profile/UserHero.tsx | 41 +++++++++++++++++-- .../user-profile/UserRoomProfile.tsx | 1 + 3 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 .changeset/feat_add_easy_copy_user.md diff --git a/.changeset/feat_add_easy_copy_user.md b/.changeset/feat_add_easy_copy_user.md new file mode 100644 index 000000000..7b325010b --- /dev/null +++ b/.changeset/feat_add_easy_copy_user.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Change profile handle part to a button that copies it diff --git a/src/app/components/user-profile/UserHero.tsx b/src/app/components/user-profile/UserHero.tsx index d98284e46..eaf56ac9d 100644 --- a/src/app/components/user-profile/UserHero.tsx +++ b/src/app/components/user-profile/UserHero.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useMemo, useRef, useState } from 'react'; import type { CSSProperties } from 'react'; import { Avatar, @@ -12,6 +12,7 @@ import { Text, Tooltip, toRem, + Chip, } from 'folds'; import classNames from 'classnames'; import FocusTrap from 'focus-trap-react'; @@ -27,11 +28,20 @@ import { useBlobCache } from '$hooks/useBlobCache'; import { ImageViewer } from '$components/image-viewer'; import { AvatarPresence, PresenceBadge } from '$components/presence'; import { UserAvatar } from '$components/user-avatar'; -import { CaretDown, CaretUp, profileIcon, userFallbackIcon } from '$components/icons/phosphor'; +import { + CaretDown, + CaretUp, + Check, + profileIcon, + userFallbackIcon, +} from '$components/icons/phosphor'; import { ClientSideHoverFreeze } from '$components/ClientSideHoverFreeze'; import { useUserProfile } from '$hooks/useUserProfile'; import { shadeColor, areColorsTooSimilar } from '$utils/shadeColor'; import * as css from './styles.css'; +import { copyToClipboard } from '$utils/dom'; +import { useTimeoutToggle } from '$hooks/useTimeoutToggle'; +import { CopyIcon, CrossIcon } from '@phosphor-icons/react'; type UserHeroProps = { userId: string; @@ -228,11 +238,15 @@ export function UserHero({ userId, avatarUrl, bannerUrl, presence, autoplayGifs type UserHeroNameProps = { displayName?: string; userId: string; + server?: string; customHeroCards?: boolean; }; -export function UserHeroName({ displayName, userId, customHeroCards }: UserHeroNameProps) { +export function UserHeroName({ displayName, userId, server, customHeroCards }: UserHeroNameProps) { const username = getMxIdLocalPart(userId); const nick = useNickname(userId); + const [copied, setCopied] = useTimeoutToggle(); + const [isHovered, setIsHovered] = useState(false); + const isSuccess = useRef(false); // Sable username color and fonts const { color, font } = useSableCosmetics(userId, useRoom(), customHeroCards); @@ -257,7 +271,26 @@ export function UserHeroName({ displayName, userId, customHeroCards }: UserHeroN - @{username} + { + if (username && server) { + copyToClipboard(`@${username}:${server}`); + isSuccess.current = true; + } else isSuccess.current = false; + setCopied(); + }} + style={{ backgroundColor: '#0000', padding: '0' }} + onPointerEnter={() => setIsHovered(true)} + onPointerLeave={() => setIsHovered(false)} + before={`@${username}`} + after={ + copied || isHovered ? ( + profileIcon(copied ? (isSuccess ? Check : CrossIcon) : CopyIcon) + ) : ( + <> + ) + } + /> diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index 518c3448a..951e4f167 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -581,6 +581,7 @@ export function UserRoomProfile({ userId, initialProfile }: Readonly {userId !== myUserId && (