diff --git a/web/chat/thread-menu.css b/web/chat/thread-menu.css index 31dd25cac..7e2503323 100644 --- a/web/chat/thread-menu.css +++ b/web/chat/thread-menu.css @@ -1,59 +1,7 @@ -button.topBarMenuButton { - background-color: transparent; - border: none; - cursor: pointer; - color: var(--thread-top-bar-menu-color); -} - -div.topBarMenuActionList { - position: absolute; - right: 10px; - top: 55px; - z-index: 1; - display: flex; - flex-direction: column; - background-color: var(--thread-menu-bg); - border-radius: 4px; - padding: 4px 0; -} - -button.topBarMenuAction { - z-index: 1; - background-color: transparent; - padding: 12px 16px; - color: var(--thread-menu-color); - background-color: var(--thread-menu-bg); - font-size: var(--m-font-16); - line-height: 1.5; - border: none; - cursor: pointer; - display: flex; - align-items: center; -} - -button.topBarMenuAction:hover { - color: var(--thread-menu-color-hover); -} - -div.topBarMenuActionIcon { - font-size: var(--l-font-18); - display: flex; - justify-content: center; - margin-right: 8px; - width: 20px; -} - -button.topBarMenuActionDangerous { - color: var(--thread-menu-color-dangerous); -} -button.topBarMenuActionDangerous:hover { - color: var(--thread-menu-color-dangerous-hover); -} - hr.separator { height: 1px; - background: var(--thread-menu-separator-color); + background: var(--menu-separator-color); margin: 10px 16px; max-width: 130px; border: none; } diff --git a/web/chat/thread-menu.react.js b/web/chat/thread-menu.react.js index 0f73d720e..f7b5cca0f 100644 --- a/web/chat/thread-menu.react.js +++ b/web/chat/thread-menu.react.js @@ -1,255 +1,216 @@ // @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 ThreadSettingsModal from '../modals/threads/thread-settings-modal.react'; import { useSelector } from '../redux/redux-utils'; import SWMansionIcon from '../SWMansionIcon.react'; -import ThreadMenuItem from './thread-menu-item.react'; import css from './thread-menu.css'; type ThreadMenuProps = { +threadInfo: ThreadInfo, }; function ThreadMenu(props: ThreadMenuProps): React.Node { - const [isOpen, setIsOpen] = React.useState(false); - const { setModal, clearModal } = useModalContext(); const { threadInfo } = props; const onClickSettings = React.useCallback( () => setModal(), [setModal, threadInfo.id], ); const settingsItem = React.useMemo(() => { return ( - ); }, [onClickSettings]); const membersItem = React.useMemo(() => { if (threadInfo.type === threadTypes.PERSONAL) { return null; } - return ; + return ; }, [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_MEMBERS = false; const SHOW_VIEW_SUBCHANNELS = false; const SHOW_CREATE_SUBCHANNELS = false; const items = [ settingsItem, SHOW_NOTIFICATIONS && notificationsItem, SHOW_MEMBERS && membersItem, sidebarItem, SHOW_VIEW_SUBCHANNELS && viewSubchannelsItem, SHOW_CREATE_SUBCHANNELS && createSubchannelsItem, leaveThreadItem && separator, leaveThreadItem, ]; return items.filter(Boolean); }, [ settingsItem, membersItem, sidebarItem, viewSubchannelsItem, createSubchannelsItem, leaveThreadItem, ]); - - const closeMenuCallback = React.useCallback(() => { - document.removeEventListener('click', closeMenuCallback); - if (isOpen) { - setIsOpen(false); - } - }, [isOpen]); - - React.useEffect(() => { - if (!document || !isOpen) { - return undefined; - } - document.addEventListener('click', closeMenuCallback); - return () => document.removeEventListener('click', closeMenuCallback); - }, [closeMenuCallback, isOpen]); - - const switchMenuCallback = React.useCallback(() => { - setIsOpen(isMenuOpen => !isMenuOpen); - }, []); - - if (menuItems.length === 0) { - return null; - } - - let menuActionList = null; - if (isOpen) { - menuActionList = ( -
{menuItems}
- ); - } - - return ( - <> - - {menuActionList} - + const icon = React.useMemo( + () => , + [], ); + return {menuItems}; } export default ThreadMenu; diff --git a/web/chat/thread-menu-item.react.js b/web/components/menu-item.react.js similarity index 57% rename from web/chat/thread-menu-item.react.js rename to web/components/menu-item.react.js index 5e93597f6..9a16b6cdb 100644 --- a/web/chat/thread-menu-item.react.js +++ b/web/components/menu-item.react.js @@ -1,37 +1,37 @@ // @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 css from './thread-menu.css'; +import css from './menu.css'; -type ThreadMenuItemProps = { +type MenuItemProps = { +onClick?: () => mixed, +icon: IconDefinition, +text: string, +dangerous?: boolean, }; -function ThreadMenuItem(props: ThreadMenuItemProps): React.Node { +function MenuItem(props: MenuItemProps): React.Node { const { onClick, icon, text, dangerous } = props; - const itemClasses = classNames(css.topBarMenuAction, { - [css.topBarMenuActionDangerous]: dangerous, + const itemClasses = classNames(css.menuAction, { + [css.menuActionDangerous]: dangerous, }); return ( ); } -const MemoizedThreadMenuItem: React.ComponentType = React.memo( - ThreadMenuItem, +const MemoizedMenuItem: React.ComponentType = React.memo( + MenuItem, ); -export default MemoizedThreadMenuItem; +export default MemoizedMenuItem; diff --git a/web/chat/thread-menu.css b/web/components/menu.css similarity index 56% copy from web/chat/thread-menu.css copy to web/components/menu.css index 31dd25cac..bc02a3e79 100644 --- a/web/chat/thread-menu.css +++ b/web/components/menu.css @@ -1,59 +1,59 @@ -button.topBarMenuButton { +button.menuButton { background-color: transparent; border: none; cursor: pointer; color: var(--thread-top-bar-menu-color); } -div.topBarMenuActionList { +div.menuActionList { position: absolute; right: 10px; top: 55px; z-index: 1; display: flex; flex-direction: column; - background-color: var(--thread-menu-bg); + background-color: var(--menu-bg); border-radius: 4px; padding: 4px 0; } -button.topBarMenuAction { +button.menuAction { z-index: 1; background-color: transparent; padding: 12px 16px; - color: var(--thread-menu-color); - background-color: var(--thread-menu-bg); + color: var(--menu-color); + background-color: var(--menu-bg); font-size: var(--m-font-16); line-height: 1.5; border: none; cursor: pointer; display: flex; align-items: center; } -button.topBarMenuAction:hover { - color: var(--thread-menu-color-hover); +button.menuAction:hover { + color: var(--menu-color-hover); } -div.topBarMenuActionIcon { +div.menuActionIcon { font-size: var(--l-font-18); display: flex; justify-content: center; margin-right: 8px; width: 20px; } -button.topBarMenuActionDangerous { - color: var(--thread-menu-color-dangerous); +button.menuActionDangerous { + color: var(--menu-color-dangerous); } -button.topBarMenuActionDangerous:hover { - color: var(--thread-menu-color-dangerous-hover); +button.menuActionDangerous:hover { + color: var(--menu-color-dangerous-hover); } hr.separator { height: 1px; - background: var(--thread-menu-separator-color); + background: var(--menu-separator-color); margin: 10px 16px; max-width: 130px; border: none; } diff --git a/web/components/menu.react.js b/web/components/menu.react.js new file mode 100644 index 000000000..965489c75 --- /dev/null +++ b/web/components/menu.react.js @@ -0,0 +1,55 @@ +// @flow + +import * as React from 'react'; + +import css from './menu.css'; + +type MenuProps = { + +icon: React.Node, + +children?: React.Node, +}; + +function Menu(props: MenuProps): React.Node { + const [isOpen, setIsOpen] = React.useState(false); + + const { icon, children } = props; + + const closeMenuCallback = React.useCallback(() => { + document.removeEventListener('click', closeMenuCallback); + if (isOpen) { + setIsOpen(false); + } + }, [isOpen]); + + React.useEffect(() => { + if (!document || !isOpen) { + return undefined; + } + document.addEventListener('click', closeMenuCallback); + return () => document.removeEventListener('click', closeMenuCallback); + }, [closeMenuCallback, isOpen]); + + const switchMenuCallback = React.useCallback(() => { + setIsOpen(isMenuOpen => !isMenuOpen); + }, []); + + if (React.Children.count(children) === 0) { + return null; + } + + let menuActionList = null; + if (isOpen) { + menuActionList =
{children}
; + } + + return ( +
+ + {menuActionList} +
+ ); +} + +export default Menu; diff --git a/web/theme.css b/web/theme.css index 6f922f6a2..0e127aff5 100644 --- a/web/theme.css +++ b/web/theme.css @@ -1,133 +1,133 @@ :root { /* Never use color values defined here directly in CSS. Add color variables to "Color Theme" below The reason we never use color values defined here directly in CSS is 1. It makes changing themes from light / dark / user generated impossible. 2. Gives the programmer context into the color being used. 3. If our color system changes it's much easier to change color values in one place. Add a color value to the theme below, and then use it in your CSS. naming convention: - bg: background. - fg: foreground. - color: text-color */ --shades-white-100: #ffffff; --shades-white-90: #f5f5f5; --shades-white-80: #ebebeb; --shades-white-70: #e0e0e0; --shades-white-60: #cccccc; --shades-black-100: #0a0a0a; --shades-black-90: #1f1f1f; --shades-black-80: #404040; --shades-black-70: #666666; --shades-black-60: #808080; --violet-dark-100: #7e57c2; --violet-dark-80: #6d49ab; --violet-dark-60: #563894; --violet-dark-40: #44297a; --violet-dark-20: #331f5c; --violet-light-100: #ae94db; --violet-light-80: #b9a4df; --violet-light-60: #d3c6ec; --violet-light-40: #e8e0f5; --violet-light-20: #f3f0fa; --success-light-10: #d5f6e3; --success-light-50: #6cdf9c; --success-primary: #00c853; --success-dark-50: #029841; --success-dark-90: #034920; --error-light-10: #feebe6; --error-light-50: #f9947b; --error-primary: #f53100; --error-dark-50: #b62602; --error-dark-90: #4f1203; --bg: var(--shades-black-100); --fg: var(--shades-white-100); --color-disabled: var(--shades-black-60); --text-input-bg: var(--shades-black-80); --text-input-color: var(--shades-white-60); --text-input-placeholder: var(--shades-white-60); --border: var(--shades-black-80); --error: var(--error-primary); --success: var(--success-dark-50); /* Color Theme */ --btn-bg-primary: var(--violet-dark-100); --btn-bg-primary-hover: var(--violet-dark-80); --btn-bg-danger: var(--error-primary); --chat-bg: var(--violet-dark-80); --chat-confirmation-icon: var(--violet-dark-100); --keyserver-selection: var(--violet-dark-60); --thread-selection: var(--violet-light-80); --thread-hover-bg: var(--shades-black-90); --thread-active-bg: var(--shades-black-80); --chat-timestamp-color: var(--shades-black-60); --tool-tip-bg: var(--shades-black-80); --tool-tip-color: var(--shades-white-60); --border-color: var(--shades-black-80); --calendar-chevron: var(--shades-black-60); --calendar-day-bg: var(--shades-black-60); --calendar-day-selected-color: var(--violet-dark-80); --community-bg: var(--shades-black-90); --unread-bg: var(--error-primary); --settings-btn-bg: var(--violet-dark-100); --modal-bg: var(--shades-black-90); --join-bg: var(--shades-black-90); --help-color: var(--shades-black-60); --breadcrumb-color: var(--shades-white-60); --breadcrumb-color-unread: var(--shades-white-60); --btn-secondary-border: var(--shades-black-60); --thread-color-read: var(--shades-black-60); --thread-from-color-read: var(--shades-black-80); --thread-last-message-color-read: var(--shades-black-60); --relationship-button-green: var(--success-dark-50); --relationship-button-red: var(--error-primary); --relationship-button-text: var(--fg); --disconnected-bar-alert-bg: var(--error-dark-50); --disconnected-bar-alert-color: var(--shades-white-100); --disconnected-bar-connecting-bg: var(--shades-white-70); --disconnected-bar-connecting-color: var(--shades-black-100); --permission-color: var(--shades-white-60); --thread-top-bar-color: var(--shades-white-100); --thread-top-bar-menu-color: var(--shades-white-70); --thread-ancestor-keyserver-border: var(--shades-black-70); --thread-ancestor-color-light: var(--shades-white-70); --thread-ancestor-color-dark: var(--shades-black-100); --thread-ancestor-separator-color: var(--shades-white-60); --text-message-default-background: var(--shades-black-80); --message-action-tooltip-bg: var(--shades-black-90); - --thread-menu-bg: var(--shades-black-90); - --thread-menu-separator-color: var(--shades-black-80); - --thread-menu-color: var(--shades-black-60); - --thread-menu-color-hover: var(--shades-white-100); - --thread-menu-color-dangerous: var(--error-primary); - --thread-menu-color-dangerous-hover: var(--error-light-50); + --menu-bg: var(--shades-black-90); + --menu-separator-color: var(--shades-black-80); + --menu-color: var(--shades-black-60); + --menu-color-hover: var(--shades-white-100); + --menu-color-dangerous: var(--error-primary); + --menu-color-dangerous-hover: var(--error-light-50); --app-list-icon-read-only-color: var(--shades-black-60); --app-list-icon-enabled-color: var(--success-primary); --app-list-icon-disabled-color: var(--shades-white-80); --account-settings-label: var(--shades-black-60); --account-button-color: var(--violet-dark-100); --chat-thread-list-menu-color: var(--shades-white-60); --chat-thread-list-menu-bg: var(--shades-black-80); --chat-thread-list-menu-active-color: var(--shades-white-60); --chat-thread-list-menu-active-bg: var(--shades-black-90); --search-clear-color: var(--shades-white-100); --search-clear-bg: var(--shades-black-70); --search-input-color: var(--shades-white-100); --search-input-placeholder: var(--shades-black-60); --search-icon-color: var(--shades-black-60); --tabs-header-active-color: var(--shades-white-100); --tabs-header-active-border: var(--violet-light-100); --tabs-header-background-color: var(--shades-black-60); --tabs-header-background-border: var(--shades-black-80); --tabs-header-background-color-hover: var(--shades-white-80); --tabs-header-background-border-hover: var(--shades-black-70); }