diff --git a/web/chat/chat-thread-list-item-menu.react.js b/web/chat/chat-thread-list-item-menu.react.js index 0d82b5969..f95701c51 100644 --- a/web/chat/chat-thread-list-item-menu.react.js +++ b/web/chat/chat-thread-list-item-menu.react.js @@ -1,96 +1,97 @@ // @flow import type { SetThreadUnreadStatusPayload, SetThreadUnreadStatusRequest, } from 'lib/types/activity-types'; -import type { ChatThreadItem } from 'lib/selectors/chat-selectors'; +import type { ThreadInfo } from 'lib/types/thread-types'; import * as React from 'react'; import classNames from 'classnames'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faEllipsisV } from '@fortawesome/free-solid-svg-icons'; import { useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils'; import { setThreadUnreadStatusActionTypes, setThreadUnreadStatus, } from 'lib/actions/activity-actions'; import css from './chat-thread-list.css'; type Props = {| - +item: ChatThreadItem, + +threadInfo: ThreadInfo, + +mostRecentNonLocalMessage: ?string, |}; function ChatThreadListItemMenu(props: Props) { const [menuVisible, setMenuVisible] = React.useState(false); const toggleMenu = React.useCallback(() => { setMenuVisible(!menuVisible); }, [menuVisible]); const hideMenu = React.useCallback(() => { setMenuVisible(false); }, []); - const { threadInfo, mostRecentNonLocalMessage } = props.item; + const { threadInfo, mostRecentNonLocalMessage } = props; const dispatchActionPromise = useDispatchActionPromise(); const boundSetThreadUnreadStatus: ( request: SetThreadUnreadStatusRequest, ) => Promise = useServerCall( setThreadUnreadStatus, ); const toggleUnreadStatus = React.useCallback(() => { const { unread } = threadInfo.currentUser; const request = { threadID: threadInfo.id, unread: !unread, latestMessage: mostRecentNonLocalMessage, }; dispatchActionPromise( setThreadUnreadStatusActionTypes, boundSetThreadUnreadStatus(request), undefined, { threadID: threadInfo.id, unread: !unread, }, ); hideMenu(); }, [ threadInfo, mostRecentNonLocalMessage, dispatchActionPromise, hideMenu, boundSetThreadUnreadStatus, ]); const toggleUnreadStatusButtonText = `Mark as ${ threadInfo.currentUser.unread ? 'read' : 'unread' }`; return (
); } export default ChatThreadListItemMenu; diff --git a/web/chat/chat-thread-list-item.react.js b/web/chat/chat-thread-list-item.react.js index b5ac7e9b2..4a995007c 100644 --- a/web/chat/chat-thread-list-item.react.js +++ b/web/chat/chat-thread-list-item.react.js @@ -1,104 +1,107 @@ // @flow import type { ChatThreadItem } from 'lib/selectors/chat-selectors'; import * as React from 'react'; import classNames from 'classnames'; import { shortAbsoluteDate } from 'lib/utils/date-utils'; import css from './chat-thread-list.css'; import MessagePreview from './message-preview.react'; import ChatThreadListItemMenu from './chat-thread-list-item-menu.react'; import { useSelector } from '../redux/redux-utils'; import { useOnClickThread, useThreadIsActive, } from '../selectors/nav-selectors'; import ChatThreadListSidebar from './chat-thread-list-sidebar.react'; type Props = {| +item: ChatThreadItem, |}; function ChatThreadListItem(props: Props) { const { item } = props; const threadID = item.threadInfo.id; const onClick = useOnClickThread(threadID); const timeZone = useSelector((state) => state.timeZone); const lastActivity = shortAbsoluteDate(item.lastUpdatedTime, timeZone); const active = useThreadIsActive(threadID); const containerClassName = React.useMemo( () => classNames({ [css.thread]: true, [css.activeThread]: active, }), [active], ); const { unread } = item.threadInfo.currentUser; const titleClassName = React.useMemo( () => classNames({ [css.title]: true, [css.unread]: unread, }), [unread], ); const lastActivityClassName = React.useMemo( () => classNames({ [css.lastActivity]: true, [css.unread]: unread, [css.dark]: !unread, }), [unread], ); const { color } = item.threadInfo; const colorSplotchStyle = React.useMemo( () => ({ backgroundColor: `#${color}` }), [color], ); const sidebars = item.sidebars.map((sidebarItem) => { if (sidebarItem.type === 'sidebar') { const { type, ...sidebarInfo } = sidebarItem; return ( ); } else { return null; } }); return ( <>
{item.threadInfo.uiName}
{lastActivity}
- +
{sidebars} ); } export default ChatThreadListItem; diff --git a/web/chat/chat-thread-list-sidebar.react.js b/web/chat/chat-thread-list-sidebar.react.js index b449f2df7..89e42d4bd 100644 --- a/web/chat/chat-thread-list-sidebar.react.js +++ b/web/chat/chat-thread-list-sidebar.react.js @@ -1,27 +1,32 @@ // @flow import type { SidebarInfo } from 'lib/types/thread-types'; import * as React from 'react'; import classNames from 'classnames'; import css from './chat-thread-list.css'; import SidebarItem from './sidebar-item.react'; import { useThreadIsActive } from '../selectors/nav-selectors'; +import ChatThreadListItemMenu from './chat-thread-list-item-menu.react'; type Props = {| +sidebarInfo: SidebarInfo, |}; function ChatThreadListSidebar(props: Props) { - const { threadInfo } = props.sidebarInfo; + const { threadInfo, mostRecentNonLocalMessage } = props.sidebarInfo; const threadID = threadInfo.id; const active = useThreadIsActive(threadID); const activeStyle = active ? css.activeThread : null; return (
+
); } export default ChatThreadListSidebar; diff --git a/web/chat/chat-thread-list.css b/web/chat/chat-thread-list.css index 1c0e8b59b..b339d6281 100644 --- a/web/chat/chat-thread-list.css +++ b/web/chat/chat-thread-list.css @@ -1,129 +1,135 @@ div.thread { display: flex; flex-direction: row; padding: 5px 5px 5px 15px; } div.thread:hover { background-color: #EEEEEE; } div.activeThread { background-color: #EEEEEE; } div.thread div.title { font-size: 16px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: black; } a.threadButton { flex: 1; cursor: pointer; overflow: hidden; } div.threadRow { display: flex; justify-content: space-between; align-items: center; } div.colorSplotch { height: 20px; width: 20px; } div.lastActivity { font-size: 15px; white-space: nowrap; } div.lastMessage { font-size: 16px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; } div.unread { color: black; font-weight: 600; } .black { color: black; } div.dark { color: #666666; } .light { color: #AAAAAA; } div.italic { font-style: italic; } div.thread div.sidebarTitle { flex: 1; font-size: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: black; align-self: flex-start; } div.sidebarLastActivity { white-space: nowrap; font-size: 14px; } svg.sidebarIcon { color: black; padding: 0 6px; font-size: 20px; } +div.sidebar .menu > button svg { + font-size: 16px; +} .menu { position: relative; display: flex; margin: 0 5px; } .menu > button { background-color: transparent; border: none; border-radius: 5px; cursor: pointer; padding: 0 10px; } .menu > button:hover { background-color: #DDDDDD; } +.menu > button:focus { + outline: none; +} .menu > button svg { - font-size: 26px; + font-size: 20px; } .menuContent { display: none; position: absolute; - top: 41px; + top: calc(100% + 1px); right: 0; z-index: 1; width: max-content; overflow: hidden; background-color: #EEEEEE; border-radius: 5px; box-shadow: 1px 1px 5px 2px #00000022; } .menuContentVisible { display: block; } .menuContent ul { list-style: none; } .menuContent li:not(:last-child) { border-bottom: 1px solid #DDDDDD; } .menuContent button { border: none; cursor: pointer; padding: 10px; font-size: 16px; } .menuContent button:hover { background-color: #DDDDDD; }