diff --git a/src/app/components/user-profile/UserHero.tsx b/src/app/components/user-profile/UserHero.tsx index 0e7fb7485..90faf0545 100644 --- a/src/app/components/user-profile/UserHero.tsx +++ b/src/app/components/user-profile/UserHero.tsx @@ -95,8 +95,9 @@ export function UserHero({ userId, avatarUrl, presence }: UserHeroProps) { type UserHeroNameProps = { displayName?: string; userId: string; + status?: string; }; -export function UserHeroName({ displayName, userId }: UserHeroNameProps) { +export function UserHeroName({ displayName, userId, status }: UserHeroNameProps) { const username = getMxIdLocalPart(userId); return ( @@ -115,6 +116,13 @@ export function UserHeroName({ displayName, userId }: UserHeroNameProps) { @{username} + {status && ( + + + {status} + + + )} ); } diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index 361d0f978..2157e456e 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -237,7 +237,7 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) { - + {showEncryption && } {userId !== myUserId && ( diff --git a/src/app/features/room/MembersDrawer.tsx b/src/app/features/room/MembersDrawer.tsx index 29ae8f30e..0f7eb70f2 100644 --- a/src/app/features/room/MembersDrawer.tsx +++ b/src/app/features/room/MembersDrawer.tsx @@ -167,10 +167,15 @@ function MemberItem({ } > - + {name} + {presence?.status && ( + + {presence.status} + + )} ); diff --git a/src/app/features/settings/account/Profile.tsx b/src/app/features/settings/account/Profile.tsx index 9b9ca3da5..fcd7d846b 100644 --- a/src/app/features/settings/account/Profile.tsx +++ b/src/app/features/settings/account/Profile.tsx @@ -1,9 +1,11 @@ import React, { ChangeEventHandler, FormEventHandler, + Suspense, useCallback, useEffect, useMemo, + useRef, useState, } from 'react'; import { @@ -23,6 +25,8 @@ import { Header, config, Spinner, + PopOut, + RectCords, } from 'folds'; import FocusTrap from 'focus-trap-react'; import { SequenceCard } from '../../../components/sequence-card'; @@ -43,6 +47,11 @@ import { ModalWide } from '../../../styles/Modal.css'; import { createUploadAtom, UploadSuccess } from '../../../state/upload'; import { CompactUploadCardRenderer } from '../../../components/upload-card'; import { useCapabilities } from '../../../hooks/useCapabilities'; +import { useUserPresence } from '../../../hooks/useUserPresence'; + +const EmojiBoard = React.lazy(() => + import('../../../components/emoji-board').then((m) => ({ default: m.EmojiBoard })), +); type ProfileProps = { profile: UserProfile; @@ -310,6 +319,166 @@ function ProfileDisplayName({ profile, userId }: ProfileProps) { ); } +function ProfileStatus() { + const mx = useMatrixClient(); + const userId = mx.getUserId()!; + const presence = useUserPresence(userId); + + const [statusMsg, setStatusMsg] = useState(presence?.status ?? ''); + const [emojiAnchor, setEmojiAnchor] = useState(); + const inputRef = useRef(null); + + useEffect(() => { + setStatusMsg(presence?.status ?? ''); + }, [presence?.status]); + + const [saveState, saveStatus] = useAsyncCallback( + useCallback( + (msg: string) => + mx.setPresence({ + presence: 'online', + status_msg: msg, + }), + [mx], + ), + ); + const saving = saveState.status === AsyncStatus.Loading; + + const handleEmojiSelect = useCallback( + (unicode: string) => { + const input = inputRef.current; + if (input) { + const start = input.selectionStart ?? statusMsg.length; + const end = input.selectionEnd ?? statusMsg.length; + const next = statusMsg.slice(0, start) + unicode + statusMsg.slice(end); + setStatusMsg(next); + // restore cursor after emoji insertion + requestAnimationFrame(() => { + input.focus(); + input.setSelectionRange(start + unicode.length, start + unicode.length); + }); + } else { + setStatusMsg((prev) => prev + unicode); + } + setEmojiAnchor(undefined); + }, + [statusMsg], + ); + + const handleChange: ChangeEventHandler = (evt) => { + setStatusMsg(evt.currentTarget.value); + }; + + const handleSubmit: FormEventHandler = (evt) => { + evt.preventDefault(); + if (saving) return; + saveStatus(statusMsg.trim()); + }; + + const handleClear = () => { + setStatusMsg(''); + mx.setPresence({ + presence: 'online', + status_msg: '', + }); + }; + + const hasChanges = statusMsg !== (presence?.status ?? ''); + + return ( + + Status Message + + } + description={ + + Shown below your name in member lists. Supports emoji. + + } + > + + + + }> + setEmojiAnchor(undefined)} + /> + + } + > + ) => + setEmojiAnchor((prev) => + prev ? undefined : evt.currentTarget.getBoundingClientRect(), + ) + } + > + + + + } + /> + + + + {(presence?.status || statusMsg) && ( + + )} + + + ); +} + export function Profile() { const mx = useMatrixClient(); const userId = mx.getUserId()!; @@ -326,6 +495,7 @@ export function Profile() { > + );