From 6a752e2d8b7e67e41aa019079b7ccbdebec3688f Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Fri, 13 Feb 2026 12:41:31 -0500 Subject: [PATCH 1/5] simplify small chat --- .../chat/inbox/row/small-team/bottom-line.tsx | 339 ---------- shared/chat/inbox/row/small-team/contexts.tsx | 8 - shared/chat/inbox/row/small-team/index.tsx | 630 ++++++++++++++---- shared/chat/inbox/row/small-team/top-line.tsx | 195 ------ shared/chat/selectable-big-team-channel.tsx | 12 +- shared/chat/selectable-small-team.tsx | 12 +- 6 files changed, 522 insertions(+), 674 deletions(-) delete mode 100644 shared/chat/inbox/row/small-team/bottom-line.tsx delete mode 100644 shared/chat/inbox/row/small-team/contexts.tsx delete mode 100644 shared/chat/inbox/row/small-team/top-line.tsx diff --git a/shared/chat/inbox/row/small-team/bottom-line.tsx b/shared/chat/inbox/row/small-team/bottom-line.tsx deleted file mode 100644 index 1dfbc213d35e..000000000000 --- a/shared/chat/inbox/row/small-team/bottom-line.tsx +++ /dev/null @@ -1,339 +0,0 @@ -import * as C from '@/constants' -import * as Chat from '@/stores/chat2' -import * as React from 'react' -import * as Kb from '@/common-adapters' -import * as T from '@/constants/types' -import {SnippetContext, SnippetDecorationContext} from './contexts' -import {useCurrentUserState} from '@/stores/current-user' - -type Props = { - layoutSnippet?: string - backgroundColor?: string - isSelected?: boolean - isInWidget?: boolean - allowBold?: boolean -} - -const SnippetDecoration = (p: {type: Kb.IconType; color: string}) => { - const {type, color} = p - return ( - - ) -} - -const Snippet = React.memo(function Snippet(p: {isSelected?: boolean; style: Kb.Styles.StylesCrossPlatform}) { - const snippet = React.useContext(SnippetContext) - const {isSelected, style} = p - - const decoration = React.useContext(SnippetDecorationContext) - let snippetDecoration: React.ReactNode - let exploded = false - const defaultIconColor = isSelected ? Kb.Styles.globalColors.white : Kb.Styles.globalColors.black_20 - let tooltip: string | undefined - - switch (decoration) { - case T.RPCChat.SnippetDecoration.pendingMessage: - tooltip = 'Sending…' - snippetDecoration = - break - case T.RPCChat.SnippetDecoration.failedPendingMessage: - tooltip = 'Failed to send' - snippetDecoration = ( - - ) - break - case T.RPCChat.SnippetDecoration.explodingMessage: - snippetDecoration = - break - case T.RPCChat.SnippetDecoration.explodedMessage: - snippetDecoration = ( - - Message exploded. - - ) - exploded = true - break - case T.RPCChat.SnippetDecoration.audioAttachment: - snippetDecoration = - break - case T.RPCChat.SnippetDecoration.videoAttachment: - snippetDecoration = - break - case T.RPCChat.SnippetDecoration.photoAttachment: - snippetDecoration = - break - case T.RPCChat.SnippetDecoration.fileAttachment: - snippetDecoration = - break - case T.RPCChat.SnippetDecoration.stellarReceived: - snippetDecoration = - break - case T.RPCChat.SnippetDecoration.stellarSent: - snippetDecoration = - break - case T.RPCChat.SnippetDecoration.pinnedMessage: - snippetDecoration = - break - default: - snippetDecoration = null - } - return ( - <> - {!!snippetDecoration && ( - - {snippetDecoration} - - )} - {!exploded && !!snippet && ( - - {snippet} - - )} - - ) -}) - -const BottomLine = React.memo(function BottomLine(p: Props) { - const {allowBold, isSelected, backgroundColor, isInWidget, layoutSnippet} = p - - const isTypingSnippet = Chat.useChatContext(s => { - const typers = !isInWidget ? s.typing : undefined - return !!typers?.size - }) - - const you = useCurrentUserState(s => s.username) - const hasUnread = Chat.useChatContext(s => s.unread > 0) - const _draft = Chat.useChatContext(s => s.meta.draft) - const {hasResetUsers, isDecryptingSnippet, participantNeedToRekey, youAreReset, youNeedToRekey} = - Chat.useChatContext( - C.useShallow(s => { - const { - membershipType, - rekeyers, - resetParticipants, - trustedState, - conversationIDKey, - snippetDecorated, - } = s.meta - const youAreReset = membershipType === 'youAreReset' - const participantNeedToRekey = rekeyers.size > 0 - const youNeedToRekey = rekeyers.has(you) - const hasResetUsers = resetParticipants.size > 0 - - // only use layout if we don't have the meta at all - const typers = !isInWidget ? s.typing : undefined - const typingSnippet = (typers?.size ?? 0) > 0 ? 't' : undefined - const maybeLayoutSnippet = - conversationIDKey === Chat.noConversationIDKey ? layoutSnippet : undefined - - const snippet = typingSnippet ?? snippetDecorated ?? maybeLayoutSnippet ?? '' - const isDecryptingSnippet = - s.id && !snippet ? trustedState === 'requesting' || trustedState === 'untrusted' : false - - return {hasResetUsers, isDecryptingSnippet, participantNeedToRekey, youAreReset, youNeedToRekey} - }) - ) - const draft = (!isSelected && !hasUnread && _draft) || '' - - const props = { - allowBold, - backgroundColor, - draft, - hasResetUsers, - hasUnread, - isDecryptingSnippet, - isSelected, - isTypingSnippet, - participantNeedToRekey, - youAreReset, - youNeedToRekey, - } - - return -}) - -type IProps = { - allowBold?: boolean - backgroundColor?: string - draft: string - hasResetUsers: boolean - hasUnread: boolean - isDecryptingSnippet: boolean - isTypingSnippet: boolean - participantNeedToRekey: boolean - youAreReset: boolean - youNeedToRekey: boolean - isSelected?: boolean -} -const BottomLineImpl = React.memo(function BottomLineImpl(p: IProps) { - const {isDecryptingSnippet, draft, youAreReset, youNeedToRekey, isSelected, allowBold = true} = p - const {isTypingSnippet, hasResetUsers, hasUnread, participantNeedToRekey, backgroundColor} = p - - const subColor = isSelected - ? Kb.Styles.globalColors.white - : hasUnread - ? Kb.Styles.globalColors.black - : Kb.Styles.globalColors.black_50 - const showBold = allowBold && !isSelected && hasUnread - - let content: React.ReactNode - const style = React.useMemo( - () => - Kb.Styles.collapseStyles([ - styles.bottomLine, - { - color: subColor, - ...(showBold ? Kb.Styles.globalStyles.fontBold : {}), - }, - isTypingSnippet ? styles.typingSnippet : null, - ]), - [isTypingSnippet, showBold, subColor] - ) - if (youNeedToRekey) { - content = null - } else if (youAreReset) { - content = ( - - You are locked out. - - ) - } else if (participantNeedToRekey) { - content = ( - - ) - } else if (draft) { - content = ( - - - Draft: - - - {draft} - - - ) - } else if (isDecryptingSnippet) { - content = ( - - ) - } else { - content = ( - - - - ) - } - return ( - - - {hasResetUsers && ( - - )} - {youNeedToRekey && ( - - )} - {content} - - - ) -}) - -const styles = Kb.Styles.styleSheetCreate( - () => - ({ - alertMeta: Kb.Styles.platformStyles({ - common: { - alignSelf: 'center', - marginRight: 6, - }, - isMobile: {marginTop: 2}, - }), - bottom: {justifyContent: 'flex-start'}, - bottomLine: Kb.Styles.platformStyles({ - isElectron: { - color: Kb.Styles.globalColors.black_50, - display: 'block', - minHeight: 16, - overflow: 'hidden', - paddingRight: 10, - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - width: '100%', - }, - isMobile: { - color: Kb.Styles.globalColors.black_50, - flex: 1, - lineHeight: 19, - paddingRight: 40, - }, - }), - contentBox: { - ...Kb.Styles.globalStyles.fillAbsolute, - alignItems: 'center', - width: '100%', - }, - draftLabel: {color: Kb.Styles.globalColors.orange}, - innerBox: Kb.Styles.platformStyles({ - common: { - ...Kb.Styles.globalStyles.flexBoxRow, - alignItems: 'center', - flexGrow: 1, - height: 17, - position: 'relative', - }, - isMobile: {height: 21}, - }), - outerBox: { - ...Kb.Styles.globalStyles.flexBoxRow, - }, - snippetDecoration: {alignSelf: 'flex-start'}, - typingSnippet: {}, - youAreResetText: Kb.Styles.platformStyles({ - isElectron: { - fontSize: 12, - lineHeight: 13, - }, - isMobile: { - fontSize: 14, - lineHeight: 19, - }, - }), - }) as const -) -export {BottomLine} diff --git a/shared/chat/inbox/row/small-team/contexts.tsx b/shared/chat/inbox/row/small-team/contexts.tsx deleted file mode 100644 index 4773e7437c6d..000000000000 --- a/shared/chat/inbox/row/small-team/contexts.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import * as React from 'react' -import * as T from '@/constants/types' -// so popups will work in both places -export const SnippetContext = React.createContext('') -export const SnippetDecorationContext = React.createContext(T.RPCChat.SnippetDecoration.none) -export const ParticipantsContext = React.createContext | string>('') -export const IsTeamContext = React.createContext(false) -export const TimeContext = React.createContext(0) diff --git a/shared/chat/inbox/row/small-team/index.tsx b/shared/chat/inbox/row/small-team/index.tsx index f0289b126b34..08ab450c49b1 100644 --- a/shared/chat/inbox/row/small-team/index.tsx +++ b/shared/chat/inbox/row/small-team/index.tsx @@ -1,23 +1,16 @@ import * as C from '@/constants' import * as Chat from '@/stores/chat2' -import * as React from 'react' +import type * as React from 'react' import * as Kb from '@/common-adapters' -import {SimpleTopLine} from './top-line' -import {BottomLine} from './bottom-line' -import {Avatars, TeamAvatar} from '@/chat/avatars' import * as RowSizes from '../sizes' import * as T from '@/constants/types' import SwipeConvActions from './swipe-conv-actions' import './small-team.css' +import {Avatars, TeamAvatar} from '@/chat/avatars' +import {formatTimeForConversationList} from '@/util/timestamp' import {useCurrentUserState} from '@/stores/current-user' -import { - IsTeamContext, - ParticipantsContext, - TimeContext, - SnippetContext, - SnippetDecorationContext, -} from './contexts' import {useOpenedRowState} from '../opened-row-state' +import TeamMenu from '@/chat/conversation/info-panel/menu' export type Props = { conversationIDKey: T.Chat.ConversationIDKey @@ -31,19 +24,30 @@ export type Props = { onSelectConversation?: () => void } -const SmallTeam = React.memo(function SmallTeam(p: Props) { +const SmallTeam = (p: Props) => { return ( - + ) -}) +} + +const SmallTeamInner = (p: Props) => { + const {layoutName, layoutIsTeam, layoutSnippet, isSelected, layoutTime, layoutSnippetDecoration, isInWidget} = p -const SmallTeamImpl = (p: Props) => { - const {layoutName, layoutIsTeam, layoutSnippet, isSelected, layoutTime, layoutSnippetDecoration} = p - const {isInWidget} = p + const you = useCurrentUserState(s => s.username) - const {snippet, snippetDecoration} = Chat.useChatContext( + const { + snippet, + snippetDecoration, + participants, + isMuted, + isLocked, + hasUnread, + hasBadge, + timestamp, + navigateToThread, + } = Chat.useChatContext( C.useShallow(s => { const typingSnippet = (() => { const typers = !isInWidget ? s.typing : undefined @@ -55,9 +59,7 @@ const SmallTeamImpl = (p: Props) => { return 'Multiple people typing...' } })() - const {meta} = s - // only use layout if we don't have the meta at all const maybeLayoutSnippet = meta.conversationIDKey === Chat.noConversationIDKey ? layoutSnippet : undefined const snippet = typingSnippet ?? meta.snippetDecorated ?? maybeLayoutSnippet ?? '' @@ -65,45 +67,48 @@ const SmallTeamImpl = (p: Props) => { meta.conversationIDKey === Chat.noConversationIDKey ? (layoutSnippetDecoration ?? T.RPCChat.SnippetDecoration.none) : meta.snippetDecoration - return {snippet, snippetDecoration} - }) - ) - const you = useCurrentUserState(s => s.username) - const navigateToThread = Chat.useChatContext(s => s.dispatch.navigateToThread) - const participants = Chat.useChatContext( - C.useShallow(s => { - const {meta} = s + const participantInfo = s.participants const teamname = (meta.teamname || layoutIsTeam ? layoutName : '') || '' const channelname = isInWidget ? meta.channelname : '' + let participants: Array | string if (teamname && channelname) { - return `${teamname}#${channelname}` - } - if (participantInfo.name.length) { - // Filter out ourselves unless it's our 1:1 conversation - return participantInfo.name.filter((participant, _, list) => + participants = `${teamname}#${channelname}` + } else if (participantInfo.name.length) { + participants = participantInfo.name.filter((participant, _, list) => list.length === 1 ? true : participant !== you ) + } else if (layoutIsTeam && layoutName) { + participants = [layoutName] + } else { + participants = + layoutName + ?.split(',') + .filter((participant, _, list) => (list.length === 1 ? true : participant !== you)) ?? [] } - if (layoutIsTeam && layoutName) { - return [layoutName] + + return { + hasBadge: s.badge > 0, + hasUnread: s.unread > 0, + isLocked: meta.rekeyers.has(you) || meta.rekeyers.size > 0 || !!meta.wasFinalizedBy, + isMuted: meta.isMuted, + navigateToThread: s.dispatch.navigateToThread, + participants, + snippet, + snippetDecoration, + timestamp: meta.timestamp || layoutTime || 0, } - return ( - layoutName - ?.split(',') - .filter((participant, _, list) => (list.length === 1 ? true : participant !== you)) ?? [] - ) }) ) const setOpenedRow = useOpenedRowState(s => s.dispatch.setOpenRow) - - const _onSelectConversation = React.useCallback(() => { - setOpenedRow(Chat.noConversationIDKey) - navigateToThread('inboxSmall') - }, [navigateToThread, setOpenedRow]) - - const onSelectConversation = isSelected ? undefined : (p.onSelectConversation ?? _onSelectConversation) + const onSelectConversation = isSelected + ? undefined + : (p.onSelectConversation ?? + (() => { + setOpenedRow(Chat.noConversationIDKey) + navigateToThread('inboxSmall') + })) const backgroundColor = isInWidget ? Kb.Styles.globalColors.white @@ -113,116 +118,468 @@ const SmallTeamImpl = (p: Props) => { ? Kb.Styles.globalColors.fastBlank : Kb.Styles.globalColors.blueGrey - const children = React.useMemo(() => { - return ( - - + + - - - - - - - + ) : ( + + )} + + + - - - - + + + + + + + ) +} + +type TopLineProps = { + isSelected: boolean + isInWidget: boolean + hasUnread: boolean + hasBadge: boolean + participants: Array | string + timestamp: number +} + +const TopLine = (p: TopLineProps) => { + const {isSelected, isInWidget, hasUnread, hasBadge, participants, timestamp} = p + const showGear = !isInWidget + const showBold = !isSelected && hasUnread + const subColor = isSelected + ? Kb.Styles.globalColors.white + : hasUnread + ? Kb.Styles.globalColors.black + : Kb.Styles.globalColors.black_50 + const iconHoverColor = isSelected ? Kb.Styles.globalColors.white_75 : Kb.Styles.globalColors.black + + const makePopup = (p: Kb.Popup2Parms) => { + const {attachTo, hidePopup} = p + return ( + ) - }, [backgroundColor, isInWidget, isSelected, onSelectConversation, layoutSnippet]) + } + const {showingPopup, showPopup, popup, popupAnchor} = Kb.usePopup2(makePopup) + + const tssubColor = (!hasBadge || isSelected) && subColor + const timestampStyle = Kb.Styles.collapseStyles([ + showBold && styles.bold, + styles.timestamp, + tssubColor !== false && ({color: tssubColor} as Kb.Styles.StylesCrossPlatform), + ]) + const timestampText = timestamp ? formatTimeForConversationList(timestamp) : '' + + const usernameColor = isSelected ? Kb.Styles.globalColors.white : Kb.Styles.globalColors.black + const nameBackgroundColor = isInWidget + ? Kb.Styles.globalColors.white + : isSelected + ? Kb.Styles.globalColors.blue + : Kb.Styles.isPhone + ? Kb.Styles.globalColors.fastBlank + : Kb.Styles.globalColors.blueGrey + const nameContainerStyle = Kb.Styles.collapseStyles([ + styles.name, + showBold && styles.bold, + {color: usernameColor}, + Kb.Styles.isMobile && {backgroundColor: nameBackgroundColor}, + ]) + const teamContainerStyle = Kb.Styles.collapseStyles([ + styles.teamTextStyle, + showBold && styles.bold, + {color: usernameColor}, + ]) return ( - - - - - - {children} - - - - - + + {showGear && showingPopup && popup} + + + {typeof participants === 'string' ? ( + + + {participants} + + + ) : ( + + )} + + + + {timestampText} + + {!Kb.Styles.isMobile && showGear && ( + + )} + {hasBadge ? : null} + ) } -type RowAvatarProps = { +type BottomLineProps = { + snippet?: string + snippetDecoration?: T.RPCChat.SnippetDecoration backgroundColor?: string - isSelected: boolean + isSelected?: boolean + isInWidget?: boolean + allowBold?: boolean } -const RowAvatars = React.memo(function RowAvatars(p: RowAvatarProps) { - const {backgroundColor, isSelected} = p - const layoutIsTeam = React.useContext(IsTeamContext) - const participants = React.useContext(ParticipantsContext) - const isMuted = Chat.useChatContext(s => s.meta.isMuted) + +const BottomLine = (p: BottomLineProps) => { + const {allowBold = true, isSelected, backgroundColor, isInWidget} = p + const snippet = p.snippet ?? '' + const snippetDecoration = p.snippetDecoration ?? T.RPCChat.SnippetDecoration.none + const you = useCurrentUserState(s => s.username) - const isLocked = Chat.useChatContext(s => { - const {meta} = s - const isLocked = meta.rekeyers.has(you) || meta.rekeyers.size > 0 || !!meta.wasFinalizedBy - return isLocked - }) + const { + isTypingSnippet, + hasUnread, + draft: _draft, + hasResetUsers, + participantNeedToRekey, + youAreReset, + youNeedToRekey, + trustedState, + hasId, + } = Chat.useChatContext( + C.useShallow(s => { + const typers = !isInWidget ? s.typing : undefined + const {membershipType, rekeyers, resetParticipants, trustedState} = s.meta + return { + draft: s.meta.draft, + hasId: !!s.id, + hasResetUsers: resetParticipants.size > 0, + hasUnread: s.unread > 0, + isTypingSnippet: !!typers?.size, + participantNeedToRekey: rekeyers.size > 0, + trustedState, + youAreReset: membershipType === 'youAreReset', + youNeedToRekey: rekeyers.has(you), + } + }) + ) - let participantOne = '' - let participantTwo = '' - let teamname = '' + const isDecryptingSnippet = + hasId && !snippet ? trustedState === 'requesting' || trustedState === 'untrusted' : false + const draft = (!isSelected && !hasUnread && _draft) || '' - if (typeof participants === 'string') { - teamname = participants.split('#')[0] ?? '' - } else if (layoutIsTeam) { - teamname = participants[0] ?? '' + const subColor = isSelected + ? Kb.Styles.globalColors.white + : hasUnread + ? Kb.Styles.globalColors.black + : Kb.Styles.globalColors.black_50 + const showBold = allowBold && !isSelected && hasUnread + const style = Kb.Styles.collapseStyles([ + styles.bottomLine, + {color: subColor, ...(showBold ? Kb.Styles.globalStyles.fontBold : {})}, + isTypingSnippet && styles.typingSnippet, + ]) + + let content: React.ReactNode + if (youNeedToRekey) { + content = null + } else if (youAreReset) { + content = ( + + You are locked out. + + ) + } else if (participantNeedToRekey) { + content = ( + + ) + } else if (draft) { + content = ( + + + Draft: + + + {draft} + + + ) + } else if (isDecryptingSnippet) { + content = ( + + ) } else { - participantOne = participants[0] ?? '' - participantTwo = participants[1] ?? '' + content = ( + + + + ) } - return teamname ? ( - - ) : ( - + + return ( + + + {hasResetUsers && ( + + )} + {youNeedToRekey && ( + + )} + {content} + + ) -}) +} + +const SnippetContent = (p: { + snippet: string + snippetDecoration: T.RPCChat.SnippetDecoration + isSelected?: boolean + style: Kb.Styles.StylesCrossPlatform +}) => { + const {snippet, snippetDecoration: decoration, isSelected, style} = p + const defaultIconColor = isSelected ? Kb.Styles.globalColors.white : Kb.Styles.globalColors.black_20 + + let decorationNode: React.ReactNode + let exploded = false + let tooltip: string | undefined + + switch (decoration) { + case T.RPCChat.SnippetDecoration.pendingMessage: + tooltip = 'Sending\u2026' + decorationNode = + break + case T.RPCChat.SnippetDecoration.failedPendingMessage: + tooltip = 'Failed to send' + decorationNode = ( + + ) + break + case T.RPCChat.SnippetDecoration.explodingMessage: + decorationNode = + break + case T.RPCChat.SnippetDecoration.explodedMessage: + decorationNode = ( + + Message exploded. + + ) + exploded = true + break + case T.RPCChat.SnippetDecoration.audioAttachment: + decorationNode = + break + case T.RPCChat.SnippetDecoration.videoAttachment: + decorationNode = + break + case T.RPCChat.SnippetDecoration.photoAttachment: + decorationNode = + break + case T.RPCChat.SnippetDecoration.fileAttachment: + decorationNode = + break + case T.RPCChat.SnippetDecoration.stellarReceived: + decorationNode = + break + case T.RPCChat.SnippetDecoration.stellarSent: + decorationNode = + break + case T.RPCChat.SnippetDecoration.pinnedMessage: + decorationNode = + break + default: + decorationNode = null + } + + return ( + <> + {!!decorationNode && ( + + {decorationNode} + + )} + {!exploded && !!snippet && ( + + {snippet} + + )} + + ) +} + +const SnippetDecorationIcon = (p: {type: Kb.IconType; color: string}) => ( + +) const styles = Kb.Styles.styleSheetCreate( () => ({ + alertMeta: Kb.Styles.platformStyles({ + common: {alignSelf: 'center', marginRight: 6}, + isMobile: {marginTop: 2}, + }), + bold: {...Kb.Styles.globalStyles.fontBold}, + bottom: {justifyContent: 'flex-start'}, + bottomLine: Kb.Styles.platformStyles({ + isElectron: { + color: Kb.Styles.globalColors.black_50, + display: 'block', + minHeight: 16, + overflow: 'hidden', + paddingRight: 10, + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + width: '100%', + }, + isMobile: { + color: Kb.Styles.globalColors.black_50, + flex: 1, + lineHeight: 19, + paddingRight: 40, + }, + }), container: { flexShrink: 0, height: RowSizes.smallRowHeight, }, + contentBox: { + ...Kb.Styles.globalStyles.fillAbsolute, + alignItems: 'center', + width: '100%', + }, conversationRow: { - ...Kb.Styles.globalStyles.flexBoxColumn, flexGrow: 1, height: '100%', justifyContent: 'center', paddingLeft: Kb.Styles.globalMargins.tiny, }, + draftLabel: {color: Kb.Styles.globalColors.orange}, fastBlank: Kb.Styles.platformStyles({ isPhone: {backgroundColor: Kb.Styles.globalColors.fastBlank}, isTablet: {backgroundColor: undefined}, }), - flexOne: {flex: 1}, + icon: {position: 'relative'} as const, + innerBox: Kb.Styles.platformStyles({ + common: { + alignItems: 'center', + flexGrow: 1, + height: 17, + position: 'relative', + }, + isMobile: {height: 21}, + }), + insideContainer: { + flexGrow: 1, + height: Kb.Styles.isMobile ? 21 : 17, + position: 'relative', + }, + name: {paddingRight: Kb.Styles.globalMargins.tiny}, + nameContainer: { + ...Kb.Styles.globalStyles.fillAbsolute, + alignItems: 'center', + }, rowContainer: Kb.Styles.platformStyles({ common: { - ...Kb.Styles.globalStyles.flexBoxRow, alignItems: 'center', height: '100%', paddingLeft: Kb.Styles.globalMargins.xsmall, @@ -234,12 +591,41 @@ const styles = Kb.Styles.styleSheetCreate( paddingRight: Kb.Styles.globalMargins.small, }, }), + snippetDecoration: {alignSelf: 'flex-start'} as const, + teamTextStyle: Kb.Styles.platformStyles({ + isElectron: { + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + }, + }), + timestamp: Kb.Styles.platformStyles({ + common: { + backgroundColor: Kb.Styles.globalColors.fastBlank, + color: Kb.Styles.globalColors.blueDark, + }, + isTablet: {backgroundColor: undefined}, + }), + topContainer: { + alignItems: 'center', + }, + typingSnippet: Kb.Styles.platformStyles({}), + unreadDotStyle: { + backgroundColor: Kb.Styles.globalColors.orange, + borderRadius: 6, + height: 8, + marginLeft: 4, + width: 8, + }, withBottomLine: { justifyContent: 'flex-end', paddingBottom: Kb.Styles.globalMargins.xxtiny, }, - withoutBottomLine: {justifyContent: 'center'}, + youAreResetText: Kb.Styles.platformStyles({ + isElectron: {fontSize: 12, lineHeight: 13}, + isMobile: {fontSize: 14, lineHeight: 19}, + }), }) as const ) -export {SmallTeam} +export {SmallTeam, BottomLine} diff --git a/shared/chat/inbox/row/small-team/top-line.tsx b/shared/chat/inbox/row/small-team/top-line.tsx deleted file mode 100644 index 3e45c439e679..000000000000 --- a/shared/chat/inbox/row/small-team/top-line.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import * as Chat from '@/stores/chat2' -import * as React from 'react' -import * as Kb from '@/common-adapters' -import TeamMenu from '@/chat/conversation/info-panel/menu' -import {formatTimeForConversationList} from '@/util/timestamp' -import {TimeContext, ParticipantsContext} from './contexts' - -type Props = { - isSelected: boolean - layoutName?: string - layoutIsTeam?: boolean - isInWidget: boolean - layoutTime?: number -} - -const Timestamp = React.memo(function Timestamp() { - const layoutTime = React.useContext(TimeContext) - const timeNum = Chat.useChatContext(s => s.meta.timestamp || layoutTime) - const timestamp = timeNum ? formatTimeForConversationList(timeNum) : '' - return <>{timestamp} -}) - -const Names = React.memo(function Names(p: {isSelected?: boolean; showBold: boolean; isInWidget: boolean}) { - const participants = React.useContext(ParticipantsContext) - const {isSelected, isInWidget, showBold} = p - const usernameColor = isSelected ? Kb.Styles.globalColors.white : Kb.Styles.globalColors.black - const backgroundColor = isInWidget - ? Kb.Styles.globalColors.white - : isSelected - ? Kb.Styles.globalColors.blue - : Kb.Styles.isPhone - ? Kb.Styles.globalColors.fastBlank - : Kb.Styles.globalColors.blueGrey - const nameContainerStyle = React.useMemo( - () => - Kb.Styles.collapseStyles([ - styles.name, - showBold && styles.bold, - {color: usernameColor}, - Kb.Styles.isMobile && {backgroundColor}, - ]), - [showBold, usernameColor, backgroundColor] - ) - - const teamContainerStyle = React.useMemo( - () => Kb.Styles.collapseStyles([styles.teamTextStyle, showBold && styles.bold, {color: usernameColor}]), - [showBold, usernameColor] - ) - return typeof participants === 'string' ? ( - - - {participants} - - - ) : ( - - ) -}) - -const SimpleTopLine = React.memo(function SimpleTopLine(p: Props) { - const {isSelected, isInWidget} = p - const hasUnread = Chat.useChatContext(s => s.unread > 0) - const hasBadge = Chat.useChatContext(s => s.badge > 0) - const props = { - hasBadge, - hasUnread, - isInWidget, - isSelected, - } - return -}) - -type IProps = { - isSelected?: boolean - isInWidget: boolean - hasBadge: boolean - hasUnread: boolean -} -const SimpleTopLineImpl = React.memo(function SimpleTopLineImpl(p: IProps) { - const {isSelected, isInWidget, hasBadge, hasUnread} = p - const showGear = !isInWidget - const showBold = !isSelected && hasUnread - const subColor = isSelected - ? Kb.Styles.globalColors.white - : hasUnread - ? Kb.Styles.globalColors.black - : Kb.Styles.globalColors.black_50 - - const iconHoverColor = isSelected ? Kb.Styles.globalColors.white_75 : Kb.Styles.globalColors.black - - const makePopup = React.useCallback((p: Kb.Popup2Parms) => { - const {attachTo, hidePopup} = p - return ( - - ) - }, []) - const {showingPopup, showPopup, popup, popupAnchor} = Kb.usePopup2(makePopup) - - const tssubColor = (!hasBadge || isSelected) && subColor - const timestampStyle = React.useMemo( - () => - Kb.Styles.collapseStyles([ - showBold && styles.bold, - styles.timestamp, - tssubColor !== false && ({color: tssubColor} as Kb.Styles.StylesCrossPlatform), - ]), - [showBold, tssubColor] - ) - - return ( - - {showGear && showingPopup && popup} - - - - - - - - - {!Kb.Styles.isMobile && showGear && ( - - )} - {hasBadge ? : null} - - ) -}) - -const styles = Kb.Styles.styleSheetCreate( - () => - ({ - bold: {...Kb.Styles.globalStyles.fontBold}, - container: { - ...Kb.Styles.globalStyles.flexBoxRow, - alignItems: 'center', - }, - icon: {position: 'relative'}, - insideContainer: { - ...Kb.Styles.globalStyles.flexBoxRow, - flexGrow: 1, - height: Kb.Styles.isMobile ? 21 : 17, - position: 'relative', - }, - name: {paddingRight: Kb.Styles.globalMargins.tiny}, - nameContainer: { - ...Kb.Styles.globalStyles.flexBoxRow, - ...Kb.Styles.globalStyles.fillAbsolute, - alignItems: 'center', - }, - teamTextStyle: Kb.Styles.platformStyles({ - isElectron: { - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - }, - }), - timestamp: Kb.Styles.platformStyles({ - common: { - backgroundColor: Kb.Styles.globalColors.fastBlank, - color: Kb.Styles.globalColors.blueDark, - }, - isTablet: {backgroundColor: undefined}, - }), - unreadDotStyle: { - backgroundColor: Kb.Styles.globalColors.orange, - borderRadius: 6, - height: 8, - marginLeft: 4, - width: 8, - }, - }) as const -) - -export {SimpleTopLine} diff --git a/shared/chat/selectable-big-team-channel.tsx b/shared/chat/selectable-big-team-channel.tsx index bc8758fcb6c5..c644d8ea5c8e 100644 --- a/shared/chat/selectable-big-team-channel.tsx +++ b/shared/chat/selectable-big-team-channel.tsx @@ -2,9 +2,8 @@ import * as React from 'react' import * as Kb from '@/common-adapters' import {TeamAvatar} from './avatars' import {pluralize} from '@/util/string' -import {BottomLine} from './inbox/row/small-team/bottom-line' +import {BottomLine} from './inbox/row/small-team' import type * as T from '@/constants/types' -import {SnippetContext} from './inbox/row/small-team/contexts' type Props = { isSelected: boolean @@ -68,9 +67,12 @@ const SelectableBigTeamChannel = (props: Props) => { {!props.numSearchHits && ( - - - + )} {!!props.numSearchHits && ( { usernameColor={props.usernameColor} /> {!props.numSearchHits && ( - - - + )} {props.showBadge && } From 984afb152a48542ee6ddc7fb41b3b99014d45100 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Fri, 13 Feb 2026 14:56:48 -0500 Subject: [PATCH 2/5] WIP --- shared/chat/inbox/row/small-team/index.tsx | 134 +++++++++------------ 1 file changed, 54 insertions(+), 80 deletions(-) diff --git a/shared/chat/inbox/row/small-team/index.tsx b/shared/chat/inbox/row/small-team/index.tsx index 08ab450c49b1..a301c0b0ec83 100644 --- a/shared/chat/inbox/row/small-team/index.tsx +++ b/shared/chat/inbox/row/small-team/index.tsx @@ -37,67 +37,63 @@ const SmallTeamInner = (p: Props) => { const you = useCurrentUserState(s => s.username) - const { - snippet, - snippetDecoration, - participants, - isMuted, - isLocked, - hasUnread, - hasBadge, - timestamp, - navigateToThread, - } = Chat.useChatContext( - C.useShallow(s => { - const typingSnippet = (() => { - const typers = !isInWidget ? s.typing : undefined - if (!typers?.size) return undefined - if (typers.size === 1) { - const [t] = typers - return `${t} is typing...` - } else { - return 'Multiple people typing...' + const {snippet, snippetDecoration, isMuted, isLocked, hasUnread, hasBadge, timestamp, navigateToThread} = + Chat.useChatContext( + C.useShallow(s => { + const typingSnippet = (() => { + const typers = !isInWidget ? s.typing : undefined + if (!typers?.size) return undefined + if (typers.size === 1) { + const [t] = typers + return `${t} is typing...` + } else { + return 'Multiple people typing...' + } + })() + const {meta} = s + const maybeLayoutSnippet = + meta.conversationIDKey === Chat.noConversationIDKey ? layoutSnippet : undefined + const snippet = typingSnippet ?? meta.snippetDecorated ?? maybeLayoutSnippet ?? '' + const snippetDecoration = + meta.conversationIDKey === Chat.noConversationIDKey + ? (layoutSnippetDecoration ?? T.RPCChat.SnippetDecoration.none) + : meta.snippetDecoration + + return { + hasBadge: s.badge > 0, + hasUnread: s.unread > 0, + isLocked: meta.rekeyers.has(you) || meta.rekeyers.size > 0 || !!meta.wasFinalizedBy, + isMuted: meta.isMuted, + navigateToThread: s.dispatch.navigateToThread, + snippet, + snippetDecoration, + timestamp: meta.timestamp || layoutTime || 0, } - })() - const {meta} = s - const maybeLayoutSnippet = - meta.conversationIDKey === Chat.noConversationIDKey ? layoutSnippet : undefined - const snippet = typingSnippet ?? meta.snippetDecorated ?? maybeLayoutSnippet ?? '' - const snippetDecoration = - meta.conversationIDKey === Chat.noConversationIDKey - ? (layoutSnippetDecoration ?? T.RPCChat.SnippetDecoration.none) - : meta.snippetDecoration + }) + ) + const participants = Chat.useChatContext( + C.useShallow(s => { + const {meta} = s const participantInfo = s.participants const teamname = (meta.teamname || layoutIsTeam ? layoutName : '') || '' const channelname = isInWidget ? meta.channelname : '' - let participants: Array | string if (teamname && channelname) { - participants = `${teamname}#${channelname}` - } else if (participantInfo.name.length) { - participants = participantInfo.name.filter((participant, _, list) => + return `${teamname}#${channelname}` + } + if (participantInfo.name.length) { + return participantInfo.name.filter((participant, _, list) => list.length === 1 ? true : participant !== you ) - } else if (layoutIsTeam && layoutName) { - participants = [layoutName] - } else { - participants = - layoutName - ?.split(',') - .filter((participant, _, list) => (list.length === 1 ? true : participant !== you)) ?? [] } - - return { - hasBadge: s.badge > 0, - hasUnread: s.unread > 0, - isLocked: meta.rekeyers.has(you) || meta.rekeyers.size > 0 || !!meta.wasFinalizedBy, - isMuted: meta.isMuted, - navigateToThread: s.dispatch.navigateToThread, - participants, - snippet, - snippetDecoration, - timestamp: meta.timestamp || layoutTime || 0, + if (layoutIsTeam && layoutName) { + return [layoutName] } + return ( + layoutName + ?.split(',') + .filter((participant, _, list) => (list.length === 1 ? true : participant !== you)) ?? [] + ) }) ) @@ -141,10 +137,7 @@ const SmallTeamInner = (p: Props) => { : styles.container } > - + {teamname ? ( ) : ( @@ -157,10 +150,7 @@ const SmallTeamInner = (p: Props) => { participantTwo={participantTwo} /> )} - + { @@ -242,10 +231,10 @@ const TopLine = (p: TopLineProps) => { ]) return ( - + {showGear && showingPopup && popup} - + {typeof participants === 'string' ? ( @@ -294,18 +283,16 @@ type BottomLineProps = { snippetDecoration?: T.RPCChat.SnippetDecoration backgroundColor?: string isSelected?: boolean - isInWidget?: boolean allowBold?: boolean } const BottomLine = (p: BottomLineProps) => { - const {allowBold = true, isSelected, backgroundColor, isInWidget} = p + const {allowBold = true, isSelected, backgroundColor} = p const snippet = p.snippet ?? '' const snippetDecoration = p.snippetDecoration ?? T.RPCChat.SnippetDecoration.none const you = useCurrentUserState(s => s.username) const { - isTypingSnippet, hasUnread, draft: _draft, hasResetUsers, @@ -316,14 +303,12 @@ const BottomLine = (p: BottomLineProps) => { hasId, } = Chat.useChatContext( C.useShallow(s => { - const typers = !isInWidget ? s.typing : undefined const {membershipType, rekeyers, resetParticipants, trustedState} = s.meta return { draft: s.meta.draft, hasId: !!s.id, hasResetUsers: resetParticipants.size > 0, hasUnread: s.unread > 0, - isTypingSnippet: !!typers?.size, participantNeedToRekey: rekeyers.size > 0, trustedState, youAreReset: membershipType === 'youAreReset', @@ -345,7 +330,6 @@ const BottomLine = (p: BottomLineProps) => { const style = Kb.Styles.collapseStyles([ styles.bottomLine, {color: subColor, ...(showBold ? Kb.Styles.globalStyles.fontBold : {})}, - isTypingSnippet && styles.typingSnippet, ]) let content: React.ReactNode @@ -400,10 +384,7 @@ const BottomLine = (p: BottomLineProps) => { return ( - + {hasResetUsers && ( )} @@ -414,7 +395,7 @@ const BottomLine = (p: BottomLineProps) => { backgroundColor={Kb.Styles.globalColors.red} /> )} - {content} + {content} ) @@ -561,7 +542,6 @@ const styles = Kb.Styles.styleSheetCreate( icon: {position: 'relative'} as const, innerBox: Kb.Styles.platformStyles({ common: { - alignItems: 'center', flexGrow: 1, height: 17, position: 'relative', @@ -576,11 +556,9 @@ const styles = Kb.Styles.styleSheetCreate( name: {paddingRight: Kb.Styles.globalMargins.tiny}, nameContainer: { ...Kb.Styles.globalStyles.fillAbsolute, - alignItems: 'center', }, rowContainer: Kb.Styles.platformStyles({ common: { - alignItems: 'center', height: '100%', paddingLeft: Kb.Styles.globalMargins.xsmall, paddingRight: Kb.Styles.globalMargins.xsmall, @@ -606,10 +584,6 @@ const styles = Kb.Styles.styleSheetCreate( }, isTablet: {backgroundColor: undefined}, }), - topContainer: { - alignItems: 'center', - }, - typingSnippet: Kb.Styles.platformStyles({}), unreadDotStyle: { backgroundColor: Kb.Styles.globalColors.orange, borderRadius: 6, From 5a5d1007cadcba1715b53c20b4260625fce67952 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Fri, 13 Feb 2026 15:16:21 -0500 Subject: [PATCH 3/5] WIP --- shared/chat/inbox/row/small-team/index.tsx | 53 +++++++++------------- 1 file changed, 21 insertions(+), 32 deletions(-) diff --git a/shared/chat/inbox/row/small-team/index.tsx b/shared/chat/inbox/row/small-team/index.tsx index a301c0b0ec83..a37186eeab84 100644 --- a/shared/chat/inbox/row/small-team/index.tsx +++ b/shared/chat/inbox/row/small-team/index.tsx @@ -37,7 +37,7 @@ const SmallTeamInner = (p: Props) => { const you = useCurrentUserState(s => s.username) - const {snippet, snippetDecoration, isMuted, isLocked, hasUnread, hasBadge, timestamp, navigateToThread} = + const {snippet, snippetDecoration, isMuted, isLocked, hasUnread, hasBadge, timestamp, navigateToThread, teamDisplayName} = Chat.useChatContext( C.useShallow(s => { const typingSnippet = (() => { @@ -58,6 +58,11 @@ const SmallTeamInner = (p: Props) => { meta.conversationIDKey === Chat.noConversationIDKey ? (layoutSnippetDecoration ?? T.RPCChat.SnippetDecoration.none) : meta.snippetDecoration + const metaTeamname = (meta.teamname || (layoutIsTeam ? layoutName : '')) || '' + const channelname = isInWidget ? meta.channelname : '' + const teamDisplayName = metaTeamname + ? channelname ? `${metaTeamname}#${channelname}` : metaTeamname + : '' return { hasBadge: s.badge > 0, @@ -67,6 +72,7 @@ const SmallTeamInner = (p: Props) => { navigateToThread: s.dispatch.navigateToThread, snippet, snippetDecoration, + teamDisplayName, timestamp: meta.timestamp || layoutTime || 0, } }) @@ -74,20 +80,14 @@ const SmallTeamInner = (p: Props) => { const participants = Chat.useChatContext( C.useShallow(s => { - const {meta} = s const participantInfo = s.participants - const teamname = (meta.teamname || layoutIsTeam ? layoutName : '') || '' - const channelname = isInWidget ? meta.channelname : '' - if (teamname && channelname) { - return `${teamname}#${channelname}` - } if (participantInfo.name.length) { return participantInfo.name.filter((participant, _, list) => list.length === 1 ? true : participant !== you ) } - if (layoutIsTeam && layoutName) { - return [layoutName] + if (layoutIsTeam) { + return [] } return ( layoutName @@ -114,17 +114,9 @@ const SmallTeamInner = (p: Props) => { ? Kb.Styles.globalColors.fastBlank : Kb.Styles.globalColors.blueGrey - let participantOne = '' - let participantTwo = '' - let teamname = '' - if (typeof participants === 'string') { - teamname = participants.split('#')[0] ?? '' - } else if (layoutIsTeam) { - teamname = participants[0] ?? '' - } else { - participantOne = participants[0] ?? '' - participantTwo = participants[1] ?? '' - } + const teamname = teamDisplayName ? teamDisplayName.split('#')[0] ?? '' : '' + const participantOne = teamname ? '' : participants[0] ?? '' + const participantTwo = teamname ? '' : participants[1] ?? '' return ( @@ -157,6 +149,8 @@ const SmallTeamInner = (p: Props) => { isInWidget={isInWidget} hasUnread={hasUnread} hasBadge={hasBadge} + backgroundColor={backgroundColor} + teamDisplayName={teamDisplayName} participants={participants} timestamp={timestamp} /> @@ -179,12 +173,14 @@ type TopLineProps = { isInWidget: boolean hasUnread: boolean hasBadge: boolean - participants: Array | string + backgroundColor: string + teamDisplayName: string + participants: Array timestamp: number } const TopLine = (p: TopLineProps) => { - const {isSelected, isInWidget, hasUnread, hasBadge, participants, timestamp} = p + const {isSelected, isInWidget, hasUnread, hasBadge, backgroundColor, teamDisplayName, participants, timestamp} = p const showGear = !isInWidget const showBold = !isSelected && hasUnread const subColor = isSelected @@ -211,18 +207,11 @@ const TopLine = (p: TopLineProps) => { const timestampText = timestamp ? formatTimeForConversationList(timestamp) : '' const usernameColor = isSelected ? Kb.Styles.globalColors.white : Kb.Styles.globalColors.black - const nameBackgroundColor = isInWidget - ? Kb.Styles.globalColors.white - : isSelected - ? Kb.Styles.globalColors.blue - : Kb.Styles.isPhone - ? Kb.Styles.globalColors.fastBlank - : Kb.Styles.globalColors.blueGrey const nameContainerStyle = Kb.Styles.collapseStyles([ styles.name, showBold && styles.bold, {color: usernameColor}, - Kb.Styles.isMobile && {backgroundColor: nameBackgroundColor}, + Kb.Styles.isMobile && {backgroundColor}, ]) const teamContainerStyle = Kb.Styles.collapseStyles([ styles.teamTextStyle, @@ -235,10 +224,10 @@ const TopLine = (p: TopLineProps) => { {showGear && showingPopup && popup} - {typeof participants === 'string' ? ( + {teamDisplayName ? ( - {participants} + {teamDisplayName} ) : ( From 6fb5e491f5532759ba712f73024b0384b4e51699 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Fri, 13 Feb 2026 15:30:00 -0500 Subject: [PATCH 4/5] WIP --- shared/chat/inbox/row/big-team-channel.tsx | 61 ++++++++++++--------- shared/chat/inbox/row/big-team-header.tsx | 45 +++++++-------- shared/chat/inbox/row/big-teams-divider.tsx | 11 ++-- shared/chat/inbox/row/big-teams-label.tsx | 19 ++++--- 4 files changed, 67 insertions(+), 69 deletions(-) diff --git a/shared/chat/inbox/row/big-team-channel.tsx b/shared/chat/inbox/row/big-team-channel.tsx index e4089507b3b3..f80b156a94fa 100644 --- a/shared/chat/inbox/row/big-team-channel.tsx +++ b/shared/chat/inbox/row/big-team-channel.tsx @@ -1,6 +1,7 @@ +import * as C from '@/constants' import * as Chat from '@/stores/chat2' +import type * as React from 'react' import * as Kb from '@/common-adapters' -import * as React from 'react' import * as RowSizes from './sizes' import * as T from '@/constants/types' @@ -12,36 +13,43 @@ type Props = { layoutSnippetDecoration?: T.RPCChat.SnippetDecoration } -const BigTeamChannel = React.memo(function BigTeamChannel(props: Props) { +const BigTeamChannel = (props: Props) => { return ( - + ) -}) -const BigTeamChannelImpl = (props: Props) => { +} +const BigTeamChannelInner = (props: Props) => { const {selected, layoutChannelname, layoutSnippetDecoration} = props - const channelname = Chat.useChatContext(s => s.meta.channelname || layoutChannelname) - const isError = Chat.useChatContext(s => s.meta.trustedState === 'error') - const snippetDecoration = Chat.useChatContext(s => { - const d = - s.meta.conversationIDKey === Chat.noConversationIDKey - ? (layoutSnippetDecoration ?? T.RPCChat.SnippetDecoration.none) - : s.meta.snippetDecoration - - switch (d) { - case T.RPCChat.SnippetDecoration.pendingMessage: - case T.RPCChat.SnippetDecoration.failedPendingMessage: - return d - default: - return 0 - } - }) - const hasBadge = Chat.useChatContext(s => s.badge > 0) - const hasDraft = Chat.useChatContext(s => !!s.meta.draft) - const hasUnread = Chat.useChatContext(s => s.unread > 0) - const isMuted = Chat.useChatContext(s => s.meta.isMuted) - const navigateToThread = Chat.useChatContext(s => s.dispatch.navigateToThread) + const {channelname, isError, snippetDecoration, hasBadge, hasDraft, hasUnread, isMuted, navigateToThread} = + Chat.useChatContext( + C.useShallow(s => { + const d = + s.meta.conversationIDKey === Chat.noConversationIDKey + ? (layoutSnippetDecoration ?? T.RPCChat.SnippetDecoration.none) + : s.meta.snippetDecoration + let snippetDecoration: number + switch (d) { + case T.RPCChat.SnippetDecoration.pendingMessage: + case T.RPCChat.SnippetDecoration.failedPendingMessage: + snippetDecoration = d + break + default: + snippetDecoration = 0 + } + return { + channelname: s.meta.channelname || layoutChannelname, + hasBadge: s.badge > 0, + hasDraft: !!s.meta.draft, + hasUnread: s.unread > 0, + isError: s.meta.trustedState === 'error', + isMuted: s.meta.isMuted, + navigateToThread: s.dispatch.navigateToThread, + snippetDecoration, + } + }) + ) const onSelectConversation = () => navigateToThread('inboxBig') let outboxTooltip: string | undefined @@ -155,7 +163,6 @@ const styles = Kb.Styles.styleSheetCreate( ({ channelBackground: Kb.Styles.platformStyles({ common: { - ...Kb.Styles.globalStyles.flexBoxRow, alignItems: 'center', marginLeft: Kb.Styles.globalMargins.large, paddingRight: Kb.Styles.globalMargins.xsmall, diff --git a/shared/chat/inbox/row/big-team-header.tsx b/shared/chat/inbox/row/big-team-header.tsx index 0fc4172f6497..d5359603db77 100644 --- a/shared/chat/inbox/row/big-team-header.tsx +++ b/shared/chat/inbox/row/big-team-header.tsx @@ -2,7 +2,6 @@ import * as C from '@/constants' import * as Chat from '@/stores/chat2' import * as Kb from '@/common-adapters' import * as Teams from '@/stores/teams' -import * as React from 'react' import * as RowSizes from './sizes' import type * as T from '@/constants/types' import TeamMenu from '@/chat/conversation/info-panel/menu' @@ -13,37 +12,34 @@ type Props = { teamID: T.Teams.TeamID } -const BigTeamHeader = React.memo(function BigTeamHeader(props: Props) { +const BigTeamHeader = (props: Props) => { return ( - + ) -}) -const BigTeamHeaderImpl = (props: Props) => { +} +const BigTeamHeaderInner = (props: Props) => { const {teamID, teamname} = props const badgeSubscribe = Teams.useTeamsState(s => !Teams.isTeamWithChosenChannels(s, teamname)) const navigateAppend = C.useRouterState(s => s.dispatch.navigateAppend) const onClick = () => navigateAppend({props: {teamID}, selected: 'team'}) - const makePopup = React.useCallback( - (p: Kb.Popup2Parms) => { - const {attachTo, hidePopup} = p - return ( - - - - ) - }, - [teamID] - ) + const makePopup = (p: Kb.Popup2Parms) => { + const {attachTo, hidePopup} = p + return ( + + + + ) + } const {showPopup, popup, popupAnchor} = Kb.usePopup2(makePopup) return ( @@ -97,9 +93,6 @@ const styles = Kb.Styles.styleSheetCreate( borderWidth: 2, }, showMenu: Kb.Styles.platformStyles({ - common: { - ...Kb.Styles.globalStyles.flexBoxRow, - }, isElectron: { alignSelf: 'center', position: 'relative', diff --git a/shared/chat/inbox/row/big-teams-divider.tsx b/shared/chat/inbox/row/big-teams-divider.tsx index 547f2e267670..944193379468 100644 --- a/shared/chat/inbox/row/big-teams-divider.tsx +++ b/shared/chat/inbox/row/big-teams-divider.tsx @@ -1,5 +1,4 @@ import * as Chat from '@/stores/chat2' -import * as React from 'react' import * as Kb from '@/common-adapters' import * as RowSizes from './sizes' import * as T from '@/constants/types' @@ -10,7 +9,7 @@ type Props = { onEdit?: () => void } -const BigTeamsDivider = React.memo(function BigTeamsDivider(props: Props) { +const BigTeamsDivider = (props: Props) => { const {toggle, onEdit} = props const badgeCount = Chat.useChatState(s => s.bigTeamBadgeCount) return ( @@ -29,9 +28,9 @@ const BigTeamsDivider = React.memo(function BigTeamsDivider(props: Props) { > {badgeCount > 0 && } - + - + {onEdit ? ( @@ -42,7 +41,7 @@ const BigTeamsDivider = React.memo(function BigTeamsDivider(props: Props) { ) -}) +} const styles = Kb.Styles.styleSheetCreate( () => @@ -94,8 +93,6 @@ const styles = Kb.Styles.styleSheetCreate( edit: {justifyContent: 'center'}, icon: { ...Kb.Styles.globalStyles.fillAbsolute, - ...Kb.Styles.globalStyles.flexBoxRow, - alignItems: 'flex-start', justifyContent: 'center', marginTop: Kb.Styles.isMobile ? Kb.Styles.globalMargins.tiny : Kb.Styles.globalMargins.xtiny, }, diff --git a/shared/chat/inbox/row/big-teams-label.tsx b/shared/chat/inbox/row/big-teams-label.tsx index 01346f052d5b..ff4d7ceec6ad 100644 --- a/shared/chat/inbox/row/big-teams-label.tsx +++ b/shared/chat/inbox/row/big-teams-label.tsx @@ -1,27 +1,28 @@ import * as Kb from '@/common-adapters' const BigTeamsLabel = () => ( - - + + Big teams - - + + ) const styles = Kb.Styles.styleSheetCreate(() => ({ bigTeamsLabelBox: { - ...Kb.Styles.globalStyles.flexBoxRow, - alignItems: 'center', minHeight: 24, }, container: { - ...Kb.Styles.globalStyles.flexBoxRow, - alignItems: 'center', height: Kb.Styles.isMobile ? 32 : 24, marginLeft: Kb.Styles.globalMargins.tiny, - } as const, + }, text: {backgroundColor: Kb.Styles.globalColors.fastBlank}, })) From 00610aec9787efa510b0ee9018e43dafd100c337 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Fri, 13 Feb 2026 15:54:39 -0500 Subject: [PATCH 5/5] WIP --- shared/chat/inbox/container.tsx | 194 ++++++++++---------------- shared/chat/inbox/defer-loading.tsx | 8 +- shared/chat/inbox/index.desktop.tsx | 18 --- shared/chat/inbox/index.native.tsx | 5 - shared/chat/inbox/new-chat-button.tsx | 64 ++++----- shared/chat/inbox/search-row.tsx | 76 +++++----- 6 files changed, 136 insertions(+), 229 deletions(-) diff --git a/shared/chat/inbox/container.tsx b/shared/chat/inbox/container.tsx index 1bd795aef1d2..09cc8d1e3397 100644 --- a/shared/chat/inbox/container.tsx +++ b/shared/chat/inbox/container.tsx @@ -2,7 +2,7 @@ import * as C from '@/constants' import * as Chat from '@/stores/chat2' import * as React from 'react' import * as T from '@/constants/types' -import Inbox, {type Props} from '.' +import Inbox from '.' import {useIsFocused} from '@react-navigation/core' import type { ChatInboxRowItemBig, @@ -64,19 +64,14 @@ const makeSmallRows = ( }) } -type WrapperProps = Pick< - Props, - | 'isSearching' - | 'navKey' - | 'neverLoaded' - | 'rows' - | 'smallTeamsExpanded' - | 'unreadIndices' - | 'unreadTotal' - | 'selectedConversationIDKey' -> - -const InboxWrapper = React.memo(function InboxWrapper(props: WrapperProps) { +const noSmallTeams = new Array() +const noBigTeams = new Array() +const emptyMap = new Map() + +const Connected = (ownProps: OwnProps) => { + const {navKey, conversationIDKey} = ownProps + const isFocused = useIsFocused() + const chatState = Chat.useChatState( C.useShallow(s => { const inboxNumSmallRows = s.inboxNumSmallRows ?? 5 @@ -87,38 +82,38 @@ const InboxWrapper = React.memo(function InboxWrapper(props: WrapperProps) { return { allowShowFloatingButton, inboxHasLoaded: s.inboxHasLoaded, + inboxLayout, inboxNumSmallRows, inboxRefresh: s.dispatch.inboxRefresh, + isSearching: !!s.inboxSearch, queueMetaToRequest: s.dispatch.queueMetaToRequest, setInboxNumSmallRows: s.dispatch.setInboxNumSmallRows, + smallTeamsExpanded: s.smallTeamsExpanded, toggleSmallTeamsExpanded: s.dispatch.toggleSmallTeamsExpanded, } }) ) - const {allowShowFloatingButton, inboxHasLoaded, inboxNumSmallRows, inboxRefresh} = chatState - const {queueMetaToRequest, setInboxNumSmallRows, toggleSmallTeamsExpanded} = chatState - const isFocused = useIsFocused() + const { + allowShowFloatingButton, inboxHasLoaded, inboxLayout, inboxNumSmallRows, + inboxRefresh, isSearching, queueMetaToRequest, setInboxNumSmallRows, + smallTeamsExpanded, toggleSmallTeamsExpanded, + } = chatState const appendNewChatBuilder = C.useRouterState(s => s.appendNewChatBuilder) - // a hack to have it check for marked as read when we mount as the focus events don't fire always - const onNewChat = appendNewChatBuilder - const onUntrustedInboxVisible = queueMetaToRequest - const [lastIsFocused, setLastIsFocused] = React.useState(isFocused) - - if (lastIsFocused !== isFocused) { - setLastIsFocused(isFocused) - if (C.isMobile) { - if (isFocused && Chat.isSplit) { - setTimeout(() => { - Chat.getConvoState(Chat.getSelectedConversation()).dispatch.tabSelected() - }, 0) - } + const selectedConversationIDKey = conversationIDKey ?? Chat.noConversationIDKey + + // Handle focus changes on mobile + const prevIsFocusedRef = React.useRef(isFocused) + React.useEffect(() => { + if (prevIsFocusedRef.current === isFocused) return + prevIsFocusedRef.current = isFocused + if (C.isMobile && isFocused && Chat.isSplit) { + Chat.getConvoState(Chat.getSelectedConversation()).dispatch.tabSelected() } - } + }, [isFocused]) C.useOnMountOnce(() => { if (!C.isMobile) { - // On mobile this is taken care of by NavigationEvents. Chat.getConvoState(Chat.getSelectedConversation()).dispatch.tabSelected() } if (!inboxHasLoaded) { @@ -134,89 +129,45 @@ const InboxWrapper = React.memo(function InboxWrapper(props: WrapperProps) { }, [inboxHasLoaded, inboxRefresh]) ) - return ( - - ) -}) - -const noSmallTeams = new Array() -const noBigTeams = new Array() -const Connected = (ownProps: OwnProps) => { - const chatState = Chat.useChatState( - C.useShallow(s => ({ - inboxHasLoaded: s.inboxHasLoaded, - inboxLayout: s.inboxLayout, - inboxNumSmallRows: s.inboxNumSmallRows ?? 5, - isSearching: !!s.inboxSearch, - smallTeamsExpanded: s.smallTeamsExpanded, - })) - ) - const {inboxHasLoaded, inboxLayout: _inboxLayout, inboxNumSmallRows} = chatState - const {isSearching, smallTeamsExpanded} = chatState - const {conversationIDKey} = ownProps - const neverLoaded = !inboxHasLoaded - const selectedConversationIDKey = conversationIDKey ?? Chat.noConversationIDKey - const {navKey} = ownProps - const bigTeams = _inboxLayout ? _inboxLayout.bigTeams || noBigTeams : noBigTeams + // Compute rows + const bigTeams = inboxLayout?.bigTeams || noBigTeams const showAllSmallRows = smallTeamsExpanded || !bigTeams.length - const allSmallTeams = _inboxLayout ? _inboxLayout.smallTeams || noSmallTeams : noSmallTeams + const allSmallTeams = inboxLayout?.smallTeams || noSmallTeams const smallTeamsBelowTheFold = !showAllSmallRows && allSmallTeams.length > inboxNumSmallRows - const smallTeams = React.useMemo(() => { - if (!showAllSmallRows) { - return allSmallTeams.slice(0, inboxNumSmallRows) - } else { - return allSmallTeams - } - }, [showAllSmallRows, inboxNumSmallRows, allSmallTeams]) - const smallRows = React.useMemo(() => { - return makeSmallRows(smallTeams) - }, [smallTeams]) - - const bigRows = React.useMemo(() => { - return makeBigRows(bigTeams) - }, [bigTeams]) + const smallTeams = showAllSmallRows ? allSmallTeams : allSmallTeams.slice(0, inboxNumSmallRows) + const smallRows = makeSmallRows(smallTeams) + const bigRows = makeBigRows(bigTeams) const hasAllSmallTeamConvs = - (_inboxLayout?.smallTeams?.length ?? 0) === (_inboxLayout?.totalSmallTeams ?? 0) - const divider: Array = React.useMemo(() => { - return bigRows.length !== 0 || !hasAllSmallTeamConvs + (inboxLayout?.smallTeams?.length ?? 0) === (inboxLayout?.totalSmallTeams ?? 0) + const divider: Array = + bigRows.length !== 0 || !hasAllSmallTeamConvs ? [{showButton: !hasAllSmallTeamConvs || smallTeamsBelowTheFold, type: 'divider'}] : [] - }, [bigRows.length, hasAllSmallTeamConvs, smallTeamsBelowTheFold]) - - const rows: Array = React.useMemo(() => { - const builderAfterSmall = new Array() - const builderAfterDivider = new Array() - const builderAfterBig = new Array() - const teamBuilder: ChatInboxRowItemTeamBuilder = {type: 'teamBuilder'} - if (smallRows.length !== 0) { - if (bigRows.length === 0) { - if (divider.length !== 0) { - builderAfterDivider.push(teamBuilder) - } else { - builderAfterSmall.push(teamBuilder) - } + + const teamBuilder: ChatInboxRowItemTeamBuilder = {type: 'teamBuilder'} + const builderAfterSmall: Array = [] + const builderAfterDivider: Array = [] + const builderAfterBig: Array = [] + if (smallRows.length !== 0) { + if (bigRows.length === 0) { + if (divider.length !== 0) { + builderAfterDivider.push(teamBuilder) } else { - builderAfterBig.push(teamBuilder) + builderAfterSmall.push(teamBuilder) } + } else { + builderAfterBig.push(teamBuilder) } - return [ - ...smallRows, - ...builderAfterSmall, - ...divider, - ...builderAfterDivider, - ...bigRows, - ...builderAfterBig, - ] - }, [bigRows, smallRows, divider]) + } + const rows: Array = [ + ...smallRows, + ...builderAfterSmall, + ...divider, + ...builderAfterDivider, + ...bigRows, + ...builderAfterBig, + ] const unreadIndices = Chat.useChatState( C.useShallow(s => @@ -233,19 +184,24 @@ const Connected = (ownProps: OwnProps) => { const unreadTotal = Array.from(unreadIndices.values()).reduce((acc, val) => acc + val, 0) - const props = { - isSearching, - navKey, - neverLoaded, - rows, - selectedConversationIDKey, - smallTeamsExpanded: smallTeamsExpanded || bigTeams.length === 0, - unreadIndices: unreadIndices.size ? unreadIndices : emptyMap, - unreadTotal, - } - return + return ( + + ) } -const emptyMap = new Map() - export default Connected diff --git a/shared/chat/inbox/defer-loading.tsx b/shared/chat/inbox/defer-loading.tsx index 7da11da3fbbd..2bf52ccb9668 100644 --- a/shared/chat/inbox/defer-loading.tsx +++ b/shared/chat/inbox/defer-loading.tsx @@ -5,7 +5,7 @@ import {useIsFocused, useNavigationState} from '@react-navigation/core' // keep track of this even on unmount, else if you background / foreground you'll lose it let _everFocused = false -const Deferred = React.memo(function Deferred() { +export default function Deferred() { const [visible, setVisible] = React.useState(_everFocused) const isFocused = useIsFocused() const navKey = useNavigationState(state => state.key) @@ -27,10 +27,4 @@ const Deferred = React.memo(function Deferred() { }, [isFocused, visible]) return visible ? : null -}) - -const DeferredOuter = () => { - return } - -export default DeferredOuter diff --git a/shared/chat/inbox/index.desktop.tsx b/shared/chat/inbox/index.desktop.tsx index c40b4875c1ea..2cc0d2efd57e 100644 --- a/shared/chat/inbox/index.desktop.tsx +++ b/shared/chat/inbox/index.desktop.tsx @@ -462,10 +462,6 @@ const styles = Kb.Styles.styleSheetCreate( position: 'relative', }, }), - divider: { - backgroundColor: 'purple', - overflow: 'hidden', - }, fakeAvatar: Kb.Styles.platformStyles({ isElectron: { backgroundColor: Kb.Styles.globalColors.black_10, @@ -482,11 +478,6 @@ const styles = Kb.Styles.styleSheetCreate( width: '100%', }, }), - fakeRemovingRowDivider: { - position: 'absolute', - top: 0, - width: '100%', - }, fakeRow: Kb.Styles.platformStyles({ isElectron: { backgroundColor: Kb.Styles.globalColors.blueGrey, @@ -502,11 +493,6 @@ const styles = Kb.Styles.styleSheetCreate( right: 0, zIndex: 9999, }, - fakeRowDivider: { - bottom: 0, - position: 'absolute', - width: '100%', - }, fakeText: { flexGrow: 1, height: '100%', @@ -554,14 +540,10 @@ const styles = Kb.Styles.styleSheetCreate( paddingTop: 1, width: Kb.Styles.globalMargins.small, }, - hover: {backgroundColor: Kb.Styles.globalColors.blueGreyDark}, list: { flex: 1, height: '100%', }, - rowWithDragger: { - height: 68, - }, spacer: { backgroundColor: Kb.Styles.globalColors.blueGrey, bottom: 0, diff --git a/shared/chat/inbox/index.native.tsx b/shared/chat/inbox/index.native.tsx index e1aa6aaf9ceb..42bf1c36d096 100644 --- a/shared/chat/inbox/index.native.tsx +++ b/shared/chat/inbox/index.native.tsx @@ -400,11 +400,6 @@ const styles = Kb.Styles.styleSheetCreate( () => ({ button: {width: '100%'}, - buttonBar: { - alignItems: 'flex-end', - alignSelf: 'flex-end', - justifyContent: 'flex-end', - }, container: Kb.Styles.platformStyles({ common: { ...Kb.Styles.globalStyles.flexBoxColumn, diff --git a/shared/chat/inbox/new-chat-button.tsx b/shared/chat/inbox/new-chat-button.tsx index bb086547ac38..62445e915d25 100644 --- a/shared/chat/inbox/new-chat-button.tsx +++ b/shared/chat/inbox/new-chat-button.tsx @@ -1,6 +1,5 @@ import * as C from '@/constants' import * as Chat from '@/stores/chat2' -import * as React from 'react' import * as Kb from '@/common-adapters' const HeaderNewChatButton = () => { @@ -12,45 +11,38 @@ const HeaderNewChatButton = () => { (s.inboxLayout.bigTeams || []).length === 0 ) - const appendNewChatBuilder = C.useRouterState(s => s.appendNewChatBuilder) - const onNewChat = React.useCallback(() => { - appendNewChatBuilder() - }, [appendNewChatBuilder]) - const content = React.useMemo(() => { - return ( - - - - - - - - + const onNewChat = C.useRouterState(s => s.appendNewChatBuilder) + + if (hide) return null + + return ( + + + + + + - ) - }, [onNewChat]) - return hide ? null : content + + + ) } const styles = Kb.Styles.styleSheetCreate( () => ({ - button: { - marginLeft: Kb.Styles.globalMargins.small, - marginRight: Kb.Styles.globalMargins.small, - }, gradientContainer: Kb.Styles.platformStyles({ isElectron: { height: '100%', @@ -83,10 +75,6 @@ const styles = Kb.Styles.styleSheetCreate( }, }), gradientYellow: {backgroundColor: '#FFF75A', flex: 1}, - newMeta: { - alignSelf: 'center', - marginRight: Kb.Styles.globalMargins.tiny, - }, rainbowButton: Kb.Styles.platformStyles({ common: { margin: 2, diff --git a/shared/chat/inbox/search-row.tsx b/shared/chat/inbox/search-row.tsx index ceedb67615a9..0ae7328faf2f 100644 --- a/shared/chat/inbox/search-row.tsx +++ b/shared/chat/inbox/search-row.tsx @@ -6,50 +6,40 @@ import StartNewChat from './row/start-new-chat' type OwnProps = {headerContext: 'chat-header' | 'inbox-header'} -export default React.memo(function InboxSearchRow(ownProps: OwnProps) { +export default function InboxSearchRow(ownProps: OwnProps) { const {headerContext} = ownProps - const hasLoadedEmptyInbox = Chat.useChatState( - s => - s.inboxHasLoaded && - !!s.inboxLayout && - (s.inboxLayout.smallTeams || []).length === 0 && - (s.inboxLayout.bigTeams || []).length === 0 + const chatState = Chat.useChatState( + C.useShallow(s => { + const hasLoadedEmptyInbox = + s.inboxHasLoaded && + !!s.inboxLayout && + (s.inboxLayout.smallTeams || []).length === 0 && + (s.inboxLayout.bigTeams || []).length === 0 + return { + inboxSearch: s.dispatch.inboxSearch, + inboxSearchMoveSelectedIndex: s.dispatch.inboxSearchMoveSelectedIndex, + inboxSearchSelect: s.dispatch.inboxSearchSelect, + isSearching: !!s.inboxSearch, + showEmptyInbox: !s.inboxSearch && hasLoadedEmptyInbox, + } + }) ) - const showEmptyInbox = Chat.useChatState(s => !s.inboxSearch && hasLoadedEmptyInbox) + const {inboxSearch, inboxSearchMoveSelectedIndex, inboxSearchSelect, isSearching, showEmptyInbox} = chatState const showStartNewChat = !C.isMobile && showEmptyInbox - const isSearching = Chat.useChatState(s => !!s.inboxSearch) const showFilter = !showEmptyInbox - const navigateUp = C.useRouterState(s => s.dispatch.navigateUp) - const onBack = React.useCallback(() => { - navigateUp() - }, [navigateUp]) + const routerState = C.useRouterState( + C.useShallow(s => ({ + appendNewChatBuilder: s.appendNewChatBuilder, + navigateUp: s.dispatch.navigateUp, + })) + ) const [query, setQuery] = React.useState('') - const inboxSearchSelect = Chat.useChatState(s => s.dispatch.inboxSearchSelect) - const inboxSearch = Chat.useChatState(s => s.dispatch.inboxSearch) - const inboxSearchMoveSelectedIndex = Chat.useChatState(s => s.dispatch.inboxSearchMoveSelectedIndex) - const onEnsureSelection = React.useCallback(() => { - inboxSearchSelect() - }, [inboxSearchSelect]) - - const appendNewChatBuilder = C.useRouterState(s => s.appendNewChatBuilder) - const onNewChat = React.useCallback(() => { - appendNewChatBuilder() - }, [appendNewChatBuilder]) - const onQueryChanged = React.useCallback( - (q: string) => { - setQuery(q) - inboxSearch(q) - }, - [inboxSearch] - ) - const onSelectDown = React.useCallback(() => { - inboxSearchMoveSelectedIndex(true) - }, [inboxSearchMoveSelectedIndex]) - const onSelectUp = React.useCallback(() => { - inboxSearchMoveSelectedIndex(false) - }, [inboxSearchMoveSelectedIndex]) + const onQueryChanged = (q: string) => { + setQuery(q) + inboxSearch(q) + } const [lastSearching, setLastSearching] = React.useState(isSearching) if (lastSearching !== isSearching) { @@ -64,12 +54,14 @@ export default React.memo(function InboxSearchRow(ownProps: OwnProps) { return ( <> - {!!showStartNewChat && } + {!!showStartNewChat && ( + + )} {!!showFilter && ( inboxSearchMoveSelectedIndex(false)} + onSelectDown={() => inboxSearchMoveSelectedIndex(true)} + onEnsureSelection={inboxSearchSelect} onQueryChanged={onQueryChanged} query={query} showNewChat={showNewChat} @@ -78,4 +70,4 @@ export default React.memo(function InboxSearchRow(ownProps: OwnProps) { )} ) -}) +}