diff --git a/web/chat/thread-menu.react.js b/web/chat/thread-menu.react.js index d2f2f4be8..dd83517f8 100644 --- a/web/chat/thread-menu.react.js +++ b/web/chat/thread-menu.react.js @@ -1,230 +1,221 @@ // @flow -import { - faArrowRight, - faBell, - faCog, - faCommentAlt, - faSignOutAlt, - faPlusCircle, - faUserFriends, -} from '@fortawesome/free-solid-svg-icons'; import * as React from 'react'; import { leaveThread, leaveThreadActionTypes, } from 'lib/actions/thread-actions'; import { childThreadInfos } from 'lib/selectors/thread-selectors'; import { threadHasPermission, viewerIsMember, threadIsChannel, } from 'lib/shared/thread-utils'; import { type ThreadInfo, threadTypes, threadPermissions, } from 'lib/types/thread-types'; import { useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils'; import MenuItem from '../components/menu-item.react'; import Menu from '../components/menu.react'; import SidebarListModal from '../modals/chat/sidebar-list-modal.react'; import { useModalContext } from '../modals/modal-provider.react'; import ConfirmLeaveThreadModal from '../modals/threads/confirm-leave-thread-modal.react'; import ThreadMembersModal from '../modals/threads/members/members-modal.react'; import ThreadSettingsModal from '../modals/threads/thread-settings-modal.react'; import { useSelector } from '../redux/redux-utils'; import SWMansionIcon from '../SWMansionIcon.react'; import css from './thread-menu.css'; type ThreadMenuProps = { +threadInfo: ThreadInfo, }; function ThreadMenu(props: ThreadMenuProps): React.Node { const { setModal, clearModal } = useModalContext(); const { threadInfo } = props; const onClickSettings = React.useCallback( () => setModal(), [setModal, threadInfo.id], ); const settingsItem = React.useMemo(() => { return ( ); }, [onClickSettings]); const onClickMembers = React.useCallback( () => setModal( , ), [clearModal, setModal, threadInfo.id], ); const membersItem = React.useMemo(() => { if (threadInfo.type === threadTypes.PERSONAL) { return null; } return ( ); }, [onClickMembers, threadInfo.type]); const childThreads = useSelector( state => childThreadInfos(state)[threadInfo.id], ); const hasSidebars = React.useMemo(() => { return childThreads?.some( childThreadInfo => childThreadInfo.type === threadTypes.SIDEBAR, ); }, [childThreads]); const onClickSidebars = React.useCallback( () => setModal(), [setModal, threadInfo], ); const sidebarItem = React.useMemo(() => { if (!hasSidebars) { return null; } return ( ); }, [hasSidebars, onClickSidebars]); const canCreateSubchannels = React.useMemo( () => threadHasPermission(threadInfo, threadPermissions.CREATE_SUBCHANNELS), [threadInfo], ); const hasSubchannels = React.useMemo(() => { return !!childThreads?.some(threadIsChannel); }, [childThreads]); const viewSubchannelsItem = React.useMemo(() => { if (!hasSubchannels && !canCreateSubchannels) { return null; } return ( - + ); }, [canCreateSubchannels, hasSubchannels]); const createSubchannelsItem = React.useMemo(() => { if (!canCreateSubchannels) { return null; } return ( ); }, [canCreateSubchannels]); const dispatchActionPromise = useDispatchActionPromise(); const callLeaveThread = useServerCall(leaveThread); const onConfirmLeaveThread = React.useCallback(() => { dispatchActionPromise( leaveThreadActionTypes, callLeaveThread(threadInfo.id), ); clearModal(); }, [callLeaveThread, clearModal, dispatchActionPromise, threadInfo.id]); const onClickLeaveThread = React.useCallback( () => setModal( , ), [clearModal, onConfirmLeaveThread, setModal, threadInfo], ); const leaveThreadItem = React.useMemo(() => { const canLeaveThread = threadHasPermission( threadInfo, threadPermissions.LEAVE_THREAD, ); if (!viewerIsMember(threadInfo) || !canLeaveThread) { return null; } return ( ); }, [onClickLeaveThread, threadInfo]); const menuItems = React.useMemo(() => { const notificationsItem = ( - + ); const separator =
; // TODO: Enable menu items when the modals are implemented const SHOW_NOTIFICATIONS = false; const SHOW_VIEW_SUBCHANNELS = false; const SHOW_CREATE_SUBCHANNELS = false; const items = [ settingsItem, SHOW_NOTIFICATIONS && notificationsItem, membersItem, sidebarItem, SHOW_VIEW_SUBCHANNELS && viewSubchannelsItem, SHOW_CREATE_SUBCHANNELS && createSubchannelsItem, leaveThreadItem && separator, leaveThreadItem, ]; return items.filter(Boolean); }, [ settingsItem, membersItem, sidebarItem, viewSubchannelsItem, createSubchannelsItem, leaveThreadItem, ]); const icon = React.useMemo( () => , [], ); return {menuItems}; } export default ThreadMenu; diff --git a/web/components/menu-item.react.js b/web/components/menu-item.react.js index 9a16b6cdb..fff1c7fe6 100644 --- a/web/components/menu-item.react.js +++ b/web/components/menu-item.react.js @@ -1,37 +1,36 @@ // @flow -import type { IconDefinition } from '@fortawesome/fontawesome-common-types'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import classNames from 'classnames'; import * as React from 'react'; +import SWMansionIcon, { type Icon } from '../SWMansionIcon.react'; import css from './menu.css'; type MenuItemProps = { +onClick?: () => mixed, - +icon: IconDefinition, + +icon: Icon, +text: string, +dangerous?: boolean, }; function MenuItem(props: MenuItemProps): React.Node { const { onClick, icon, text, dangerous } = props; const itemClasses = classNames(css.menuAction, { [css.menuActionDangerous]: dangerous, }); return ( ); } const MemoizedMenuItem: React.ComponentType = React.memo( MenuItem, ); export default MemoizedMenuItem; diff --git a/web/components/menu.css b/web/components/menu.css index 98a6f0dae..e07674e0e 100644 --- a/web/components/menu.css +++ b/web/components/menu.css @@ -1,74 +1,80 @@ button.menuButton { background-color: transparent; border: none; cursor: pointer; color: inherit; } div.menuActionList { position: absolute; z-index: 4; display: flex; flex-direction: column; background-color: var(--menu-bg); color: var(--menu-color); border-radius: 4px; padding: 4px 0; line-height: var(--line-height-text); min-width: max-content; } div.menuActionListThreadActions { font-size: var(--m-font-16); top: 40px; right: -20px; } div.menuActionListMemberActions { font-size: var(--xs-font-12); background-color: var(--menu-bg-light); color: var(--menu-color-light); top: 0; right: 5px; } button.menuAction { color: inherit; z-index: 1; background-color: transparent; padding: 12px 16px; line-height: 1.5; background-color: transparent; border: none; cursor: pointer; display: flex; align-items: center; color: inherit; font-size: inherit; } button.menuAction:hover { color: var(--menu-color-hover); } div.menuActionIcon { - font-size: 1.125em; display: flex; justify-content: center; margin-right: 8px; + height: 24px; + width: 24px; +} + +div.menuActionListMemberActions div.menuActionIcon { + height: 18px; + width: 18px; } button.menuActionDangerous { color: var(--menu-color-dangerous); } button.menuActionDangerous:hover { color: var(--menu-color-dangerous-hover); } hr.separator { height: 1px; background: var(--menu-separator-color); margin: 10px 16px; max-width: 130px; border: none; } diff --git a/web/modals/threads/members/member.react.js b/web/modals/threads/members/member.react.js index 6cef6ffdf..673a8d81c 100644 --- a/web/modals/threads/members/member.react.js +++ b/web/modals/threads/members/member.react.js @@ -1,190 +1,185 @@ // @flow -import { - faPlusCircle, - faMinusCircle, - faSignOutAlt, -} from '@fortawesome/free-solid-svg-icons'; import classNames from 'classnames'; import * as React from 'react'; import { removeUsersFromThread, changeThreadMemberRoles, } from 'lib/actions/thread-actions'; import { memberIsAdmin, memberHasAdminPowers, threadHasPermission, removeMemberFromThread, switchMemberAdminRoleInThread, } from 'lib/shared/thread-utils'; import { stringForUser } from 'lib/shared/user-utils'; import type { SetState } from 'lib/types/hook-types'; import { type RelativeMemberInfo, type ThreadInfo, threadPermissions, } from 'lib/types/thread-types'; import { useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils'; import Label from '../../../components/label.react'; import MenuItem from '../../../components/menu-item.react'; import Menu from '../../../components/menu.react'; import SWMansionIcon from '../../../SWMansionIcon.react'; import css from './members-modal.css'; type Props = { +memberInfo: RelativeMemberInfo, +threadInfo: ThreadInfo, +setOpenMenu: SetState, +isMenuOpen: boolean, }; function ThreadMember(props: Props): React.Node { const { memberInfo, threadInfo, setOpenMenu, isMenuOpen } = props; const userName = stringForUser(memberInfo); const onMenuChange = React.useCallback( menuOpen => { if (menuOpen) { setOpenMenu(() => memberInfo.id); } else { setOpenMenu(menu => (menu === memberInfo.id ? null : menu)); } }, [memberInfo.id, setOpenMenu], ); const dispatchActionPromise = useDispatchActionPromise(); const boundRemoveUsersFromThread = useServerCall(removeUsersFromThread); const onClickRemoveUser = React.useCallback( () => removeMemberFromThread( threadInfo, memberInfo, dispatchActionPromise, boundRemoveUsersFromThread, ), [boundRemoveUsersFromThread, dispatchActionPromise, memberInfo, threadInfo], ); const isCurrentlyAdmin = memberIsAdmin(memberInfo, threadInfo); const boundChangeThreadMemberRoles = useServerCall(changeThreadMemberRoles); const onMemberAdminRoleToggled = React.useCallback( () => switchMemberAdminRoleInThread( threadInfo, memberInfo, isCurrentlyAdmin, dispatchActionPromise, boundChangeThreadMemberRoles, ), [ boundChangeThreadMemberRoles, dispatchActionPromise, isCurrentlyAdmin, memberInfo, threadInfo, ], ); const menuItems = React.useMemo(() => { const { role } = memberInfo; if (!role) { return []; } const canRemoveMembers = threadHasPermission( threadInfo, threadPermissions.REMOVE_MEMBERS, ); const canChangeRoles = threadHasPermission( threadInfo, threadPermissions.CHANGE_ROLE, ); const actions = []; const isAdmin = memberIsAdmin(memberInfo, threadInfo); if (canChangeRoles && memberInfo.username && isAdmin) { actions.push( , ); } else if (canChangeRoles && memberInfo.username) { actions.push( , ); } if ( canRemoveMembers && !memberInfo.isViewer && (canChangeRoles || threadInfo.roles[role]?.isDefault) ) { actions.push( , ); } return actions; }, [memberInfo, onClickRemoveUser, onMemberAdminRoleToggled, threadInfo]); const userSettingsIcon = React.useMemo( () => , [], ); const label = React.useMemo(() => { if (memberIsAdmin(memberInfo, threadInfo)) { return ; } else if (memberHasAdminPowers(memberInfo)) { return ; } return null; }, [memberInfo, threadInfo]); const memberContainerClasses = classNames(css.memberContainer, { [css.memberContainerWithMenuOpen]: isMenuOpen, }); return (
{userName} {label}
{menuItems}
); } export default ThreadMember;