mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-13 01:50:46 +00:00
Compare commits
3 Commits
hs/impleme
...
hs/persist
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35c19592af | ||
|
|
db2c5ce26c | ||
|
|
3fe5392588 |
@@ -19,6 +19,11 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
}
|
}
|
||||||
|
.mx_UserProfileSettings_profile_statusMessage {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_UserProfileSettings_profile_controls {
|
.mx_UserProfileSettings_profile_controls {
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import { deop, op } from "./slash-commands/op";
|
|||||||
import { CommandCategories } from "./slash-commands/interface";
|
import { CommandCategories } from "./slash-commands/interface";
|
||||||
import { Command } from "./slash-commands/command";
|
import { Command } from "./slash-commands/command";
|
||||||
import { goto, join } from "./slash-commands/join";
|
import { goto, join } from "./slash-commands/join";
|
||||||
|
import { SdkContextClass } from "./contexts/SDKContext";
|
||||||
|
|
||||||
export { CommandCategories, Command };
|
export { CommandCategories, Command };
|
||||||
|
|
||||||
@@ -883,6 +884,25 @@ export const Commands = [
|
|||||||
},
|
},
|
||||||
renderingTypes: [TimelineRenderingType.Room],
|
renderingTypes: [TimelineRenderingType.Room],
|
||||||
}),
|
}),
|
||||||
|
new Command({
|
||||||
|
command: "statusmsg",
|
||||||
|
description: _td("slash_command|status_msg"),
|
||||||
|
category: CommandCategories.actions,
|
||||||
|
args: "<message>",
|
||||||
|
isEnabled: (cli) => !isCurrentLocalRoom(cli),
|
||||||
|
runFn: function (cli, _roomId, _threadId, args) {
|
||||||
|
return success((async () => {
|
||||||
|
const supported = await cli.doesServerSupportExtendedProfiles();
|
||||||
|
if (!supported) return reject(new UserFriendlyError("slash_command|extended_profile_not_supported"));
|
||||||
|
if (!args) return reject(new UserFriendlyError("slash_command|no_status_message"));
|
||||||
|
await cli.setExtendedProfileProperty("uk.half-shot.status", args);
|
||||||
|
// Refresh profile.
|
||||||
|
void SdkContextClass.instance.userProfilesStore.fetchProfile(cli.getSafeUserId());
|
||||||
|
})());
|
||||||
|
},
|
||||||
|
renderingTypes: [TimelineRenderingType.Room],
|
||||||
|
}),
|
||||||
|
|
||||||
|
|
||||||
// Command definitions for autocompletion ONLY:
|
// Command definitions for autocompletion ONLY:
|
||||||
// /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes
|
// /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { RoomMember } from "../../../../models/rooms/RoomMember";
|
|||||||
import { _t, _td, TranslationKey } from "../../../../languageHandler";
|
import { _t, _td, TranslationKey } from "../../../../languageHandler";
|
||||||
import UserIdentifierCustomisations from "../../../../customisations/UserIdentifier";
|
import UserIdentifierCustomisations from "../../../../customisations/UserIdentifier";
|
||||||
import { E2EStatus } from "../../../../utils/ShieldUtils";
|
import { E2EStatus } from "../../../../utils/ShieldUtils";
|
||||||
|
import { useUserProfileValue } from "../../../../hooks/useUserProfileValue";
|
||||||
|
|
||||||
interface MemberTileViewModelProps {
|
interface MemberTileViewModelProps {
|
||||||
member: RoomMember;
|
member: RoomMember;
|
||||||
@@ -30,6 +31,7 @@ export interface MemberTileViewState extends MemberTileViewModelProps {
|
|||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
title?: string;
|
title?: string;
|
||||||
userLabel?: string;
|
userLabel?: string;
|
||||||
|
statusMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PowerStatus {
|
export enum PowerStatus {
|
||||||
@@ -44,10 +46,10 @@ const PowerLabel: Record<PowerStatus, TranslationKey> = {
|
|||||||
|
|
||||||
export function useMemberTileViewModel(props: MemberTileViewModelProps): MemberTileViewState {
|
export function useMemberTileViewModel(props: MemberTileViewModelProps): MemberTileViewState {
|
||||||
const [e2eStatus, setE2eStatus] = useState<E2EStatus | undefined>();
|
const [e2eStatus, setE2eStatus] = useState<E2EStatus | undefined>();
|
||||||
|
const cli = MatrixClientPeg.safeGet();
|
||||||
|
const statusMessage = useUserProfileValue(cli, "uk.half-shot.status", props.member.userId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cli = MatrixClientPeg.safeGet();
|
|
||||||
|
|
||||||
const updateE2EStatus = async (): Promise<void> => {
|
const updateE2EStatus = async (): Promise<void> => {
|
||||||
const { userId } = props.member;
|
const { userId } = props.member;
|
||||||
const isMe = userId === cli.getUserId();
|
const isMe = userId === cli.getUserId();
|
||||||
@@ -103,7 +105,7 @@ export function useMemberTileViewModel(props: MemberTileViewModelProps): MemberT
|
|||||||
cli.removeListener(CryptoEvent.UserTrustStatusChanged, onUserTrustStatusChanged);
|
cli.removeListener(CryptoEvent.UserTrustStatusChanged, onUserTrustStatusChanged);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [props.member]);
|
}, [props.member, cli]);
|
||||||
|
|
||||||
const onClick = (): void => {
|
const onClick = (): void => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
@@ -156,5 +158,6 @@ export function useMemberTileViewModel(props: MemberTileViewModelProps): MemberT
|
|||||||
e2eStatus,
|
e2eStatus,
|
||||||
showPresence: props.showPresence,
|
showPresence: props.showPresence,
|
||||||
userLabel,
|
userLabel,
|
||||||
|
statusMessage: statusMessage ?? undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,11 +28,12 @@ interface IProps {
|
|||||||
colored?: boolean;
|
colored?: boolean;
|
||||||
emphasizeDisplayName?: boolean;
|
emphasizeDisplayName?: boolean;
|
||||||
withTooltip?: boolean;
|
withTooltip?: boolean;
|
||||||
|
statusMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class DisambiguatedProfile extends React.Component<IProps> {
|
export default class DisambiguatedProfile extends React.Component<IProps> {
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
const { fallbackName, member, colored, emphasizeDisplayName, withTooltip, onClick } = this.props;
|
const { fallbackName, member, colored, emphasizeDisplayName, withTooltip, onClick, statusMessage } = this.props;
|
||||||
const rawDisplayName = member?.rawDisplayName || fallbackName;
|
const rawDisplayName = member?.rawDisplayName || fallbackName;
|
||||||
const mxid = member?.userId;
|
const mxid = member?.userId;
|
||||||
|
|
||||||
@@ -42,6 +43,7 @@ export default class DisambiguatedProfile extends React.Component<IProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mxidElement;
|
let mxidElement;
|
||||||
|
let statusElement;
|
||||||
let title: string | undefined;
|
let title: string | undefined;
|
||||||
|
|
||||||
if (mxid) {
|
if (mxid) {
|
||||||
@@ -59,6 +61,10 @@ export default class DisambiguatedProfile extends React.Component<IProps> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (statusMessage) {
|
||||||
|
statusElement = <span className="mx_DisambiguatedProfile_mxid">({statusMessage})</span>;
|
||||||
|
}
|
||||||
|
|
||||||
const displayNameClasses = classNames(colorClass, {
|
const displayNameClasses = classNames(colorClass, {
|
||||||
mx_DisambiguatedProfile_displayName: emphasizeDisplayName,
|
mx_DisambiguatedProfile_displayName: emphasizeDisplayName,
|
||||||
});
|
});
|
||||||
@@ -69,6 +75,7 @@ export default class DisambiguatedProfile extends React.Component<IProps> {
|
|||||||
{rawDisplayName}
|
{rawDisplayName}
|
||||||
</span>
|
</span>
|
||||||
{mxidElement}
|
{mxidElement}
|
||||||
|
{statusElement}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import { MatrixEvent, MsgType } from "matrix-js-sdk/src/matrix";
|
|||||||
|
|
||||||
import DisambiguatedProfile from "./DisambiguatedProfile";
|
import DisambiguatedProfile from "./DisambiguatedProfile";
|
||||||
import { useRoomMemberProfile } from "../../../hooks/room/useRoomMemberProfile";
|
import { useRoomMemberProfile } from "../../../hooks/room/useRoomMemberProfile";
|
||||||
|
import { useUserProfileValue } from "../../../hooks/useUserProfileValue";
|
||||||
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
@@ -24,9 +26,11 @@ export default function SenderProfile({ mxEvent, onClick, withTooltip }: IProps)
|
|||||||
userId: mxEvent.getSender(),
|
userId: mxEvent.getSender(),
|
||||||
member: mxEvent.sender,
|
member: mxEvent.sender,
|
||||||
});
|
});
|
||||||
|
const statusMessage = useUserProfileValue(MatrixClientPeg.safeGet(), "uk.half-shot.status", mxEvent.sender?.userId);
|
||||||
|
|
||||||
return mxEvent.getContent().msgtype !== MsgType.Emote ? (
|
return mxEvent.getContent().msgtype !== MsgType.Emote ? (
|
||||||
<DisambiguatedProfile
|
<DisambiguatedProfile
|
||||||
|
statusMessage={statusMessage ?? undefined}
|
||||||
fallbackName={mxEvent.getSender() ?? ""}
|
fallbackName={mxEvent.getSender() ?? ""}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
member={member}
|
member={member}
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ import { asyncSome } from "../../../utils/arrays";
|
|||||||
import { Flex } from "../../utils/Flex";
|
import { Flex } from "../../utils/Flex";
|
||||||
import CopyableText from "../elements/CopyableText";
|
import CopyableText from "../elements/CopyableText";
|
||||||
import { useUserTimezone } from "../../../hooks/useUserTimezone";
|
import { useUserTimezone } from "../../../hooks/useUserTimezone";
|
||||||
|
import { useUserProfileValue } from "../../../hooks/useUserProfileValue";
|
||||||
export interface IDevice extends Device {
|
export interface IDevice extends Device {
|
||||||
ambiguous?: boolean;
|
ambiguous?: boolean;
|
||||||
}
|
}
|
||||||
@@ -1634,6 +1635,8 @@ export const UserInfoHeader: React.FC<{
|
|||||||
roomId?: string;
|
roomId?: string;
|
||||||
}> = ({ member, e2eStatus, roomId }) => {
|
}> = ({ member, e2eStatus, roomId }) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
|
console.log(cli, member);
|
||||||
|
const statusMessage = useUserProfileValue(cli, "uk.half-shot.status", member.userId, true);
|
||||||
|
|
||||||
const onMemberAvatarClick = useCallback(() => {
|
const onMemberAvatarClick = useCallback(() => {
|
||||||
const avatarUrl = (member as RoomMember).getMxcAvatarUrl
|
const avatarUrl = (member as RoomMember).getMxcAvatarUrl
|
||||||
@@ -1668,6 +1671,7 @@ export const UserInfoHeader: React.FC<{
|
|||||||
showPresence = enablePresenceByHsUrl[cli.baseUrl];
|
showPresence = enablePresenceByHsUrl[cli.baseUrl];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let presenceLabel: JSX.Element | undefined;
|
let presenceLabel: JSX.Element | undefined;
|
||||||
if (showPresence) {
|
if (showPresence) {
|
||||||
presenceLabel = (
|
presenceLabel = (
|
||||||
@@ -1675,10 +1679,13 @@ export const UserInfoHeader: React.FC<{
|
|||||||
activeAgo={presenceLastActiveAgo}
|
activeAgo={presenceLastActiveAgo}
|
||||||
currentlyActive={presenceCurrentlyActive}
|
currentlyActive={presenceCurrentlyActive}
|
||||||
presenceState={presenceState}
|
presenceState={presenceState}
|
||||||
|
statusMessage={statusMessage ?? undefined}
|
||||||
className="mx_UserInfo_profileStatus"
|
className="mx_UserInfo_profileStatus"
|
||||||
coloured
|
coloured
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
} else if (statusMessage) {
|
||||||
|
presenceLabel = <span>{statusMessage}</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
const timezoneInfo = useUserTimezone(cli, member.userId);
|
const timezoneInfo = useUserTimezone(cli, member.userId);
|
||||||
|
|||||||
@@ -19,9 +19,10 @@ interface IEmojiButtonProps {
|
|||||||
addEmoji: (unicode: string) => boolean;
|
addEmoji: (unicode: string) => boolean;
|
||||||
menuPosition?: MenuProps;
|
menuPosition?: MenuProps;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
single?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EmojiButton({ addEmoji, menuPosition, className }: IEmojiButtonProps): JSX.Element {
|
export function EmojiButton({ addEmoji, menuPosition, className, single }: IEmojiButtonProps): JSX.Element {
|
||||||
const overflowMenuCloser = useContext(OverflowMenuContext);
|
const overflowMenuCloser = useContext(OverflowMenuContext);
|
||||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||||
|
|
||||||
@@ -32,10 +33,19 @@ export function EmojiButton({ addEmoji, menuPosition, className }: IEmojiButtonP
|
|||||||
closeMenu();
|
closeMenu();
|
||||||
overflowMenuCloser?.();
|
overflowMenuCloser?.();
|
||||||
};
|
};
|
||||||
|
const onChoose = (emoji: string) => {
|
||||||
|
try {
|
||||||
|
return addEmoji(emoji);
|
||||||
|
} finally {
|
||||||
|
if (single) {
|
||||||
|
onFinished();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
contextMenu = (
|
contextMenu = (
|
||||||
<ContextMenu {...position} onFinished={onFinished} managed={false}>
|
<ContextMenu {...position} onFinished={onFinished} managed={false}>
|
||||||
<EmojiPicker onChoose={addEmoji} onFinished={onFinished} />
|
<EmojiPicker onChoose={onChoose} onFinished={onFinished} />
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export function RoomMemberTileView(props: IProps): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
const name = vm.name;
|
const name = vm.name;
|
||||||
const nameJSX = <DisambiguatedProfile member={member} fallbackName={name || ""} />;
|
const nameJSX = <DisambiguatedProfile statusMessage={vm.statusMessage} member={member} fallbackName={name || ""} />;
|
||||||
|
|
||||||
const presenceState = member.presenceState;
|
const presenceState = member.presenceState;
|
||||||
let presenceJSX: JSX.Element | undefined;
|
let presenceJSX: JSX.Element | undefined;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ interface IProps {
|
|||||||
// whether to apply colouring to the label
|
// whether to apply colouring to the label
|
||||||
coloured?: boolean;
|
coloured?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
statusMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class PresenceLabel extends React.Component<IProps> {
|
export default class PresenceLabel extends React.Component<IProps> {
|
||||||
@@ -57,7 +58,7 @@ export default class PresenceLabel extends React.Component<IProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
return (
|
return <>
|
||||||
<div
|
<div
|
||||||
className={classNames("mx_PresenceLabel", this.props.className, {
|
className={classNames("mx_PresenceLabel", this.props.className, {
|
||||||
mx_PresenceLabel_online: this.props.coloured && this.props.presenceState === "online",
|
mx_PresenceLabel_online: this.props.coloured && this.props.presenceState === "online",
|
||||||
@@ -65,6 +66,8 @@ export default class PresenceLabel extends React.Component<IProps> {
|
|||||||
>
|
>
|
||||||
{this.getPrettyPresence(this.props.presenceState, this.props.activeAgo, this.props.currentlyActive)}
|
{this.getPrettyPresence(this.props.presenceState, this.props.activeAgo, this.props.currentlyActive)}
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
|
<p>{this.props.statusMessage}</p>
|
||||||
|
</>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ import LogoutDialog, { shouldShowLogoutDialog } from "../dialogs/LogoutDialog";
|
|||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
import { Flex } from "../../utils/Flex";
|
import { Flex } from "../../utils/Flex";
|
||||||
|
import { useUserProfileValue } from "../../../hooks/useUserProfileValue";
|
||||||
|
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||||
|
import { EmojiButton } from "../rooms/EmojiButton";
|
||||||
|
|
||||||
const SpinnerToast: React.FC<{ children?: ReactNode }> = ({ children }) => (
|
const SpinnerToast: React.FC<{ children?: ReactNode }> = ({ children }) => (
|
||||||
<>
|
<>
|
||||||
@@ -114,9 +117,21 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({
|
|||||||
const [maxUploadSize, setMaxUploadSize] = useState<number | undefined>();
|
const [maxUploadSize, setMaxUploadSize] = useState<number | undefined>();
|
||||||
const [displayNameError, setDisplayNameError] = useState<boolean>(false);
|
const [displayNameError, setDisplayNameError] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [statusMessageSupported, setStatusMessageSupported] = useState<boolean>(false);
|
||||||
|
const [statusMessageError, setStatusMessageError] = useState<boolean>(false);
|
||||||
|
|
||||||
const toastRack = useToastContext();
|
const toastRack = useToastContext();
|
||||||
|
|
||||||
const client = useMatrixClientContext();
|
const client = useMatrixClientContext();
|
||||||
|
const currentStatusMessage = useUserProfileValue(client, "uk.half-shot.status", client.getSafeUserId(), true);
|
||||||
|
const [statusMessage, setStatusMessage] = useState<string>(currentStatusMessage ?? "");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentStatusMessage) {
|
||||||
|
setStatusMessage(currentStatusMessage);
|
||||||
|
}
|
||||||
|
}, [currentStatusMessage]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -129,6 +144,13 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({
|
|||||||
})();
|
})();
|
||||||
}, [client]);
|
}, [client]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const supported = await client.doesServerSupportExtendedProfiles();
|
||||||
|
setStatusMessageSupported(supported);
|
||||||
|
})();
|
||||||
|
}, [client]);
|
||||||
|
|
||||||
const onAvatarRemove = useCallback(async () => {
|
const onAvatarRemove = useCallback(async () => {
|
||||||
const removeToast = toastRack.displayToast(
|
const removeToast = toastRack.displayToast(
|
||||||
<SpinnerToast>{_t("settings|general|avatar_remove_progress")}</SpinnerToast>,
|
<SpinnerToast>{_t("settings|general|avatar_remove_progress")}</SpinnerToast>,
|
||||||
@@ -190,6 +212,31 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({
|
|||||||
[client],
|
[client],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onStatusChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setStatusMessage(e.target.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onStatusAddEmoji = useCallback((emoij: string) => {
|
||||||
|
setStatusMessage(s => s + emoij);
|
||||||
|
return true;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onStatusCancelled = useCallback(() => {
|
||||||
|
setStatusMessage(currentStatusMessage ?? "");
|
||||||
|
}, [currentStatusMessage]);
|
||||||
|
|
||||||
|
const onStatusSave = useCallback(async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
setStatusMessageError(false);
|
||||||
|
await client.setExtendedProfileProperty("uk.half-shot.status", statusMessage);
|
||||||
|
// Force profile to be refetched.
|
||||||
|
SdkContextClass.instance.userProfilesStore.fetchProfile(client.getSafeUserId());
|
||||||
|
} catch (e) {
|
||||||
|
setStatusMessageError(true);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}, [statusMessage, client]);
|
||||||
|
|
||||||
const someFieldsDisabled = !canSetDisplayName || !canSetAvatar;
|
const someFieldsDisabled = !canSetDisplayName || !canSetAvatar;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -225,6 +272,22 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({
|
|||||||
>
|
>
|
||||||
{displayNameError && <ErrorMessage>{_t("settings|general|display_name_error")}</ErrorMessage>}
|
{displayNameError && <ErrorMessage>{_t("settings|general|display_name_error")}</ErrorMessage>}
|
||||||
</EditInPlace>
|
</EditInPlace>
|
||||||
|
<EditInPlace
|
||||||
|
className="mx_UserProfileSettings_profile_statusMessage"
|
||||||
|
label={_t("settings|general|status_message")}
|
||||||
|
value={statusMessage}
|
||||||
|
saveButtonLabel={_t("common|save")}
|
||||||
|
cancelButtonLabel={_t("common|cancel")}
|
||||||
|
savedLabel={_t("common|saved")}
|
||||||
|
savingLabel={_t("common|updating")}
|
||||||
|
onChange={onStatusChanged}
|
||||||
|
onCancel={onStatusCancelled}
|
||||||
|
onSave={onStatusSave}
|
||||||
|
disabled={!statusMessageSupported}
|
||||||
|
>
|
||||||
|
{statusMessageError && <ErrorMessage>{_t("settings|general|status_message_error")}</ErrorMessage>}
|
||||||
|
</EditInPlace>
|
||||||
|
{statusMessageSupported ? <EmojiButton single addEmoji={onStatusAddEmoji}/> : null}
|
||||||
</div>
|
</div>
|
||||||
{avatarError && (
|
{avatarError && (
|
||||||
<Alert title={_t("settings|general|avatar_upload_error_title")} type="critical">
|
<Alert title={_t("settings|general|avatar_upload_error_title")} type="critical">
|
||||||
|
|||||||
77
src/hooks/useUserProfileValue.ts
Normal file
77
src/hooks/useUserProfileValue.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { SdkContextClass } from "../contexts/SDKContext";
|
||||||
|
import { UserProfilesStoreEvents } from "../stores/UserProfilesStore";
|
||||||
|
import { useTypedEventEmitter } from "./useEventEmitter";
|
||||||
|
/**
|
||||||
|
* Fetch a key from a user's extended profile, and regularly refetch at a
|
||||||
|
* given interval.
|
||||||
|
*
|
||||||
|
* @param cli The Matrix Client instance.
|
||||||
|
* @param key The key to fetch.
|
||||||
|
* @param userId The user who's profile we're interested in.
|
||||||
|
* @param ignoreCache Ignore the cache on first load.
|
||||||
|
* @returns The value from the profile, or null if not set.
|
||||||
|
*/
|
||||||
|
export const useUserProfileValue = (cli: MatrixClient, key: string, userId?: string, ignoreCache?: boolean): string|null => {
|
||||||
|
const [currentValue, setProfileValue] = useState<string|null>(null);
|
||||||
|
const profilesStore = SdkContextClass.instance.userProfilesStore;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!userId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
profilesStore.subscribeToProfile(userId);
|
||||||
|
return () => profilesStore.unsubscribeToProfile(userId);
|
||||||
|
}, [userId]);
|
||||||
|
|
||||||
|
useTypedEventEmitter(profilesStore, UserProfilesStoreEvents.ProfileUpdated, (updatedUserId, updatedProfile) => {
|
||||||
|
if (userId !== updatedUserId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const value = (updatedProfile as any)?.[key];
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
// Err, definitely not a tz.
|
||||||
|
throw Error("Profile value was not a string");
|
||||||
|
}
|
||||||
|
setProfileValue(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!userId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const profile = await (ignoreCache ? profilesStore.fetchProfile(userId) : profilesStore.getOrFetchProfile(userId));
|
||||||
|
// TODO: Types.
|
||||||
|
const value = (profile as any)?.[key];
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
// Err, definitely not a tz.
|
||||||
|
throw Error("Profile value was not a string");
|
||||||
|
}
|
||||||
|
setProfileValue(value);
|
||||||
|
} catch (ex) {
|
||||||
|
if (ex instanceof MatrixError && ex.errcode === "M_NOT_FOUND") {
|
||||||
|
// No timezone set, ignore.
|
||||||
|
setProfileValue(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [userId, key, cli]);
|
||||||
|
|
||||||
|
return currentValue;
|
||||||
|
};
|
||||||
@@ -48,7 +48,6 @@ export const useUserTimezone = (cli: MatrixClient, userId: string): { timezone:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
(async () => {
|
(async () => {
|
||||||
console.log("Trying to fetch TZ");
|
|
||||||
try {
|
try {
|
||||||
const tz = await cli.getExtendedProfileProperty(userId, "us.cloke.msc4175.tz");
|
const tz = await cli.getExtendedProfileProperty(userId, "us.cloke.msc4175.tz");
|
||||||
if (typeof tz !== "string") {
|
if (typeof tz !== "string") {
|
||||||
|
|||||||
@@ -2532,6 +2532,8 @@
|
|||||||
"remove_email_prompt": "Remove %(email)s?",
|
"remove_email_prompt": "Remove %(email)s?",
|
||||||
"remove_msisdn_prompt": "Remove %(phone)s?",
|
"remove_msisdn_prompt": "Remove %(phone)s?",
|
||||||
"spell_check_locale_placeholder": "Choose a locale",
|
"spell_check_locale_placeholder": "Choose a locale",
|
||||||
|
"status_message": "Current status",
|
||||||
|
"status_message_error": "Failed to set status message",
|
||||||
"unable_to_load_emails": "Unable to load email addresses",
|
"unable_to_load_emails": "Unable to load email addresses",
|
||||||
"unable_to_load_msisdns": "Unable to load phone numbers",
|
"unable_to_load_msisdns": "Unable to load phone numbers",
|
||||||
"username": "Username"
|
"username": "Username"
|
||||||
@@ -2961,6 +2963,7 @@
|
|||||||
"error_invalid_room": "Command failed: Unable to find room (%(roomId)s)",
|
"error_invalid_room": "Command failed: Unable to find room (%(roomId)s)",
|
||||||
"error_invalid_runfn": "Command error: Unable to handle slash command.",
|
"error_invalid_runfn": "Command error: Unable to handle slash command.",
|
||||||
"error_invalid_user_in_room": "Could not find user in room",
|
"error_invalid_user_in_room": "Could not find user in room",
|
||||||
|
"extended_profile_not_supported": "Status messages are not supported on this server",
|
||||||
"help": "Displays list of commands with usages and descriptions",
|
"help": "Displays list of commands with usages and descriptions",
|
||||||
"help_dialog_title": "Command Help",
|
"help_dialog_title": "Command Help",
|
||||||
"holdcall": "Places the call in the current room on hold",
|
"holdcall": "Places the call in the current room on hold",
|
||||||
@@ -2984,6 +2987,7 @@
|
|||||||
"myroomnick": "Changes your display nickname in the current room only",
|
"myroomnick": "Changes your display nickname in the current room only",
|
||||||
"nick": "Changes your display nickname",
|
"nick": "Changes your display nickname",
|
||||||
"no_active_call": "No active call in this room",
|
"no_active_call": "No active call in this room",
|
||||||
|
"no_status_message": "You need to provide a status message",
|
||||||
"op": "Define the power level of a user",
|
"op": "Define the power level of a user",
|
||||||
"part_unknown_alias": "Unrecognised room address: %(roomAlias)s",
|
"part_unknown_alias": "Unrecognised room address: %(roomAlias)s",
|
||||||
"plain": "Sends a message as plain text, without interpreting it as markdown",
|
"plain": "Sends a message as plain text, without interpreting it as markdown",
|
||||||
@@ -2999,6 +3003,7 @@
|
|||||||
"server_error_detail": "Server unavailable, overloaded, or something else went wrong.",
|
"server_error_detail": "Server unavailable, overloaded, or something else went wrong.",
|
||||||
"shrug": "Prepends ¯\\_(ツ)_/¯ to a plain-text message",
|
"shrug": "Prepends ¯\\_(ツ)_/¯ to a plain-text message",
|
||||||
"spoiler": "Sends the given message as a spoiler",
|
"spoiler": "Sends the given message as a spoiler",
|
||||||
|
"status_msg": "Update your status message in your profile",
|
||||||
"tableflip": "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message",
|
"tableflip": "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message",
|
||||||
"topic": "Gets or sets the room topic",
|
"topic": "Gets or sets the room topic",
|
||||||
"topic_none": "This room has no topic.",
|
"topic_none": "This room has no topic.",
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ import {
|
|||||||
MatrixEvent,
|
MatrixEvent,
|
||||||
RoomMember,
|
RoomMember,
|
||||||
RoomMemberEvent,
|
RoomMemberEvent,
|
||||||
|
SyncState,
|
||||||
|
TypedEventEmitter,
|
||||||
|
ClientEvent,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { LruCache } from "../utils/LruCache";
|
import { LruCache } from "../utils/LruCache";
|
||||||
@@ -27,18 +30,66 @@ interface GetOptions {
|
|||||||
shouldThrow: boolean;
|
shouldThrow: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum UserProfilesStoreEvents {
|
||||||
|
ProfileUpdated = "profile_updated",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserProfilesStoreEventsMap {
|
||||||
|
[UserProfilesStoreEvents.ProfileUpdated]: (userId: string, profile: IMatrixProfile|null) => void,
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This store provides cached access to user profiles.
|
* This store provides cached access to user profiles.
|
||||||
* Listens for membership events and invalidates the cache for a profile on update with different profile values.
|
* Listens for membership events and invalidates the cache for a profile on update with different profile values.
|
||||||
*/
|
*/
|
||||||
export class UserProfilesStore {
|
export class UserProfilesStore extends TypedEventEmitter<UserProfilesStoreEvents, UserProfilesStoreEventsMap> {
|
||||||
private profiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
|
private profiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
|
||||||
private profileLookupErrors = new LruCache<string, MatrixError>(cacheSize);
|
private profileLookupErrors = new LruCache<string, MatrixError>(cacheSize);
|
||||||
private knownProfiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
|
private knownProfiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
|
||||||
|
|
||||||
|
private readonly profileSubscriptions = new Map<string, {subs: number, lastSync: number}>();
|
||||||
|
|
||||||
public constructor(private client: MatrixClient) {
|
public constructor(private client: MatrixClient) {
|
||||||
|
super();
|
||||||
client.on(RoomMemberEvent.Membership, this.onRoomMembershipEvent);
|
client.on(RoomMemberEvent.Membership, this.onRoomMembershipEvent);
|
||||||
|
client.on(ClientEvent.Sync, (state) => {
|
||||||
|
if (state !== SyncState.Syncing) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
const time = Date.now();
|
||||||
|
for (const [userId, entry] of this.profileSubscriptions.entries()) {
|
||||||
|
if (time - entry.lastSync < 60000) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
void this.fetchProfile(userId, { shouldThrow: false});
|
||||||
|
entry.lastSync = time;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public subscribeToProfile(userId: string) {
|
||||||
|
const existingProfile = this.profileSubscriptions.get(userId);
|
||||||
|
console.log(`Sub for ${userId} incremented to ${(existingProfile?.subs ?? 0) + 1}`);
|
||||||
|
if (existingProfile) {
|
||||||
|
existingProfile.subs += 1;
|
||||||
|
} else {
|
||||||
|
this.profileSubscriptions.set(userId, { subs: 1, lastSync: 0})
|
||||||
|
}
|
||||||
|
console.log(`Number of subs: ${this.profileSubscriptions.size}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsubscribeToProfile(userId: string) {
|
||||||
|
const existingProfile = this.profileSubscriptions.get(userId);
|
||||||
|
if (!existingProfile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
existingProfile.subs -= 1;
|
||||||
|
console.log(`Sub for ${userId} reduced to ${existingProfile.subs}`);
|
||||||
|
if (existingProfile.subs === 0) {
|
||||||
|
this.profileSubscriptions.delete(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronously get a profile from the store cache.
|
* Synchronously get a profile from the store cache.
|
||||||
@@ -104,6 +155,7 @@ export class UserProfilesStore {
|
|||||||
public async fetchProfile(userId: string, options?: GetOptions): Promise<IMatrixProfile | null> {
|
public async fetchProfile(userId: string, options?: GetOptions): Promise<IMatrixProfile | null> {
|
||||||
const profile = await this.fetchProfileFromApi(userId, options);
|
const profile = await this.fetchProfileFromApi(userId, options);
|
||||||
this.profiles.set(userId, profile);
|
this.profiles.set(userId, profile);
|
||||||
|
this.emit(UserProfilesStoreEvents.ProfileUpdated, userId, profile);
|
||||||
return profile;
|
return profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user