diff --git a/web/chat/chat-message-list.css b/web/chat/chat-message-list.css index c3389303f..aeabc0856 100644 --- a/web/chat/chat-message-list.css +++ b/web/chat/chat-message-list.css @@ -1,251 +1,241 @@ div.outerMessageContainer { position: relative; height: calc(100vh - 128px); min-height: 0; display: flex; flex-direction: column; } div.messageContainer { height: 100%; overflow-y: auto; display: flex; flex-direction: column-reverse; } div.mirroredMessageContainer { flex-direction: column !important; transform: scaleY(-1); } div.mirroredMessageContainer > div { transform: scaleY(-1); } div.message { display: flex; flex-direction: column; flex-shrink: 0; } div.loading { text-align: center; padding: 12px; } div.conversationHeader { color: var(--chat-timestamp-color); font-size: var(--xs-font-12); padding: 6px 0; line-height: var(--line-height-text); text-align: center; } div.conversationHeader:last-child { padding-top: 6px; } div.messageTooltipActiveArea { position: absolute; display: flex; top: 0; bottom: 0; align-items: center; padding: 0 12px; } div.viewerMessageTooltipActiveArea { right: 100%; } div.nonViewerMessageActiveArea { left: 100%; } div.messageTooltipActiveArea > div + div { margin-left: 4px; } div.messageTooltipLinkIcon:hover { cursor: pointer; } div.textMessage { padding: 6px 12px; white-space: pre-wrap; word-wrap: break-word; width: 100%; box-sizing: border-box; } div.textMessageDefaultBackground { background-color: var(--text-message-default-background); } div.normalTextMessage { font-size: 16px; } div.emojiOnlyTextMessage { font-size: 32px; font-family: emoji; } span.authorName { color: #777777; font-size: 14px; -} -span.authorNamePositionAvatar { padding: 4px 56px; } -span.authorNamePositionNoAvatar { - padding: 4px 24px; -} div.darkTextMessage { color: white; } div.lightTextMessage { color: black; } div.content { display: flex; flex-shrink: 0; align-items: center; margin-bottom: 5px; box-sizing: border-box; width: 100%; } div.nonViewerContent { align-self: flex-start; justify-content: flex-start; padding-right: 8px; } div.viewerContent { align-self: flex-end; justify-content: flex-end; padding-right: 4px; } div.iconContainer { margin-right: 1px; } div.iconContainer > svg { height: 16px; } div.messageBoxContainer { position: relative; display: flex; max-width: calc(min(68%, 1000px)); -} -div.messageBoxContainerPositionAvatar { margin: 0 4px; } -div.messageBoxContainerPositionNoAvatar { - margin: 0 4px 0 12px; -} div.fixedWidthMessageBoxContainer { width: 68%; } div.messageBox { overflow: hidden; display: flex; flex-wrap: wrap; justify-content: space-between; flex-shrink: 0; max-width: 100%; } div.fixedWidthMessageBox { width: 100%; } div.failedSend { display: flex; justify-content: flex-end; flex-shrink: 0; margin-right: 45px; padding-bottom: 6px; } .deliveryFailed { text-transform: uppercase; font-size: 14px; padding: 0 3px; color: var(--fg); } .retryButtonText { text-transform: uppercase; font-size: 14px; } div.messageBox > div.imageGrid { display: grid; width: 100%; grid-template-columns: repeat(6, 1fr); grid-gap: 5px; } div.messageBox span.multimedia > span.multimediaImage { min-height: initial; min-width: initial; } div.messageBox span.multimedia > span.multimediaImage > img { max-height: 600px; } div.imageGrid > span.multimedia { grid-column-end: span 3; } div.imageGrid > span.multimedia:first-child { margin-top: 0; } div.imageGrid > span.multimedia > span.multimediaImage { flex: 1; } div.imageGrid > span.multimedia > span.multimediaImage:after { content: ''; display: block; padding-bottom: calc(min(600px, 100%)); } div.imageGrid > span.multimedia > span.multimediaImage > img { position: absolute; width: 100%; height: 100%; object-fit: cover; } div.imageGrid > span.multimedia:nth-last-child(n + 3):first-child, div.imageGrid > span.multimedia:nth-last-child(n + 3):first-child ~ * { grid-column-end: span 2; } div.imageGrid > span.multimedia:nth-last-child(n + 4):first-child, div.imageGrid > span.multimedia:nth-last-child(n + 4):first-child ~ * { grid-column-end: span 3; } div.imageGrid > span.multimedia:nth-last-child(n + 5):first-child, div.imageGrid > span.multimedia:nth-last-child(n + 5):first-child ~ * { grid-column-end: span 2; } div.sidebarMarginBottom { margin-bottom: 2px; } svg.inlineEngagementIcon { color: #666666; } div.avatarContainer { display: flex; height: 100%; align-items: flex-end; margin: 0 4px 0 12px; } div.avatarOffset { width: 40px; } .pinIconContainer { position: absolute; top: 1px; } .pinIconLeft { left: -20px; } .pinIconRight { right: -20px; } diff --git a/web/chat/chat-thread-composer.react.js b/web/chat/chat-thread-composer.react.js index 25fd3d28f..ef3043918 100644 --- a/web/chat/chat-thread-composer.react.js +++ b/web/chat/chat-thread-composer.react.js @@ -1,202 +1,191 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import { useDispatch } from 'react-redux'; import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import { useENSNames } from 'lib/hooks/ens-cache.js'; import { userSearchIndexForPotentialMembers } from 'lib/selectors/user-selectors.js'; import { getPotentialMemberItems } from 'lib/shared/search-utils.js'; import { threadIsPending } from 'lib/shared/thread-utils.js'; import type { AccountUserInfo, UserListItem } from 'lib/types/user-types.js'; import css from './chat-thread-composer.css'; import Button from '../components/button.react.js'; import Label from '../components/label.react.js'; import Search from '../components/search.react.js'; import UserAvatar from '../components/user-avatar.react.js'; import type { InputState } from '../input/input-state.js'; import { updateNavInfoActionType } from '../redux/action-types.js'; import { useSelector } from '../redux/redux-utils.js'; -import { shouldRenderAvatars } from '../utils/avatar-utils.js'; type Props = { +userInfoInputArray: $ReadOnlyArray, +otherUserInfos: { [id: string]: AccountUserInfo }, +threadID: string, +inputState: InputState, }; type ActiveThreadBehavior = | 'reset-active-thread-if-pending' | 'keep-active-thread'; function ChatThreadComposer(props: Props): React.Node { const { userInfoInputArray, otherUserInfos, threadID, inputState } = props; const [usernameInputText, setUsernameInputText] = React.useState(''); const dispatch = useDispatch(); const userSearchIndex = useSelector(userSearchIndexForPotentialMembers); const userInfoInputIDs = React.useMemo( () => userInfoInputArray.map(userInfo => userInfo.id), [userInfoInputArray], ); const userListItems = React.useMemo( () => getPotentialMemberItems( usernameInputText, otherUserInfos, userSearchIndex, userInfoInputIDs, ), [usernameInputText, otherUserInfos, userSearchIndex, userInfoInputIDs], ); const userListItemsWithENSNames = useENSNames(userListItems); const onSelectUserFromSearch = React.useCallback( (id: string) => { const selectedUserIDs = userInfoInputArray.map(user => user.id); dispatch({ type: updateNavInfoActionType, payload: { selectedUserList: [...selectedUserIDs, id], }, }); setUsernameInputText(''); }, [dispatch, userInfoInputArray], ); const onRemoveUserFromSelected = React.useCallback( (id: string) => { const selectedUserIDs = userInfoInputArray.map(user => user.id); if (!selectedUserIDs.includes(id)) { return; } dispatch({ type: updateNavInfoActionType, payload: { selectedUserList: selectedUserIDs.filter(userID => userID !== id), }, }); }, [dispatch, userInfoInputArray], ); - const usernameStyle = React.useMemo( - () => ({ - marginLeft: shouldRenderAvatars ? 8 : 0, - }), - [], - ); - const userSearchResultList = React.useMemo(() => { if ( !userListItemsWithENSNames.length || (!usernameInputText && userInfoInputArray.length) ) { return null; } const userItems = userListItemsWithENSNames.map( (userSearchResult: UserListItem) => (
  • ), ); return ; }, [ onSelectUserFromSearch, userInfoInputArray.length, userListItemsWithENSNames, usernameInputText, - usernameStyle, ]); const hideSearch = React.useCallback( (threadBehavior: ActiveThreadBehavior = 'keep-active-thread') => { dispatch({ type: updateNavInfoActionType, payload: { chatMode: 'view', activeChatThreadID: threadBehavior === 'keep-active-thread' || !threadIsPending(threadID) ? threadID : null, }, }); }, [dispatch, threadID], ); const onCloseSearch = React.useCallback(() => { hideSearch('reset-active-thread-if-pending'); }, [hideSearch]); const userInfoInputArrayWithENSNames = useENSNames(userInfoInputArray); const tagsList = React.useMemo(() => { if (!userInfoInputArrayWithENSNames?.length) { return null; } const labels = userInfoInputArrayWithENSNames.map(user => { return ( ); }); return
    {labels}
    ; }, [userInfoInputArrayWithENSNames, onRemoveUserFromSelected]); React.useEffect(() => { if (!inputState) { return undefined; } inputState.registerSendCallback(hideSearch); return () => inputState.unregisterSendCallback(hideSearch); }, [hideSearch, inputState]); const threadSearchContainerStyles = classNames(css.threadSearchContainer, { [css.fullHeight]: !userInfoInputArray.length, }); return (
    {tagsList} {userSearchResultList}
    ); } export default ChatThreadComposer; diff --git a/web/chat/chat-thread-list-item.react.js b/web/chat/chat-thread-list-item.react.js index acb6240ba..6e0a4b215 100644 --- a/web/chat/chat-thread-list-item.react.js +++ b/web/chat/chat-thread-list-item.react.js @@ -1,175 +1,161 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import type { ChatThreadItem } from 'lib/selectors/chat-selectors.js'; import { useAncestorThreads } from 'lib/shared/ancestor-threads.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 '../components/thread-avatar.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { useOnClickThread, useThreadIsActive, } from '../selectors/thread-selectors.js'; -import { shouldRenderAvatars } from '../utils/avatar-utils.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 { color } = item.threadInfo; - const colorSplotchStyle = React.useMemo( - () => ({ backgroundColor: `#${color}` }), - [color], - ); - 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 avatar = React.useMemo(() => { - if (!shouldRenderAvatars) { - return
    ; - } - return ; - }, [colorSplotchStyle, threadInfo]); - return ( <>
    -
    +
    {unreadDot}
    - {avatar} +

    {ancestorPath}

    {uiName}
    {lastActivity}
    {sidebars} ); } export default ChatThreadListItem; diff --git a/web/chat/chat-thread-list.css b/web/chat/chat-thread-list.css index 5e64a447e..c5b8a7406 100644 --- a/web/chat/chat-thread-list.css +++ b/web/chat/chat-thread-list.css @@ -1,289 +1,288 @@ .thread { display: flex; flex-direction: row; } .threadListSidebar { display: flex; flex-direction: row; height: 32px; padding-right: 8px; position: relative; cursor: pointer; } .threadListSidebar > svg { position: absolute; top: -7px; left: 30px; } .thread:first-child { padding-top: 6px; } .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, -div.colorSplotch { +div.spacer { width: 42px; border-radius: 1.68px; } -div.colorSplotchContainer { +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.sidebarLastActivity { white-space: nowrap; font-size: var(--xxs-font-10); line-height: var(--line-height-text); font-weight: var(--semi-bold); } svg.sidebarIcon { color: var(--thread-color-read); padding: 0 6px; font-size: 20px; } 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: auto; } 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; } diff --git a/web/chat/composed-message.react.js b/web/chat/composed-message.react.js index ca009381c..71941e232 100644 --- a/web/chat/composed-message.react.js +++ b/web/chat/composed-message.react.js @@ -1,248 +1,240 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import { Circle as CircleIcon, CheckCircle as CheckCircleIcon, XCircle as XCircleIcon, } from 'react-feather'; import { useStringForUser } from 'lib/hooks/ens-cache.js'; import { type ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js'; import { getMessageLabel } from 'lib/shared/edit-messages-utils.js'; import { assertComposableMessageType } from 'lib/types/message-types.js'; import { type ThreadInfo } from 'lib/types/thread-types.js'; import css from './chat-message-list.css'; import FailedSend from './failed-send.react.js'; import InlineEngagement from './inline-engagement.react.js'; import CommIcon from '../CommIcon.react.js'; import UserAvatar from '../components/user-avatar.react.js'; import { type InputState, InputStateContext } from '../input/input-state.js'; -import { shouldRenderAvatars } from '../utils/avatar-utils.js'; import { useMessageTooltip } from '../utils/tooltip-action-utils.js'; import { tooltipPositions } from '../utils/tooltip-utils.js'; const availableTooltipPositionsForViewerMessage = [ tooltipPositions.LEFT, tooltipPositions.LEFT_BOTTOM, tooltipPositions.LEFT_TOP, tooltipPositions.RIGHT, tooltipPositions.RIGHT_BOTTOM, tooltipPositions.RIGHT_TOP, tooltipPositions.BOTTOM, tooltipPositions.TOP, ]; const availableTooltipPositionsForNonViewerMessage = [ tooltipPositions.RIGHT, tooltipPositions.RIGHT_BOTTOM, tooltipPositions.RIGHT_TOP, tooltipPositions.LEFT, tooltipPositions.LEFT_BOTTOM, tooltipPositions.LEFT_TOP, tooltipPositions.BOTTOM, tooltipPositions.TOP, ]; type BaseProps = { +item: ChatMessageInfoItem, +threadInfo: ThreadInfo, +shouldDisplayPinIndicator: boolean, +sendFailed: boolean, +children: React.Node, +fixedWidth?: boolean, +borderRadius: number, }; type BaseConfig = React.Config; type Props = { ...BaseProps, // withInputState +inputState: ?InputState, +onMouseLeave: ?() => mixed, +onMouseEnter: (event: SyntheticEvent) => mixed, +containsInlineEngagement: boolean, +stringForUser: ?string, }; class ComposedMessage extends React.PureComponent { static defaultProps: { +borderRadius: number } = { borderRadius: 8, }; render(): React.Node { assertComposableMessageType(this.props.item.messageInfo.type); const { borderRadius, item, threadInfo, shouldDisplayPinIndicator } = this.props; const { hasBeenEdited, isPinned } = item; const { id, creator } = item.messageInfo; const threadColor = threadInfo.color; const { isViewer } = creator; const contentClassName = classNames({ [css.content]: true, [css.viewerContent]: isViewer, [css.nonViewerContent]: !isViewer, }); const messageBoxContainerClassName = classNames({ [css.messageBoxContainer]: true, [css.fixedWidthMessageBoxContainer]: this.props.fixedWidth, - [css.messageBoxContainerPositionAvatar]: shouldRenderAvatars, - [css.messageBoxContainerPositionNoAvatar]: !shouldRenderAvatars, }); const messageBoxClassName = classNames({ [css.messageBox]: true, [css.fixedWidthMessageBox]: this.props.fixedWidth, }); const messageBoxStyle = { borderTopRightRadius: isViewer && !item.startsCluster ? 0 : borderRadius, borderBottomRightRadius: isViewer && !item.endsCluster ? 0 : borderRadius, borderTopLeftRadius: !isViewer && !item.startsCluster ? 0 : borderRadius, borderBottomLeftRadius: !isViewer && !item.endsCluster ? 0 : borderRadius, }; let authorName = null; const { stringForUser } = this.props; - const authorNameClassName = classNames({ - [css.authorName]: true, - [css.authorNamePositionAvatar]: shouldRenderAvatars, - [css.authorNamePositionNoAvatar]: !shouldRenderAvatars, - }); if (stringForUser) { - authorName = {stringForUser}; + authorName = {stringForUser}; } let deliveryIcon = null; let failedSendInfo = null; if (isViewer) { let deliveryIconSpan; let deliveryIconColor = threadColor; if (id !== null && id !== undefined) { deliveryIconSpan = ; } else if (this.props.sendFailed) { deliveryIconSpan = ; deliveryIconColor = 'FF0000'; failedSendInfo = ; } else { deliveryIconSpan = ; } deliveryIcon = (
    {deliveryIconSpan}
    ); } let inlineEngagement = null; const label = getMessageLabel(hasBeenEdited, threadInfo); if ( (this.props.containsInlineEngagement && item.threadCreatedFromMessage) || Object.keys(item.reactions).length > 0 || label ) { const positioning = isViewer ? 'right' : 'left'; inlineEngagement = (
    ); } let avatar; - if (!isViewer && item.endsCluster && shouldRenderAvatars) { + if (!isViewer && item.endsCluster) { avatar = (
    ); - } else if (!isViewer && shouldRenderAvatars) { + } else if (!isViewer) { avatar =
    ; } const pinIconPositioning = isViewer ? 'left' : 'right'; const pinIconName = pinIconPositioning === 'left' ? 'pin-mirror' : 'pin'; const pinIconContainerClassName = classNames({ [css.pinIconContainer]: true, [css.pinIconLeft]: pinIconPositioning === 'left', [css.pinIconRight]: pinIconPositioning === 'right', }); let pinIcon; if (isPinned && shouldDisplayPinIndicator) { pinIcon = (
    ); } return ( {authorName}
    {avatar}
    {pinIcon}
    {this.props.children}
    {deliveryIcon}
    {failedSendInfo} {inlineEngagement}
    ); } } type ConnectedConfig = React.Config< BaseProps, typeof ComposedMessage.defaultProps, >; const ConnectedComposedMessage: React.ComponentType = React.memo(function ConnectedComposedMessage(props) { const { item, threadInfo } = props; const inputState = React.useContext(InputStateContext); const { creator } = props.item.messageInfo; const { isViewer } = creator; const availablePositions = isViewer ? availableTooltipPositionsForViewerMessage : availableTooltipPositionsForNonViewerMessage; const containsInlineEngagement = !!item.threadCreatedFromMessage; const { onMouseLeave, onMouseEnter } = useMessageTooltip({ item, threadInfo, availablePositions, }); const shouldShowUsername = !isViewer && item.startsCluster; const stringForUser = useStringForUser(shouldShowUsername ? creator : null); return ( ); }); export default ConnectedComposedMessage; diff --git a/web/chat/inline-engagement.css b/web/chat/inline-engagement.css index 0a7931093..34961a756 100644 --- a/web/chat/inline-engagement.css +++ b/web/chat/inline-engagement.css @@ -1,94 +1,89 @@ div.inlineEngagementContainer { display: flex; flex-direction: row; align-items: center; } div.centerContainer { justify-content: center; } div.leftContainer { justify-content: flex-start; position: relative; top: -10px; left: 44px; margin-right: 44px; } div.rightContainer { justify-content: flex-end; position: relative; top: -10px; right: 30px; margin-left: 31px; } -div.leftContainerNoAvatar { - left: 12px; - margin-right: 12px; -} - a.threadsContainer, a.threadsSplitContainer, a.reactionsContainer, a.reactionsSplitContainer { background: var(--inline-engagement-bg); color: var(--inline-engagement-color); font-size: var(--s-font-14); line-height: var(--line-height-text); transition: background 0.2s ease-in-out; padding: 8px; gap: 4px; flex-direction: row; display: flex; align-items: center; } a.threadsContainer, a.reactionsContainer { border-radius: 16px; } a.threadsSplitContainer { border-radius: 16px 0 0 16px; } a.reactionsSplitContainer { border-radius: 0 16px 16px 0; } a.threadsContainer:hover, a.threadsSplitContainer:hover, a.reactionsContainer:hover, a.reactionsSplitContainer:hover { background: var(--inline-engagement-bg-hover); } div.unread { font-weight: bold; } svg.inlineEngagementIcon { color: #666666; } div.messageLabel { display: flex; flex-shrink: 0; } div.messageLabel > span { font-size: 12px; padding: 0 3px; color: var(--message-label-color); } div.messageLabelLeft { margin-left: 8px; margin-right: 4px; } div.messageLabelRight { margin-right: 12px; margin-left: 4px; } div.onlyMessageLabel { margin-top: 8px; } diff --git a/web/chat/inline-engagement.react.js b/web/chat/inline-engagement.react.js index ab887012b..9f20ebb58 100644 --- a/web/chat/inline-engagement.react.js +++ b/web/chat/inline-engagement.react.js @@ -1,141 +1,138 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import useInlineEngagementText from 'lib/hooks/inline-engagement-text.react.js'; import type { ReactionInfo } from 'lib/selectors/chat-selectors.js'; import { stringForReactionList } from 'lib/shared/reaction-utils.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import css from './inline-engagement.css'; import CommIcon from '../CommIcon.react.js'; import MessageReactionsModal from '../modals/chat/message-reactions-modal.react.js'; import { useOnClickThread } from '../selectors/thread-selectors.js'; -import { shouldRenderAvatars } from '../utils/avatar-utils.js'; type Props = { +threadInfo: ?ThreadInfo, +reactions?: ReactionInfo, +positioning: 'left' | 'center' | 'right', +label?: ?string, }; function InlineEngagement(props: Props): React.Node { const { threadInfo, reactions, positioning, label } = props; const { pushModal, popModal } = useModalContext(); const repliesText = useInlineEngagementText(threadInfo); const containerClasses = classNames([ css.inlineEngagementContainer, { [css.leftContainer]: positioning === 'left', [css.centerContainer]: positioning === 'center', [css.rightContainer]: positioning === 'right', - [css.leftContainerNoAvatar]: - positioning === 'left' && !shouldRenderAvatars, }, ]); const reactionsExist = reactions && Object.keys(reactions).length > 0; const threadsContainerClasses = classNames({ [css.threadsContainer]: threadInfo && !reactionsExist, [css.threadsSplitContainer]: threadInfo && reactionsExist, }); const reactionsContainerClasses = classNames({ [css.reactionsContainer]: reactionsExist && !threadInfo, [css.reactionsSplitContainer]: reactionsExist && threadInfo, }); const onClickThreadInner = useOnClickThread(threadInfo); const onClickThread = React.useCallback( (event: SyntheticEvent) => { popModal(); onClickThreadInner(event); }, [popModal, onClickThreadInner], ); const sidebarItem = React.useMemo(() => { if (!threadInfo || !repliesText) { return null; } return ( {repliesText} ); }, [threadInfo, repliesText, onClickThread, threadsContainerClasses]); const onClickReactions = React.useCallback( (event: SyntheticEvent) => { event.preventDefault(); if (!reactions) { return; } pushModal( , ); }, [popModal, pushModal, reactions], ); const reactionsList = React.useMemo(() => { if (!reactions || Object.keys(reactions).length === 0) { return null; } const reactionText = stringForReactionList(reactions); return ( {reactionText} ); }, [reactions, onClickReactions, reactionsContainerClasses]); const isLeft = positioning === 'left'; const labelClasses = classNames({ [css.messageLabel]: true, [css.messageLabelLeft]: isLeft, [css.messageLabelRight]: !isLeft, [css.onlyMessageLabel]: !sidebarItem && !reactionsList, }); const messageLabel = React.useMemo(() => { if (!label) { return null; } return (
    {label}
    ); }, [label, labelClasses]); let body; if (isLeft) { body = ( <> {messageLabel} {sidebarItem} {reactionsList} ); } else { body = ( <> {sidebarItem} {reactionsList} {messageLabel} ); } return
    {body}
    ; } export default InlineEngagement; diff --git a/web/chat/thread-top-bar.css b/web/chat/thread-top-bar.css index 66a1ef5f0..8a5b2935d 100644 --- a/web/chat/thread-top-bar.css +++ b/web/chat/thread-top-bar.css @@ -1,64 +1,57 @@ div.topBarContainer { display: flex; background-color: var(--bg); align-items: center; justify-content: space-between; padding: 16px; color: var(--thread-top-bar-color); border-bottom: 1px solid var(--border); } div.topBarThreadInfo { height: 24px; display: flex; align-items: center; column-gap: 8px; overflow: hidden; } -div.threadColorSquare { - width: 24px; - height: 24px; - border-radius: 4px; - flex: 0 0 auto; -} - .threadTitle { font-size: var(--m-font-16); font-weight: var(--bold); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } button.topBarMenu { background-color: transparent; border: none; cursor: pointer; color: var(--thread-top-bar-menu-color); } .pinnedCountBanner { background-color: var(--pinned-count-banner-color); height: 40px; text-align: center; display: flex; align-items: center; justify-content: center; } a.pinnedCountText { color: var(--pinned-count-text-color); font-size: var(--xs-font-12); display: inline-flex; align-items: center; } a.pinnedCountText:hover { cursor: pointer; text-decoration: underline; } .chevronRight { vertical-align: middle; } diff --git a/web/chat/thread-top-bar.react.js b/web/chat/thread-top-bar.react.js index 6232bda46..81269a12d 100644 --- a/web/chat/thread-top-bar.react.js +++ b/web/chat/thread-top-bar.react.js @@ -1,101 +1,82 @@ // @flow import * as React from 'react'; import { ChevronRight } from 'react-feather'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { threadIsPending } from 'lib/shared/thread-utils.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js'; import ThreadMenu from './thread-menu.react.js'; import css from './thread-top-bar.css'; import ThreadAvatar from '../components/thread-avatar.react.js'; import { InputStateContext } from '../input/input-state.js'; import MessageResultsModal from '../modals/chat/message-results-modal.react.js'; -import { shouldRenderAvatars } from '../utils/avatar-utils.js'; type ThreadTopBarProps = { +threadInfo: ThreadInfo, }; function ThreadTopBar(props: ThreadTopBarProps): React.Node { const { threadInfo } = props; const { pushModal } = useModalContext(); - const threadBackgroundColorStyle = React.useMemo( - () => ({ - background: `#${threadInfo.color}`, - }), - [threadInfo.color], - ); let threadMenu = null; if (!threadIsPending(threadInfo.id)) { threadMenu = ; } // To allow the pinned messages modal to be re-used by the message search // modal, it will be useful to make the modal accept a prop that defines it's // name, instead of setting it directly in the modal. const bannerText = React.useMemo(() => { if (!threadInfo.pinnedCount || threadInfo.pinnedCount === 0) { return ''; } const messageNoun = threadInfo.pinnedCount === 1 ? 'message' : 'messages'; return `${threadInfo.pinnedCount} pinned ${messageNoun}`; }, [threadInfo.pinnedCount]); const inputState = React.useContext(InputStateContext); const pushThreadPinsModal = React.useCallback(() => { pushModal( , ); }, [pushModal, inputState, threadInfo, bannerText]); const pinnedCountBanner = React.useMemo(() => { if (!bannerText) { return null; } return ( ); }, [bannerText, pushThreadPinsModal]); const { uiName } = useResolvedThreadInfo(threadInfo); - const avatar = React.useMemo(() => { - if (!shouldRenderAvatars) { - return ( -
    - ); - } - return ; - }, [threadBackgroundColorStyle, threadInfo]); - return ( <>
    - {avatar} +
    {uiName}
    {threadMenu}
    {pinnedCountBanner} ); } export default ThreadTopBar; diff --git a/web/chat/typeahead-tooltip.css b/web/chat/typeahead-tooltip.css index e935cf1f8..7f225ceba 100644 --- a/web/chat/typeahead-tooltip.css +++ b/web/chat/typeahead-tooltip.css @@ -1,75 +1,71 @@ .suggestionsContainer { position: fixed; box-sizing: border-box; display: flex; flex-direction: column; align-items: flex-start; width: 296px; max-height: 268px; padding: 8px; background: var(--typeahead-overlay-dark); border: 1px solid var(--typeahead-overlay-light); box-shadow: 0px 1px 2px var(--typeahead-overlay-shadow-primary), 0px 4px 12px var(--typeahead-overlay-shadow-secondary); border-radius: 8px; overflow-y: scroll; overflow-x: hidden; } .suggestion { box-sizing: border-box; display: flex; flex-direction: row; align-items: center; justify-content: left; padding: 8px; width: 280px; height: 40px; background: var(--typeahead-overlay-dark); border: 0; border-radius: 4px; color: var(--typeahead-overlay-text); font-family: var(--font-stack); font-size: var(--m-font-16); font-weight: var(--normal); line-height: var(--line-height-text); } .suggestion span { display: inline-block; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; } .suggestionHover { background-color: var(--typeahead-overlay-light); } .notVisible { opacity: 0; } .visible { opacity: 1; transition: opacity 100ms ease-in; } span.username { margin-left: 8px; } - -span.usernameNoAvatar { - margin-left: 0; -} diff --git a/web/components/avatar.react.js b/web/components/avatar.react.js index 2dc8537f5..14eb0a685 100644 --- a/web/components/avatar.react.js +++ b/web/components/avatar.react.js @@ -1,70 +1,69 @@ // @flow import classnames from 'classnames'; import * as React from 'react'; import type { ResolvedClientAvatar } from 'lib/types/avatar-types'; import css from './avatar.css'; -import { shouldRenderAvatars } from '../utils/avatar-utils.js'; type Props = { +avatarInfo: ResolvedClientAvatar, +size: 'micro' | 'small' | 'large' | 'profile', }; function Avatar(props: Props): React.Node { const { avatarInfo, size } = props; const containerSizeClassName = classnames({ [css.imgContainer]: avatarInfo.type === 'image', [css.micro]: size === 'micro', [css.small]: size === 'small', [css.large]: size === 'large', [css.profile]: size === 'profile', }); const emojiSizeClassName = classnames({ [css.emojiContainer]: true, [css.emojiMicro]: size === 'micro', [css.emojiSmall]: size === 'small', [css.emojiLarge]: size === 'large', [css.emojiProfile]: size === 'profile', }); const emojiContainerColorStyle = React.useMemo(() => { if (avatarInfo.type === 'emoji') { return { backgroundColor: `#${avatarInfo.color}` }; } return undefined; }, [avatarInfo.color, avatarInfo.type]); const avatar = React.useMemo(() => { if (avatarInfo.type === 'image') { return ( image avatar ); } return (
    {avatarInfo.emoji}
    ); }, [ avatarInfo.emoji, avatarInfo.type, avatarInfo.uri, containerSizeClassName, emojiContainerColorStyle, emojiSizeClassName, ]); - return shouldRenderAvatars ? avatar : null; + return avatar; } export default Avatar; diff --git a/web/modals/chat/message-reactions-modal.react.js b/web/modals/chat/message-reactions-modal.react.js index dfec1ed24..5d63d1f46 100644 --- a/web/modals/chat/message-reactions-modal.react.js +++ b/web/modals/chat/message-reactions-modal.react.js @@ -1,53 +1,43 @@ // @flow import * as React from 'react'; import type { ReactionInfo } from 'lib/selectors/chat-selectors.js'; import { useMessageReactionsList } from 'lib/shared/reaction-utils.js'; import css from './message-reactions-modal.css'; import UserAvatar from '../../components/user-avatar.react.js'; -import { shouldRenderAvatars } from '../../utils/avatar-utils.js'; import Modal from '../modal.react.js'; type Props = { +onClose: () => void, +reactions: ReactionInfo, }; function MessageReactionsModal(props: Props): React.Node { const { onClose, reactions } = props; const messageReactionsList = useMessageReactionsList(reactions); - const usernameStyle = React.useMemo( - () => ({ - marginLeft: shouldRenderAvatars ? 8 : 0, - }), - [], - ); - const reactionsList = React.useMemo( () => messageReactionsList.map(messageReactionUser => (
    -
    - {messageReactionUser.username} -
    +
    {messageReactionUser.username}
    {messageReactionUser.reaction}
    )), - [messageReactionsList, usernameStyle], + [messageReactionsList], ); return (
    {reactionsList}
    ); } export default MessageReactionsModal; diff --git a/web/modals/components/add-members-item.react.js b/web/modals/components/add-members-item.react.js index 1055677fb..e9d80ed70 100644 --- a/web/modals/components/add-members-item.react.js +++ b/web/modals/components/add-members-item.react.js @@ -1,65 +1,55 @@ // @flow import * as React from 'react'; import type { UserListItem } from 'lib/types/user-types.js'; import css from './add-members.css'; import Button from '../../components/button.react.js'; import UserAvatar from '../../components/user-avatar.react.js'; -import { shouldRenderAvatars } from '../../utils/avatar-utils.js'; type AddMembersItemProps = { +userInfo: UserListItem, +onClick: (userID: string) => void, +userAdded: boolean, }; function AddMemberItem(props: AddMembersItemProps): React.Node { const { userInfo, onClick, userAdded = false } = props; const canBeAdded = !userInfo.alertText; const onClickCallback = React.useCallback(() => { if (!canBeAdded) { return; } onClick(userInfo.id); }, [canBeAdded, onClick, userInfo.id]); const action = React.useMemo(() => { if (!canBeAdded) { return userInfo.alertTitle; } if (userAdded) { return Remove; } else { return 'Add'; } }, [canBeAdded, userAdded, userInfo.alertTitle]); - const usernameStyle = React.useMemo( - () => ({ - marginLeft: shouldRenderAvatars ? 8 : 0, - }), - [], - ); - return ( ); } export default AddMemberItem; diff --git a/web/modals/threads/sidebars/sidebar.react.js b/web/modals/threads/sidebars/sidebar.react.js index 4dcabfd84..380f62945 100644 --- a/web/modals/threads/sidebars/sidebar.react.js +++ b/web/modals/threads/sidebars/sidebar.react.js @@ -1,102 +1,101 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import type { ChatThreadItem } from 'lib/selectors/chat-selectors.js'; import { useMessagePreview } from 'lib/shared/message-utils.js'; import { shortAbsoluteDate } from 'lib/utils/date-utils.js'; import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js'; import css from './sidebars-modal.css'; import Button from '../../../components/button.react.js'; import ThreadAvatar from '../../../components/thread-avatar.react.js'; import { getDefaultTextMessageRules } from '../../../markdown/rules.react.js'; import { useOnClickThread } from '../../../selectors/thread-selectors.js'; -import { shouldRenderAvatars } from '../../../utils/avatar-utils.js'; type Props = { +sidebar: ChatThreadItem, +isLastItem?: boolean, }; function Sidebar(props: Props): React.Node { const { sidebar, isLastItem } = props; const { threadInfo, lastUpdatedTime, mostRecentMessageInfo } = sidebar; const { unread } = threadInfo.currentUser; const { popModal } = useModalContext(); const navigateToThread = useOnClickThread(threadInfo); const onClickThread = React.useCallback( event => { popModal(); navigateToThread(event); }, [popModal, navigateToThread], ); const sidebarInfoClassName = classNames({ [css.sidebarInfo]: true, [css.unread]: unread, }); - const previewTextClassName = classNames({ - [css.longTextEllipsis]: true, - [css.avatarOffset]: shouldRenderAvatars, - }); + const previewTextClassName = classNames([ + css.longTextEllipsis, + css.avatarOffset, + ]); const lastActivity = React.useMemo( () => shortAbsoluteDate(lastUpdatedTime), [lastUpdatedTime], ); const messagePreviewResult = useMessagePreview( mostRecentMessageInfo, threadInfo, getDefaultTextMessageRules().simpleMarkdownRules, ); const lastMessage = React.useMemo(() => { if (!messagePreviewResult) { return
    No messages
    ; } const { message, username } = messagePreviewResult; const previewText = username ? `${username.text}: ${message.text}` : message.text; return ( <>
    {previewText}
    {lastActivity}
    ); }, [lastActivity, messagePreviewResult, previewTextClassName]); const { uiName } = useResolvedThreadInfo(threadInfo); return ( ); } export default Sidebar; diff --git a/web/modals/threads/subchannels/subchannel.react.js b/web/modals/threads/subchannels/subchannel.react.js index 82f420e96..8ecd3aa5d 100644 --- a/web/modals/threads/subchannels/subchannel.react.js +++ b/web/modals/threads/subchannels/subchannel.react.js @@ -1,98 +1,89 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import { useModalContext } from 'lib/components/modal-provider.react.js'; -import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import { type ChatThreadItem } from 'lib/selectors/chat-selectors.js'; import { useMessagePreview } from 'lib/shared/message-utils.js'; import { shortAbsoluteDate } from 'lib/utils/date-utils.js'; import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js'; import css from './subchannels-modal.css'; import Button from '../../../components/button.react.js'; import ThreadAvatar from '../../../components/thread-avatar.react.js'; import { getDefaultTextMessageRules } from '../../../markdown/rules.react.js'; import { useOnClickThread } from '../../../selectors/thread-selectors.js'; -import { shouldRenderAvatars } from '../../../utils/avatar-utils.js'; type Props = { +chatThreadItem: ChatThreadItem, }; function Subchannel(props: Props): React.Node { const { chatThreadItem } = props; const { threadInfo, mostRecentMessageInfo, lastUpdatedTimeIncludingSidebars, } = chatThreadItem; const { unread } = threadInfo.currentUser; const subchannelTitleClassName = classNames({ [css.subchannelInfo]: true, [css.unread]: unread, }); const { popModal } = useModalContext(); const navigateToThread = useOnClickThread(threadInfo); const onClickThread = React.useCallback( event => { popModal(); navigateToThread(event); }, [popModal, navigateToThread], ); const lastActivity = React.useMemo( () => shortAbsoluteDate(lastUpdatedTimeIncludingSidebars), [lastUpdatedTimeIncludingSidebars], ); const messagePreviewResult = useMessagePreview( mostRecentMessageInfo, threadInfo, getDefaultTextMessageRules().simpleMarkdownRules, ); const lastMessage = React.useMemo(() => { if (!messagePreviewResult) { return
    No messages
    ; } const { message, username } = messagePreviewResult; const previewText = username ? `${username.text}: ${message.text}` : message.text; return ( <>
    {previewText}
    {lastActivity}
    ); }, [lastActivity, messagePreviewResult]); const { uiName } = useResolvedThreadInfo(threadInfo); - const avatar = React.useMemo(() => { - if (!shouldRenderAvatars) { - return ; - } - return ; - }, [threadInfo]); - return ( ); } export default Subchannel; diff --git a/web/modals/threads/thread-picker-modal.css b/web/modals/threads/thread-picker-modal.css index 073b8d5b4..dc5e9c714 100644 --- a/web/modals/threads/thread-picker-modal.css +++ b/web/modals/threads/thread-picker-modal.css @@ -1,45 +1,39 @@ div.container { display: flex; flex-direction: column; overflow: hidden; margin: 16px; } div.contentContainer { overflow: scroll; height: 448px; } div.threadPickerOptionContainer { display: flex; } .threadPickerOptionButton { flex: 1; justify-content: left; padding: 12px 16px; font-size: var(--m-font-16); } .threadPickerOptionButton:hover { background-color: var(--thread-hover-bg); border-radius: 8px; } -div.threadSplotch { - min-width: 40px; - height: 40px; - border-radius: 10px; -} - div.threadNameText { color: var(--shades-white-100); margin-left: 16px; } div.noResultsText { text-align: center; color: var(--shades-white-100); margin-top: 24px; font-weight: 500; } diff --git a/web/modals/threads/thread-picker-modal.react.js b/web/modals/threads/thread-picker-modal.react.js index bf1c47675..4f658649e 100644 --- a/web/modals/threads/thread-picker-modal.react.js +++ b/web/modals/threads/thread-picker-modal.react.js @@ -1,155 +1,140 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { createSelector } from 'reselect'; import { useGlobalThreadSearchIndex } from 'lib/selectors/nav-selectors.js'; import { onScreenEntryEditableThreadInfos } from 'lib/selectors/thread-selectors.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js'; import css from './thread-picker-modal.css'; import Button from '../../components/button.react.js'; import Search from '../../components/search.react.js'; import ThreadAvatar from '../../components/thread-avatar.react.js'; import { useSelector } from '../../redux/redux-utils.js'; -import { shouldRenderAvatars } from '../../utils/avatar-utils.js'; import Modal, { type ModalOverridableProps } from '../modal.react.js'; type OptionProps = { +threadInfo: ThreadInfo, +createNewEntry: (threadID: string) => void, +onCloseModal: () => void, }; function ThreadPickerOption(props: OptionProps) { const { threadInfo, createNewEntry, onCloseModal } = props; const onClickThreadOption = React.useCallback(() => { createNewEntry(threadInfo.id); onCloseModal(); }, [threadInfo.id, createNewEntry, onCloseModal]); - const splotchColorStyle = React.useMemo( - () => ({ - backgroundColor: `#${threadInfo.color}`, - }), - [threadInfo.color], - ); - const { uiName } = useResolvedThreadInfo(threadInfo); - const avatar = React.useMemo(() => { - if (!shouldRenderAvatars) { - return
    ; - } - return ; - }, [splotchColorStyle, threadInfo]); - return (
    ); } type Props = { ...ModalOverridableProps, +createNewEntry: (threadID: string) => void, }; function ThreadPickerModal(props: Props): React.Node { const { createNewEntry, ...modalProps } = props; const onScreenThreadInfos = useSelector(onScreenEntryEditableThreadInfos); const searchIndex = useGlobalThreadSearchIndex(); invariant( onScreenThreadInfos.length > 0, "ThreadPicker can't be open when onScreenThreadInfos is empty", ); const [searchText, setSearchText] = React.useState(''); const [searchResults, setSearchResults] = React.useState>( new Set(), ); const searchRef = React.useRef(); React.useEffect(() => { searchRef.current?.focus(); }, []); const onChangeSearchText = React.useCallback( (text: string) => { const results = searchIndex.getSearchResults(text); setSearchText(text); setSearchResults(new Set(results)); }, [searchIndex], ); const listDataSelector = createSelector( state => state.onScreenThreadInfos, state => state.searchText, state => state.searchResults, ( threadInfos: $ReadOnlyArray, text: string, results: Set, ) => text ? threadInfos.filter(threadInfo => results.has(threadInfo.id)) : [...threadInfos], ); const threads = useSelector(() => listDataSelector({ onScreenThreadInfos, searchText, searchResults, }), ); const threadPickerContent = React.useMemo(() => { const options = threads.map(threadInfo => ( )); if (options.length === 0 && searchText.length > 0) { return (
    No results for {searchText}
    ); } else { return options; } }, [threads, createNewEntry, modalProps.onClose, searchText]); return (
    {threadPickerContent}
    ); } export default ThreadPickerModal; diff --git a/web/navigation-panels/nav-state-info-bar.css b/web/navigation-panels/nav-state-info-bar.css index 363379886..04bbbc207 100644 --- a/web/navigation-panels/nav-state-info-bar.css +++ b/web/navigation-panels/nav-state-info-bar.css @@ -1,32 +1,24 @@ div.topBarContainer { display: flex; background-color: var(--bg); align-items: center; color: var(--thread-top-bar-color); height: 56px; overflow: hidden; } -div.threadColorSquare { - width: 24px; - height: 24px; - border-radius: 4px; - flex: 0 0 auto; - margin: 0 12px 0 16px; -} - div.avatarContainer { margin: 0 12px 0 16px; } div.hide { height: 0px; opacity: 0; transition: height 200ms ease-in-out, opacity 200ms ease-in-out; } div.show { height: 56px; opacity: 1; transition: height 200ms ease-in-out, opacity 200ms ease-in-out; } diff --git a/web/navigation-panels/nav-state-info-bar.react.js b/web/navigation-panels/nav-state-info-bar.react.js index 282a384c7..cbab4c660 100644 --- a/web/navigation-panels/nav-state-info-bar.react.js +++ b/web/navigation-panels/nav-state-info-bar.react.js @@ -1,89 +1,67 @@ // @flow import classnames from 'classnames'; import * as React from 'react'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import ThreadAncestors from './chat-thread-ancestors.react.js'; import css from './nav-state-info-bar.css'; import ThreadAvatar from '../components/thread-avatar.react.js'; -import { shouldRenderAvatars } from '../utils/avatar-utils.js'; type NavStateInfoBarProps = { +threadInfo: ThreadInfo, }; function NavStateInfoBar(props: NavStateInfoBarProps): React.Node { const { threadInfo } = props; - const threadBackgroundColorStyle = React.useMemo( - () => ({ - background: `#${threadInfo.color}`, - }), - [threadInfo.color], - ); - - const avatar = React.useMemo(() => { - if (!shouldRenderAvatars) { - return ( -
    - ); - } - return ( + return ( + <>
    - ); - }, [threadBackgroundColorStyle, threadInfo]); - - return ( - <> - {avatar} ); } type PossiblyEmptyNavStateInfoBarProps = { +threadInfoInput: ?ThreadInfo, }; function PossiblyEmptyNavStateInfoBar( props: PossiblyEmptyNavStateInfoBarProps, ): React.Node { const { threadInfoInput } = props; const [threadInfo, setThreadInfo] = React.useState(threadInfoInput); React.useEffect(() => { if (threadInfoInput !== threadInfo) { if (threadInfoInput) { setThreadInfo(threadInfoInput); } else { const timeout = setTimeout(() => { setThreadInfo(null); }, 200); return () => clearTimeout(timeout); } } return undefined; }, [threadInfoInput, threadInfo]); const content = React.useMemo(() => { if (threadInfo) { return ; } else { return null; } }, [threadInfo]); const classes = classnames(css.topBarContainer, { [css.hide]: !threadInfoInput, [css.show]: threadInfoInput, }); return
    {content}
    ; } export default PossiblyEmptyNavStateInfoBar; diff --git a/web/settings/account-settings.react.js b/web/settings/account-settings.react.js index a134b9c12..d310d9004 100644 --- a/web/settings/account-settings.react.js +++ b/web/settings/account-settings.react.js @@ -1,121 +1,113 @@ // @flow import * as React from 'react'; import { logOut, logOutActionTypes } from 'lib/actions/user-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import { useStringForUser } from 'lib/hooks/ens-cache.js'; import { preRequestUserStateSelector } from 'lib/selectors/account-selectors.js'; import { accountHasPassword } from 'lib/shared/account-utils.js'; import { useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils.js'; import css from './account-settings.css'; import PasswordChangeModal from './password-change-modal.js'; import BlockListModal from './relationship/block-list-modal.react.js'; import FriendListModal from './relationship/friend-list-modal.react.js'; import Button from '../components/button.react.js'; import UserAvatar from '../components/user-avatar.react.js'; import { useSelector } from '../redux/redux-utils.js'; -import { shouldRenderAvatars } from '../utils/avatar-utils.js'; function AccountSettings(): React.Node { const sendLogoutRequest = useServerCall(logOut); const preRequestUserState = useSelector(preRequestUserStateSelector); const dispatchActionPromise = useDispatchActionPromise(); const logOutUser = React.useCallback( () => dispatchActionPromise( logOutActionTypes, sendLogoutRequest(preRequestUserState), ), [dispatchActionPromise, preRequestUserState, sendLogoutRequest], ); const { pushModal, popModal } = useModalContext(); const showPasswordChangeModal = React.useCallback( () => pushModal(), [pushModal], ); const openFriendList = React.useCallback( () => pushModal(), [popModal, pushModal], ); const openBlockList = React.useCallback( () => pushModal(), [popModal, pushModal], ); const isAccountWithPassword = useSelector(state => accountHasPassword(state.currentUserInfo), ); const currentUserInfo = useSelector(state => state.currentUserInfo); const stringForUser = useStringForUser(currentUserInfo); - const contentStyle = React.useMemo( - () => ({ - marginTop: shouldRenderAvatars ? 32 : 0, - }), - [], - ); - if (!currentUserInfo || currentUserInfo.anonymous) { return null; } let changePasswordSection; if (isAccountWithPassword) { changePasswordSection = (
  • Password ******
  • ); } return (

    My Account

    -
    +
    • {'Logged in as '} {stringForUser}

    • {changePasswordSection}
    • Friend List
    • Block List
    ); } export default AccountSettings; diff --git a/web/settings/relationship/block-list-row.react.js b/web/settings/relationship/block-list-row.react.js index 103e62978..abeecea27 100644 --- a/web/settings/relationship/block-list-row.react.js +++ b/web/settings/relationship/block-list-row.react.js @@ -1,50 +1,40 @@ // @flow import * as React from 'react'; import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import { useRelationshipCallbacks } from 'lib/hooks/relationship-prompt.js'; import css from './user-list-row.css'; import type { UserRowProps } from './user-list.react.js'; import MenuItem from '../../components/menu-item.react.js'; import Menu from '../../components/menu.react.js'; import UserAvatar from '../../components/user-avatar.react.js'; -import { shouldRenderAvatars } from '../../utils/avatar-utils.js'; function BlockListRow(props: UserRowProps): React.Node { const { userInfo, onMenuVisibilityChange } = props; const { unblockUser } = useRelationshipCallbacks(userInfo.id); const editIcon = ; - const usernameContainerStyle = React.useMemo( - () => ({ - marginLeft: shouldRenderAvatars ? 8 : 0, - }), - [], - ); - return (
    -
    - {userInfo.username} -
    +
    {userInfo.username}
    ); } export default BlockListRow; diff --git a/web/settings/relationship/friend-list-row.react.js b/web/settings/relationship/friend-list-row.react.js index e04297c49..5eb42c6c5 100644 --- a/web/settings/relationship/friend-list-row.react.js +++ b/web/settings/relationship/friend-list-row.react.js @@ -1,103 +1,93 @@ // @flow import * as React from 'react'; import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import { useRelationshipCallbacks } from 'lib/hooks/relationship-prompt.js'; import { userRelationshipStatus } from 'lib/types/relationship-types.js'; import css from './user-list-row.css'; import type { UserRowProps } from './user-list.react.js'; import Button from '../../components/button.react.js'; import MenuItem from '../../components/menu-item.react.js'; import Menu from '../../components/menu.react.js'; import UserAvatar from '../../components/user-avatar.react.js'; -import { shouldRenderAvatars } from '../../utils/avatar-utils.js'; const dangerButtonColor = { color: 'var(--btn-bg-danger)', }; function FriendListRow(props: UserRowProps): React.Node { const { userInfo, onMenuVisibilityChange } = props; const { friendUser, unfriendUser } = useRelationshipCallbacks(userInfo.id); const buttons = React.useMemo(() => { if (userInfo.relationshipStatus === userRelationshipStatus.REQUEST_SENT) { return ( ); } if ( userInfo.relationshipStatus === userRelationshipStatus.REQUEST_RECEIVED ) { return ( <> ); } if (userInfo.relationshipStatus === userRelationshipStatus.FRIEND) { const editIcon = ; return (
    ); } return undefined; }, [ friendUser, unfriendUser, userInfo.relationshipStatus, onMenuVisibilityChange, ]); - const usernameContainerStyle = React.useMemo( - () => ({ - marginLeft: shouldRenderAvatars ? 8 : 0, - }), - [], - ); - return (
    -
    - {userInfo.username} -
    +
    {userInfo.username}
    {buttons}
    ); } export default FriendListRow; diff --git a/web/sidebar/community-drawer-item.react.js b/web/sidebar/community-drawer-item.react.js index b94fa17ff..3821b57fd 100644 --- a/web/sidebar/community-drawer-item.react.js +++ b/web/sidebar/community-drawer-item.react.js @@ -1,126 +1,116 @@ // @flow import classnames from 'classnames'; import * as React from 'react'; import type { CommunityDrawerItemData } from 'lib/utils/drawer-utils.react.js'; import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js'; import type { HandlerProps } from './community-drawer-item-handlers.react.js'; import { getCommunityDrawerItemHandler } from './community-drawer-item-handlers.react.js'; import css from './community-drawer-item.css'; import { getChildren, getExpandButton, } from './community-drawer-utils.react.js'; import ThreadAvatar from '../components/thread-avatar.react.js'; import type { NavigationTab } from '../types/nav-types.js'; -import { shouldRenderAvatars } from '../utils/avatar-utils.js'; export type DrawerItemProps = { +itemData: CommunityDrawerItemData, +paddingLeft: number, +expandable?: boolean, +handlerType: NavigationTab, }; function CommunityDrawerItem(props: DrawerItemProps): React.Node { const { itemData: { threadInfo, itemChildren, hasSubchannelsButton, labelStyle }, paddingLeft, expandable = true, handlerType, } = props; const [handler, setHandler] = React.useState({ onClick: () => {}, expanded: false, toggleExpanded: () => {}, }); const Handler = getCommunityDrawerItemHandler(handlerType); const children = React.useMemo( () => getChildren({ expanded: handler.expanded, hasSubchannelsButton, itemChildren, paddingLeft, threadInfo, expandable, handlerType, }), [ handler.expanded, hasSubchannelsButton, itemChildren, paddingLeft, threadInfo, expandable, handlerType, ], ); const itemExpandButton = React.useMemo( () => getExpandButton({ expandable, childrenLength: itemChildren.length, hasSubchannelsButton, onExpandToggled: handler.toggleExpanded, expanded: handler.expanded, }), [ expandable, itemChildren.length, hasSubchannelsButton, handler.toggleExpanded, handler.expanded, ], ); const { uiName } = useResolvedThreadInfo(threadInfo); const titleLabel = classnames({ [css[labelStyle]]: true, [css.activeTitle]: handler.isActive, }); const style = React.useMemo(() => ({ paddingLeft }), [paddingLeft]); - const titleStyle = React.useMemo( - () => ({ - marginLeft: shouldRenderAvatars ? 8 : 0, - }), - [], - ); - return ( <>
    {itemExpandButton} -
    - {uiName} -
    +
    {uiName}
    {children}
    ); } export type CommunityDrawerItemChatProps = { +itemData: CommunityDrawerItemData, +paddingLeft: number, +expandable?: boolean, +handler: React.ComponentType, }; const MemoizedCommunityDrawerItem: React.ComponentType = React.memo(CommunityDrawerItem); export default MemoizedCommunityDrawerItem; diff --git a/web/utils/avatar-utils.js b/web/utils/avatar-utils.js deleted file mode 100644 index 66187c41c..000000000 --- a/web/utils/avatar-utils.js +++ /dev/null @@ -1,5 +0,0 @@ -// @flow - -const shouldRenderAvatars: boolean = true; - -export { shouldRenderAvatars }; diff --git a/web/utils/typeahead-utils.js b/web/utils/typeahead-utils.js index 0458e15f6..84c6b445a 100644 --- a/web/utils/typeahead-utils.js +++ b/web/utils/typeahead-utils.js @@ -1,223 +1,215 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import { oldValidUsernameRegexString } from 'lib/shared/account-utils.js'; import { getNewTextAndSelection } from 'lib/shared/mention-utils.js'; import { stringForUserExplicit } from 'lib/shared/user-utils.js'; import type { SetState } from 'lib/types/hook-types.js'; import type { RelativeMemberInfo } from 'lib/types/thread-types.js'; -import { shouldRenderAvatars } from './avatar-utils.js'; import { typeaheadStyle } from '../chat/chat-constants.js'; import css from '../chat/typeahead-tooltip.css'; import Button from '../components/button.react.js'; import UserAvatar from '../components/user-avatar.react.js'; const webTypeaheadRegex: RegExp = new RegExp( `(?(?:^(?:.|\n)*\\s+)|^)@(?${oldValidUsernameRegexString})?$`, ); export type TypeaheadTooltipAction = { +key: string, +execute: () => mixed, +actionButtonContent: { +userID: string, +username: string }, }; export type TooltipPosition = { +top: number, +left: number, }; function getCaretOffsets( textarea: HTMLTextAreaElement, text: string, ): { caretTopOffset: number, caretLeftOffset: number } { if (!textarea) { return { caretTopOffset: 0, caretLeftOffset: 0 }; } // terribly hacky but it works I guess :D // we had to use it, as it's hard to count lines in textarea // and track cursor position within it as // lines can be wrapped into new lines without \n character // as result of overflow const textareaStyle: CSSStyleDeclaration = window.getComputedStyle( textarea, null, ); const div = document.createElement('div'); for (const styleName of textareaStyle) { div.style.setProperty(styleName, textareaStyle.getPropertyValue(styleName)); } div.style.display = 'inline-block'; div.style.position = 'absolute'; div.textContent = text; const span = document.createElement('span'); span.textContent = textarea.value.slice(text.length); div.appendChild(span); document.body?.appendChild(div); const { offsetTop, offsetLeft } = span; document.body?.removeChild(div); const textareaWidth = parseInt(textareaStyle.getPropertyValue('width')); const caretLeftOffset = offsetLeft + typeaheadStyle.tooltipWidth > textareaWidth ? textareaWidth - typeaheadStyle.tooltipWidth : offsetLeft; return { caretTopOffset: offsetTop - textarea.scrollTop, caretLeftOffset, }; } export type GetTypeaheadTooltipActionsParams = { +inputStateDraft: string, +inputStateSetDraft: (draft: string) => mixed, +inputStateSetTextCursorPosition: (newPosition: number) => mixed, +suggestedUsers: $ReadOnlyArray, +textBeforeAtSymbol: string, +usernamePrefix: string, }; function getTypeaheadTooltipActions( params: GetTypeaheadTooltipActionsParams, ): $ReadOnlyArray { const { inputStateDraft, inputStateSetDraft, inputStateSetTextCursorPosition, suggestedUsers, textBeforeAtSymbol, usernamePrefix, } = params; return suggestedUsers .filter( suggestedUser => stringForUserExplicit(suggestedUser) !== 'anonymous', ) .map(suggestedUser => ({ key: suggestedUser.id, execute: () => { const { newText, newSelectionStart } = getNewTextAndSelection( textBeforeAtSymbol, inputStateDraft, usernamePrefix, suggestedUser, ); inputStateSetDraft(newText); inputStateSetTextCursorPosition(newSelectionStart); }, actionButtonContent: { userID: suggestedUser.id, username: stringForUserExplicit(suggestedUser), }, })); } function getTypeaheadTooltipButtons( setChosenPositionInOverlay: SetState, chosenPositionInOverlay: number, actions: $ReadOnlyArray, ): $ReadOnlyArray { return actions.map((action, idx) => { const { key, execute, actionButtonContent } = action; const buttonClasses = classNames(css.suggestion, { [css.suggestionHover]: idx === chosenPositionInOverlay, }); const onMouseMove: ( event: SyntheticEvent, ) => mixed = () => { setChosenPositionInOverlay(idx); }; - const usernameClassName = classNames({ - [css.username]: shouldRenderAvatars, - [css.usernameNoAvatar]: !shouldRenderAvatars, - }); - return ( ); }); } function getTypeaheadOverlayScroll( currentScrollTop: number, chosenActionPosition: number, ): number { const upperButtonBoundary = chosenActionPosition * typeaheadStyle.rowHeight; const lowerButtonBoundary = (chosenActionPosition + 1) * typeaheadStyle.rowHeight; if (upperButtonBoundary < currentScrollTop) { return upperButtonBoundary; } else if ( lowerButtonBoundary - typeaheadStyle.tooltipMaxHeight > currentScrollTop ) { return ( lowerButtonBoundary + typeaheadStyle.tooltipVerticalPadding - typeaheadStyle.tooltipMaxHeight ); } return currentScrollTop; } function getTypeaheadTooltipPosition( textarea: HTMLTextAreaElement, actionsLength: number, textBeforeAtSymbol: string, ): TooltipPosition { const { caretTopOffset, caretLeftOffset } = getCaretOffsets( textarea, textBeforeAtSymbol, ); const textareaBoundingClientRect = textarea.getBoundingClientRect(); const top: number = textareaBoundingClientRect.top - Math.min( typeaheadStyle.tooltipVerticalPadding + actionsLength * typeaheadStyle.rowHeight, typeaheadStyle.tooltipMaxHeight, ) - typeaheadStyle.tooltipTopOffset + caretTopOffset; const left: number = textareaBoundingClientRect.left - typeaheadStyle.tooltipLeftOffset + caretLeftOffset; return { top, left }; } export { webTypeaheadRegex, getCaretOffsets, getTypeaheadTooltipActions, getTypeaheadTooltipButtons, getTypeaheadOverlayScroll, getTypeaheadTooltipPosition, };