diff --git a/native/chat/chat-thread-list-item.react.js b/native/chat/chat-thread-list-item.react.js index 67940afaf..c95bf8082 100644 --- a/native/chat/chat-thread-list-item.react.js +++ b/native/chat/chat-thread-list-item.react.js @@ -1,297 +1,328 @@ // @flow +import Icon from '@expo/vector-icons/FontAwesome.js'; import * as React from 'react'; import { Text, View } from 'react-native'; import type { ChatThreadItem } from 'lib/selectors/chat-selectors.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; +import { threadTypeIsThick } from 'lib/types/thread-types-enum.js'; import type { UserInfo } from 'lib/types/user-types.js'; import { shortAbsoluteDate } from 'lib/utils/date-utils.js'; import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js'; import ChatThreadListSeeMoreSidebars from './chat-thread-list-see-more-sidebars.react.js'; import ChatThreadListSidebar from './chat-thread-list-sidebar.react.js'; import MessagePreview from './message-preview.react.js'; import SwipeableThread from './swipeable-thread.react.js'; import ThreadAvatar from '../avatars/thread-avatar.react.js'; import Button from '../components/button.react.js'; import SingleLine from '../components/single-line.react.js'; import ThreadAncestorsLabel from '../components/thread-ancestors-label.react.js'; import UnreadDot from '../components/unread-dot.react.js'; import { useColors, useStyles } from '../themes/colors.js'; type Props = { +data: ChatThreadItem, +onPressItem: ( threadInfo: ThreadInfo, pendingPersonalThreadUserInfo?: UserInfo, ) => void, +onPressSeeMoreSidebars: (threadInfo: ThreadInfo) => void, +onSwipeableWillOpen: (threadInfo: ThreadInfo) => void, +currentlyOpenedSwipeableId: string, }; function ChatThreadListItem({ data, onPressItem, onPressSeeMoreSidebars, onSwipeableWillOpen, currentlyOpenedSwipeableId, }: Props): React.Node { const styles = useStyles(unboundStyles); const colors = useColors(); const lastMessage = React.useMemo(() => { const mostRecentMessageInfo = data.mostRecentMessageInfo; if (!mostRecentMessageInfo) { return ( No messages ); } return ( ); }, [data.mostRecentMessageInfo, data.threadInfo, styles]); const numOfSidebarsWithExtendedArrow = data.sidebars.filter(sidebarItem => sidebarItem.type === 'sidebar').length - 1; const sidebars = React.useMemo( () => data.sidebars.map((sidebarItem, index) => { if (sidebarItem.type === 'sidebar') { const { type, ...sidebarInfo } = sidebarItem; return ( ); } else if (sidebarItem.type === 'seeMore') { return ( ); } else { return ; } }), [ currentlyOpenedSwipeableId, data.sidebars, data.threadInfo, numOfSidebarsWithExtendedArrow, onPressItem, onPressSeeMoreSidebars, onSwipeableWillOpen, styles.spacer, ], ); const onPress = React.useCallback(() => { onPressItem(data.threadInfo, data.pendingPersonalThreadUserInfo); }, [onPressItem, data.threadInfo, data.pendingPersonalThreadUserInfo]); const threadNameStyle = React.useMemo(() => { if (!data.threadInfo.currentUser.unread) { return styles.threadName; } return [styles.threadName, styles.unreadThreadName]; }, [ data.threadInfo.currentUser.unread, styles.threadName, styles.unreadThreadName, ]); const lastActivity = shortAbsoluteDate(data.lastUpdatedTime); const lastActivityStyle = React.useMemo(() => { if (!data.threadInfo.currentUser.unread) { return styles.lastActivity; } return [styles.lastActivity, styles.unreadLastActivity]; }, [ data.threadInfo.currentUser.unread, styles.lastActivity, styles.unreadLastActivity, ]); const resolvedThreadInfo = useResolvedThreadInfo(data.threadInfo); const unreadDot = React.useMemo( () => ( ), [data.threadInfo.currentUser.unread, styles.avatarContainer], ); const threadAvatar = React.useMemo( () => ( ), [data.threadInfo, styles.avatarContainer], ); + const isThick = threadTypeIsThick(data.threadInfo.type); + const iconStyle = data.threadInfo.currentUser.unread + ? styles.iconUnread + : styles.iconRead; + + const iconName = isThick ? 'lock' : 'server'; + const threadDetails = React.useMemo( () => ( - + + + + + + {resolvedThreadInfo.uiName} {lastMessage} {lastActivity} ), [ + iconStyle, data.threadInfo, + iconName, lastActivity, lastActivityStyle, lastMessage, resolvedThreadInfo.uiName, + styles.header, + styles.iconContainer, styles.row, styles.threadDetails, threadNameStyle, ], ); const swipeableThreadContent = React.useMemo( () => ( ), [ colors.listIosHighlightUnderlay, onPress, styles.container, styles.content, threadAvatar, threadDetails, unreadDot, ], ); const swipeableThread = React.useMemo( () => ( {swipeableThreadContent} ), [ currentlyOpenedSwipeableId, data.mostRecentNonLocalMessage, data.threadInfo, onSwipeableWillOpen, swipeableThreadContent, ], ); const chatThreadListItem = React.useMemo( () => ( <> {swipeableThread} {sidebars} ), [sidebars, swipeableThread], ); return chatThreadListItem; } const chatThreadListItemHeight = 70; const spacerHeight = 6; const unboundStyles = { container: { height: chatThreadListItemHeight, justifyContent: 'center', backgroundColor: 'listBackground', }, content: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, avatarContainer: { marginLeft: 6, marginBottom: 12, }, threadDetails: { paddingLeft: 12, paddingRight: 18, justifyContent: 'center', flex: 1, marginTop: 5, }, lastActivity: { color: 'listForegroundTertiaryLabel', fontSize: 14, marginLeft: 10, }, unreadLastActivity: { color: 'listForegroundLabel', fontWeight: 'bold', }, noMessages: { color: 'listForegroundTertiaryLabel', flex: 1, fontSize: 14, fontStyle: 'italic', }, row: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, threadName: { color: 'listForegroundSecondaryLabel', flex: 1, fontSize: 21, }, unreadThreadName: { color: 'listForegroundLabel', fontWeight: '500', }, spacer: { height: spacerHeight, }, + header: { + flexDirection: 'row', + alignItems: 'center', + }, + iconContainer: { + marginRight: 6, + }, + iconRead: { + color: 'listForegroundTertiaryLabel', + }, + iconUnread: { + color: 'listForegroundLabel', + }, }; export { ChatThreadListItem, chatThreadListItemHeight, spacerHeight }; diff --git a/native/components/thread-ancestors-label.react.js b/native/components/thread-ancestors-label.react.js index 5a03f56f5..a556e366f 100644 --- a/native/components/thread-ancestors-label.react.js +++ b/native/components/thread-ancestors-label.react.js @@ -1,79 +1,81 @@ // @flow import Icon from '@expo/vector-icons/FontAwesome5.js'; import * as React from 'react'; import { Text, View } from 'react-native'; import { useAncestorThreads } from 'lib/shared/ancestor-threads.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; +import { threadTypeIsThick } from 'lib/types/thread-types-enum.js'; import { useResolvedThreadInfos } from 'lib/utils/entity-helpers.js'; import { useColors, useStyles } from '../themes/colors.js'; type Props = { +threadInfo: ThreadInfo, }; function ThreadAncestorsLabel(props: Props): React.Node { const { threadInfo } = props; const { unread } = threadInfo.currentUser; const styles = useStyles(unboundStyles); const colors = useColors(); const ancestorThreads = useAncestorThreads(threadInfo); const resolvedAncestorThreads = useResolvedThreadInfos(ancestorThreads); const chevronIcon = React.useMemo( () => ( ), [colors.listForegroundTertiaryLabel], ); const ancestorPath = React.useMemo(() => { const path: Array = []; for (const thread of resolvedAncestorThreads) { path.push({thread.uiName}); path.push( ${thread.id}`} style={styles.chevron}> {chevronIcon} , ); } path.pop(); return path; }, [resolvedAncestorThreads, chevronIcon, styles.chevron]); const ancestorPathStyle = React.useMemo(() => { return unread ? [styles.pathText, styles.unread] : styles.pathText; }, [styles.pathText, styles.unread, unread]); - const threadAncestorsLabel = React.useMemo( - () => ( + const isThick = threadTypeIsThick(threadInfo.type); + + return React.useMemo(() => { + const label = isThick ? 'Local DM' : ancestorPath; + + return ( - {ancestorPath} + {label} - ), - [ancestorPath, ancestorPathStyle], - ); - - return threadAncestorsLabel; + ); + }, [ancestorPath, ancestorPathStyle, isThick]); } const unboundStyles = { pathText: { opacity: 0.8, fontSize: 12, color: 'listForegroundTertiaryLabel', }, unread: { color: 'listForegroundLabel', }, chevron: { paddingHorizontal: 3, }, }; export default ThreadAncestorsLabel; diff --git a/web/chat/chat-thread-list-item.react.js b/web/chat/chat-thread-list-item.react.js index 1dc0c9eb3..7a7309060 100644 --- a/web/chat/chat-thread-list-item.react.js +++ b/web/chat/chat-thread-list-item.react.js @@ -1,161 +1,178 @@ // @flow +import { + faServer as server, + faLock as lock, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import classNames from 'classnames'; import * as React from 'react'; import SWMansionIcon from 'lib/components/swmansion-icon.react.js'; import type { ChatThreadItem } from 'lib/selectors/chat-selectors.js'; import { useAncestorThreads } from 'lib/shared/ancestor-threads.js'; +import { threadTypeIsThick } from 'lib/types/thread-types-enum.js'; import { shortAbsoluteDate } from 'lib/utils/date-utils.js'; import { useResolvedThreadInfo, useResolvedThreadInfos, } from 'lib/utils/entity-helpers.js'; import ChatThreadListItemMenu from './chat-thread-list-item-menu.react.js'; import ChatThreadListSeeMoreSidebars from './chat-thread-list-see-more-sidebars.react.js'; import ChatThreadListSidebar from './chat-thread-list-sidebar.react.js'; import css from './chat-thread-list.css'; import MessagePreview from './message-preview.react.js'; import ThreadAvatar from '../avatars/thread-avatar.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { useOnClickThread, useThreadIsActive, } from '../selectors/thread-selectors.js'; type Props = { +item: ChatThreadItem, }; function ChatThreadListItem(props: Props): React.Node { const { item } = props; const { threadInfo, lastUpdatedTimeIncludingSidebars, mostRecentNonLocalMessage, mostRecentMessageInfo, } = item; const { id: threadID, currentUser } = threadInfo; const unresolvedAncestorThreads = useAncestorThreads(threadInfo); const ancestorThreads = useResolvedThreadInfos(unresolvedAncestorThreads); const lastActivity = shortAbsoluteDate(lastUpdatedTimeIncludingSidebars); const active = useThreadIsActive(threadID); const isCreateMode = useSelector( state => state.navInfo.chatMode === 'create', ); const onClick = useOnClickThread(item.threadInfo); const selectItemIfNotActiveCreation = React.useCallback( (event: SyntheticEvent) => { if (!isCreateMode || !active) { onClick(event); } }, [isCreateMode, active, onClick], ); const containerClassName = classNames({ [css.thread]: true, [css.activeThread]: active, }); const { unread } = currentUser; const titleClassName = classNames({ [css.title]: true, [css.unread]: unread, }); const lastActivityClassName = classNames({ [css.lastActivity]: true, [css.unread]: unread, [css.dark]: !unread, }); const breadCrumbsClassName = classNames(css.breadCrumbs, { [css.unread]: unread, }); let unreadDot; if (unread) { unreadDot =
; } const sidebars = item.sidebars.map((sidebarItem, index) => { if (sidebarItem.type === 'sidebar') { const { type, ...sidebarInfo } = sidebarItem; return ( 0} key={sidebarInfo.threadInfo.id} /> ); } else if (sidebarItem.type === 'seeMore') { return ( ); } else { return
; } }); const ancestorPath = ancestorThreads.map((thread, idx) => { const isNotLast = idx !== ancestorThreads.length - 1; const chevron = isNotLast && ( ); return ( {thread.uiName} {chevron} ); }); const { uiName } = useResolvedThreadInfo(threadInfo); + const isThick = threadTypeIsThick(threadInfo.type); + + const iconClass = unread ? css.iconUnread : css.iconRead; + const icon = isThick ? lock : server; + const breadCrumbs = isThick ? 'Local DM' : ancestorPath; + return ( <>
{unreadDot}
-

{ancestorPath}

+
+
+ +
+

{breadCrumbs}

+
{uiName}
{lastActivity}
{sidebars} ); } export default ChatThreadListItem; diff --git a/web/chat/chat-thread-list.css b/web/chat/chat-thread-list.css index 323c879b6..7c74999cf 100644 --- a/web/chat/chat-thread-list.css +++ b/web/chat/chat-thread-list.css @@ -1,286 +1,304 @@ .thread { display: flex; flex-direction: row; } .sidebarItem { display: flex; flex-direction: row; width: 100%; } .threadListSidebar { display: flex; flex-direction: row; height: 32px; padding-right: 8px; position: relative; cursor: pointer; } .threadListSidebar > svg { position: absolute; top: -7px; left: 30px; } .activeThread, .threadListSidebar:hover { background: var(--thread-active-bg); } .activeThread :is(.title, .lastMessage, .lastMessage *) { color: var(--chat-thread-list-color-active); } .activeThread.thread:hover { background: var(--thread-active-bg); } .thread:hover { background: var(--thread-hover-bg); } div.title { flex: 1; font-size: var(--m-font-16); font-weight: var(--semi-bold); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--thread-color-read); line-height: var(--line-height-text); } .threadButton { flex: 1; cursor: pointer; overflow: hidden; padding-left: 12px; } .threadButton + div { display: flex; flex-direction: column; } .threadButtonSidebar { cursor: pointer; overflow: hidden; display: flex; align-items: center; padding-left: 12px; } p.breadCrumbs { display: flex; padding: 8px 0 2px 0; font-size: var(--xs-font-12); font-weight: var(--normal); color: var(--breadcrumb-color); } p.breadCrumbs.unread { color: var(--breadcrumb-color-unread); } span.breadCrumb { display: flex; align-items: center; white-space: nowrap; text-overflow: ellipsis; } span.breadCrumb svg { margin-left: 4px; margin-right: 4px; } div.colorContainer { display: flex; padding-top: 8px; } div.spacer { width: 42px; border-radius: 1.68px; } div.avatarContainer { height: 42px; display: flex; } div.lastActivity { font-size: var(--xxs-font-10); color: var(--fg); line-height: 1.5; padding-right: 16px; font-weight: var(--semi-bold); white-space: nowrap; flex-grow: 1; padding-bottom: 12px; align-items: flex-end; display: flex; } div.lastMessage { font-size: var(--s-font-14); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: var(--line-height-text); padding-bottom: 8px; } div.unread { color: var(--fg); font-weight: var(--semi-bold); } div.dark { color: var(--thread-color-read); padding-right: 16px; } .messagePreviewPrimary { color: var(--thread-color-read); } .messagePreviewSecondary { color: var(--thread-preview-secondary); } div.dotContainer { display: flex; align-items: center; justify-content: center; width: 16px; } div.unreadDot { height: 4px; width: 4px; background: var(--fg); border-radius: 15px; align-self: center; } div.italic { font-style: italic; } div.sidebarTitle { flex: 1; font-size: var(--s-font-14); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--thread-color-read); align-self: flex-start; } .threadListSidebar > div.dotContainer { width: 16px; } div.sidebarTitle.unread { color: var(--fg); } div.seeMoreButton { display: flex; align-items: center; padding-left: 22px; } div.seeMoreText { padding-left: 14px; } div.sidebar .menu > button svg { font-size: 16px; color: var(--thread-color-read); } div.sidebar .menu { opacity: 0; } div.sidebar:hover .menu { display: flex; align-self: flex-end; opacity: 1; } .menu { position: relative; display: flex; justify-content: flex-end; } .menu > button { background-color: transparent; color: var(--thread-color-read); border: none; cursor: pointer; display: flex; align-items: center; } .menu > button:focus { outline: none; } .menuContent { display: none; position: absolute; 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; } button.menuContent { border: none; cursor: pointer; padding: 10px; font-size: 16px; } button.menuContent:hover { background-color: #dddddd; } ul.list { margin: 5px 3px 10px 0; overflow: auto; } div.spacer { height: 6px; } div.emptyItemContainer { display: flex; flex-direction: column; align-items: center; } div.emptyItemText { padding: 16px; font-size: 16px; text-align: center; white-space: pre-wrap; color: var(--fg); } div.threadListContainer { display: flex; flex-direction: column; overflow: hidden; flex: 1; } div.createNewThread { display: flex; flex-direction: column; align-items: stretch; padding: 8px; } img.longArrow { height: 40px; width: 25px; position: absolute; left: 28.5px; top: -18px; } img.arrow { position: absolute; left: 28px; top: -10px; } .searchBarContainer { padding: 1rem; } + +.iconRead { + color: var(--breadcrumb-color); +} + +.iconUnread { + color: var(--breadcrumb-color-unread); +} + +.iconWrapper { + padding: 7px 6px 2px 0; +} + +.header { + display: flex; + flex-direction: row; + align-items: center; +}