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 ;
}
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}
);
}
export default ThreadMember;