diff --git a/web/avatars/edit-user-avatar-menu.css b/web/avatars/edit-avatar-menu.css similarity index 100% rename from web/avatars/edit-user-avatar-menu.css rename to web/avatars/edit-avatar-menu.css diff --git a/web/avatars/edit-thread-avatar-menu.react.js b/web/avatars/edit-thread-avatar-menu.react.js new file mode 100644 index 000000000..69eb97b4c --- /dev/null +++ b/web/avatars/edit-thread-avatar-menu.react.js @@ -0,0 +1,57 @@ +// @flow + +import invariant from 'invariant'; +import * as React from 'react'; + +import { EditThreadAvatarContext } from 'lib/components/base-edit-thread-avatar-provider.react.js'; +import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; +import type { RawThreadInfo, ThreadInfo } from 'lib/types/thread-types.js'; + +import css from './edit-avatar-menu.css'; +import MenuItem from '../components/menu-item.react.js'; +import Menu from '../components/menu.react.js'; + +const editIcon = ( +
+ +
+); + +type Props = { + +threadInfo: RawThreadInfo | ThreadInfo, +}; +function EditThreadAvatarMenu(props: Props): React.Node { + const { threadInfo } = props; + + const editThreadAvatarContext = React.useContext(EditThreadAvatarContext); + invariant(editThreadAvatarContext, 'editThreadAvatarContext should be set'); + + const { baseSetThreadAvatar } = editThreadAvatarContext; + + const removeThreadAvatar = React.useCallback( + () => baseSetThreadAvatar(threadInfo.id, { type: 'remove' }), + [baseSetThreadAvatar, threadInfo.id], + ); + + const removeMenuItem = React.useMemo( + () => ( + + ), + [removeThreadAvatar], + ); + + const menuItems = React.useMemo(() => [removeMenuItem], [removeMenuItem]); + + return ( +
+ {menuItems} +
+ ); +} + +export default EditThreadAvatarMenu; diff --git a/web/avatars/edit-thread-avatar.react.js b/web/avatars/edit-thread-avatar.react.js index ebc6edc87..4723b9999 100644 --- a/web/avatars/edit-thread-avatar.react.js +++ b/web/avatars/edit-thread-avatar.react.js @@ -1,35 +1,48 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { EditThreadAvatarContext } from 'lib/components/base-edit-thread-avatar-provider.react.js'; +import { threadHasPermission } from 'lib/shared/thread-utils.js'; +import { threadPermissions } from 'lib/types/thread-permission-types.js'; import type { RawThreadInfo, ThreadInfo } from 'lib/types/thread-types.js'; +import EditThreadAvatarMenu from './edit-thread-avatar-menu.react.js'; import css from './edit-thread-avatar.css'; import ThreadAvatar from './thread-avatar.react.js'; type Props = { +threadInfo: RawThreadInfo | ThreadInfo, +disabled?: boolean, }; function EditThreadAvatar(props: Props): React.Node { const editThreadAvatarContext = React.useContext(EditThreadAvatarContext); invariant(editThreadAvatarContext, 'editThreadAvatarContext should be set'); const { threadAvatarSaveInProgress } = editThreadAvatarContext; const { threadInfo } = props; + const canEditThreadAvatar = threadHasPermission( + threadInfo, + threadPermissions.EDIT_THREAD_AVATAR, + ); + + let editThreadAvatarMenu; + if (canEditThreadAvatar && !threadAvatarSaveInProgress) { + editThreadAvatarMenu = ; + } return (
+ {editThreadAvatarMenu}
); } export default EditThreadAvatar; diff --git a/web/avatars/edit-user-avatar-menu.react.js b/web/avatars/edit-user-avatar-menu.react.js index dc854b4b8..8f17f0081 100644 --- a/web/avatars/edit-user-avatar-menu.react.js +++ b/web/avatars/edit-user-avatar-menu.react.js @@ -1,156 +1,156 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { EditUserAvatarContext } from 'lib/components/edit-user-avatar-provider.react.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import { useENSAvatar } from 'lib/hooks/ens-cache.js'; import { getETHAddressForUserInfo } from 'lib/shared/account-utils.js'; import { useUploadAvatarMedia } from './avatar-hooks.react.js'; -import css from './edit-user-avatar-menu.css'; +import css from './edit-avatar-menu.css'; import EmojiAvatarSelectionModal from './emoji-avatar-selection-modal.react.js'; import CommIcon from '../CommIcon.react.js'; import MenuItem from '../components/menu-item.react.js'; import Menu from '../components/menu.react.js'; import { allowedMimeTypeString } from '../media/file-utils.js'; import { useSelector } from '../redux/redux-utils.js'; const editIcon = (
); function EditUserAvatarMenu(): React.Node { const currentUserInfo = useSelector(state => state.currentUserInfo); const ethAddress: ?string = React.useMemo( () => getETHAddressForUserInfo(currentUserInfo), [currentUserInfo], ); const ensAvatarURI: ?string = useENSAvatar(ethAddress); const editUserAvatarContext = React.useContext(EditUserAvatarContext); invariant(editUserAvatarContext, 'editUserAvatarContext should be set'); const { baseSetUserAvatar } = editUserAvatarContext; const removeUserAvatar = React.useCallback( () => baseSetUserAvatar({ type: 'remove' }), [baseSetUserAvatar], ); const { pushModal } = useModalContext(); const openEmojiSelectionModal = React.useCallback( () => pushModal(), [pushModal], ); const emojiMenuItem = React.useMemo( () => ( ), [openEmojiSelectionModal], ); const imageInputRef = React.useRef(); const onImageMenuItemClicked = React.useCallback( () => imageInputRef.current?.click(), [], ); const uploadAvatarMedia = useUploadAvatarMedia(); const onImageSelected = React.useCallback( async event => { const uploadResult = await uploadAvatarMedia(event.target.files[0]); baseSetUserAvatar({ type: 'image', uploadID: uploadResult.id }); }, [baseSetUserAvatar, uploadAvatarMedia], ); const imageMenuItem = React.useMemo( () => ( ), [onImageMenuItemClicked], ); const setENSUserAvatar = React.useCallback( () => baseSetUserAvatar({ type: 'ens' }), [baseSetUserAvatar], ); const ethereumIcon = React.useMemo( () => , [], ); const ensMenuItem = React.useMemo( () => ( ), [ethereumIcon, setENSUserAvatar], ); const removeMenuItem = React.useMemo( () => ( ), [removeUserAvatar], ); const menuItems = React.useMemo(() => { const items = [emojiMenuItem, imageMenuItem]; if (ensAvatarURI) { items.push(ensMenuItem); } if (currentUserInfo?.avatar) { items.push(removeMenuItem); } return items; }, [ currentUserInfo?.avatar, emojiMenuItem, ensAvatarURI, ensMenuItem, imageMenuItem, removeMenuItem, ]); return (
{menuItems}
); } export default EditUserAvatarMenu; diff --git a/web/components/menu.react.js b/web/components/menu.react.js index a56201af2..a000ef8d6 100644 --- a/web/components/menu.react.js +++ b/web/components/menu.react.js @@ -1,125 +1,126 @@ // @flow import classnames from 'classnames'; import * as React from 'react'; import css from './menu.css'; import { useRenderMenu } from '../menu-provider.react.js'; type MenuVariant = | 'thread-actions' | 'member-actions' | 'community-actions' | 'role-actions'; type MenuProps = { +icon: React.Node, +children?: React.Node, +variant?: MenuVariant, +onChange?: boolean => void, }; function Menu(props: MenuProps): React.Node { const buttonRef = React.useRef(); const { renderMenu, setMenuPosition, closeMenu, setCurrentOpenMenu, currentOpenMenu, } = useRenderMenu(); const { icon, children, variant = 'thread-actions', onChange } = props; const ourSymbol = React.useRef(Symbol()); const menuActionListClasses = classnames(css.menuActionList, { [css.menuActionListThreadActions]: variant === 'thread-actions', [css.menuActionListMemberActions]: variant === 'member-actions', [css.menuActionListCommunityActions]: variant === 'community-actions', [css.menuActionListRoleActions]: variant === 'role-actions', }); const menuActionList = React.useMemo( () =>
{children}
, [children, menuActionListClasses], ); const isOurMenuOpen = currentOpenMenu === ourSymbol.current; const updatePosition = React.useCallback(() => { if (buttonRef.current && isOurMenuOpen) { const { top, left } = buttonRef.current.getBoundingClientRect(); setMenuPosition({ top, left }); } }, [isOurMenuOpen, setMenuPosition]); React.useEffect(() => { if (!window) { return undefined; } window.addEventListener('resize', updatePosition); return () => window.removeEventListener('resize', updatePosition); }, [updatePosition]); React.useEffect(updatePosition, [updatePosition]); const closeMenuCallback = React.useCallback(() => { closeMenu(ourSymbol.current); }, [closeMenu]); React.useEffect(() => { onChange?.(isOurMenuOpen); }, [isOurMenuOpen, onChange]); React.useEffect(() => { if (!isOurMenuOpen) { return undefined; } document.addEventListener('click', closeMenuCallback); return () => { document.removeEventListener('click', closeMenuCallback); }; }, [closeMenuCallback, isOurMenuOpen]); const prevActionListRef = React.useRef(null); React.useEffect(() => { if (!isOurMenuOpen) { prevActionListRef.current = null; return; } if (prevActionListRef.current === menuActionList) { return; } renderMenu(menuActionList); prevActionListRef.current = menuActionList; }, [isOurMenuOpen, menuActionList, renderMenu]); React.useEffect(() => { const ourSymbolValue = ourSymbol.current; return () => closeMenu(ourSymbolValue); }, [closeMenu]); const onClickMenuCallback = React.useCallback( e => { e.stopPropagation(); setCurrentOpenMenu(ourSymbol.current); }, [setCurrentOpenMenu], ); if (React.Children.count(children) === 0) { return null; } return ( ); } export default Menu;