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