/* Copyright 2024 New Vector Ltd. Copyright 2019-2024 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ import React, { ReactNode, createRef, useCallback, useEffect, useState } from "react"; import EditIcon from "@vector-im/compound-design-tokens/assets/web/icons/edit"; import UploadIcon from "@vector-im/compound-design-tokens/assets/web/icons/share"; import DeleteIcon from "@vector-im/compound-design-tokens/assets/web/icons/delete"; import { Menu, MenuItem } from "@vector-im/compound-web"; import classNames from "classnames"; import { _t } from "../../../languageHandler"; import { mediaFromMxc } from "../../../customisations/Media"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import { useId } from "../../../utils/useId"; import AccessibleButton from "../elements/AccessibleButton"; import BaseAvatar from "../avatars/BaseAvatar"; interface MenuProps { trigger: ReactNode; onUploadSelect: () => void; onRemoveSelect?: () => void; menuOpen: boolean; onOpenChange: (newOpen: boolean) => void; } const AvatarSettingContextMenu: React.FC = ({ trigger, onUploadSelect, onRemoveSelect, menuOpen, onOpenChange, }) => { return ( } label={_t("action|upload_file")} onSelect={onUploadSelect} /> {onRemoveSelect && ( } className="mx_AvatarSetting_removeMenuItem" label={_t("action|remove")} onSelect={onRemoveSelect} /> )} ); }; interface IProps { /** * The current value of the avatar URL, as an mxc URL or a File. * Generally, an mxc URL would be specified until the user selects a file, then * the file supplied by the onChange callback would be supplied here until it's * saved. */ avatar?: string | File; /** * If true, the user cannot change the avatar */ disabled?: boolean; /** * Called when the user has selected a new avatar * The callback is passed a File object for the new avatar data */ onChange?: (f: File) => void; /** * Called when the user wishes to remove the avatar */ removeAvatar?: () => void; /** * The alt text for the avatar */ avatarAltText: string; /** * String to use for computing the colour of the placeholder avatar if no avatar is set */ placeholderId: string; /** * String to use for the placeholder display if no avatar is set */ placeholderName: string; } /** * Component for setting or removing an avatar on something (eg. a user or a room) */ const AvatarSetting: React.FC = ({ avatar, avatarAltText, onChange, removeAvatar, disabled, placeholderId, placeholderName, }) => { const fileInputRef = createRef(); // Real URL that we can supply to the img element, either a data URL or whatever mediaFromMxc gives // This represents whatever avatar the user has chosen at the time const [avatarURL, setAvatarURL] = useState(undefined); useEffect(() => { if (avatar instanceof File) { const reader = new FileReader(); reader.onload = () => { setAvatarURL(reader.result as string); }; reader.readAsDataURL(avatar); } else if (avatar) { setAvatarURL(mediaFromMxc(avatar).getSquareThumbnailHttp(96) ?? undefined); } else { setAvatarURL(undefined); } }, [avatar]); // Prevents ID collisions when this component is used more than once on the same page. const a11yId = useId(); const onFileChanged = useCallback( (e: React.ChangeEvent) => { if (e.target.files) onChange?.(e.target.files[0]); }, [onChange], ); const uploadAvatar = useCallback((): void => { fileInputRef.current?.click(); }, [fileInputRef]); const [menuOpen, setMenuOpen] = useState(false); const onOpenChange = useCallback((newOpen: boolean) => { setMenuOpen(newOpen); }, []); let avatarElement = ( ); if (avatarURL) { avatarElement = ( ); } let uploadAvatarBtn: JSX.Element | undefined; if (!disabled) { const uploadButtonClasses = classNames("mx_AvatarSetting_uploadButton", { mx_AvatarSetting_uploadButton_active: menuOpen, }); uploadAvatarBtn = (
); } const content = (
{avatarElement} {uploadAvatarBtn}
); if (disabled) { return content; } return ( <> ); }; export default AvatarSetting;