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 (
+
+
+
+ );
+}
+
+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 (
);
}
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;