From f6177de55a99fe3bccbbe43181df2bab79011b63 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Fri, 20 Feb 2026 15:39:26 -0500 Subject: [PATCH 1/7] simpler remote --- .../remote/component-loader.desktop.tsx | 56 ++- shared/desktop/remote/store.desktop.tsx | 52 --- .../remote/use-serialize-props.desktop.tsx | 32 +- shared/menubar/chat-container.desktop.tsx | 71 --- shared/menubar/files-container.desktop.tsx | 46 -- shared/menubar/index.desktop.tsx | 338 ++++++++++++--- shared/menubar/main2.desktop.tsx | 24 +- shared/menubar/remote-container.desktop.tsx | 112 ----- shared/menubar/remote-proxy.desktop.tsx | 88 +--- shared/menubar/remote-serializer.desktop.tsx | 273 ------------ shared/pinentry/main2.desktop.tsx | 44 +- shared/pinentry/remote-container.desktop.tsx | 29 -- shared/pinentry/remote-proxy.desktop.tsx | 4 +- shared/pinentry/remote-serializer.desktop.tsx | 57 --- shared/tracker/index.desktop.tsx | 406 ++++++++++++------ shared/tracker/main2.desktop.tsx | 58 ++- shared/tracker/remote-container.desktop.tsx | 150 ------- shared/tracker/remote-proxy.desktop.tsx | 43 +- shared/tracker/remote-serializer.desktop.tsx | 206 --------- shared/unlock-folders/main2.desktop.tsx | 71 ++- .../remote-container.desktop.tsx | 65 --- .../unlock-folders/remote-proxy.desktop.tsx | 4 +- .../remote-serializer.desktop.tsx | 45 -- 23 files changed, 816 insertions(+), 1458 deletions(-) delete mode 100644 shared/desktop/remote/store.desktop.tsx delete mode 100644 shared/menubar/chat-container.desktop.tsx delete mode 100644 shared/menubar/files-container.desktop.tsx delete mode 100644 shared/menubar/remote-container.desktop.tsx delete mode 100644 shared/menubar/remote-serializer.desktop.tsx delete mode 100644 shared/pinentry/remote-container.desktop.tsx delete mode 100644 shared/pinentry/remote-serializer.desktop.tsx delete mode 100644 shared/tracker/remote-container.desktop.tsx delete mode 100644 shared/tracker/remote-serializer.desktop.tsx delete mode 100644 shared/unlock-folders/remote-container.desktop.tsx delete mode 100644 shared/unlock-folders/remote-serializer.desktop.tsx diff --git a/shared/desktop/remote/component-loader.desktop.tsx b/shared/desktop/remote/component-loader.desktop.tsx index 0b8287c88646..1bab42c3971a 100644 --- a/shared/desktop/remote/component-loader.desktop.tsx +++ b/shared/desktop/remote/component-loader.desktop.tsx @@ -1,9 +1,9 @@ -// This loads up a remote component. It makes a pass-through store which accepts its props from the main window through ipc -// Also protects it with an error boundary +// Loads a remote component. Receives props from the main window via IPC. import * as React from 'react' import * as ReactDOM from 'react-dom/client' import * as Kb from '@/common-adapters' -import RemoteStore from './store.desktop' +import * as R from '@/constants/remote' +import * as RemoteGen from '@/actions/remote-gen' import {GlobalKeyEventHandler} from '@/common-adapters/key-event-handler.desktop' import {CanFixOverdrawContext} from '@/styles' import {disableDragDrop} from '@/util/drag-drop.desktop' @@ -15,45 +15,39 @@ import ServiceDecoration from '@/common-adapters/markdown/service-decoration' setServiceDecoration(ServiceDecoration) -const {closeWindow, showInactive} = KB2.functions +const {closeWindow, showInactive, ipcRendererOn} = KB2.functions disableDragDrop() module.hot?.accept() type RemoteComponents = 'unlock-folders' | 'menubar' | 'pinentry' | 'tracker' -type Props = { - child: (p: DeserializeProps) => React.ReactNode - deserialize: (state?: DeserializeProps, props?: Partial) => DeserializeProps +type Props

= { + child: (p: P) => React.ReactNode name: RemoteComponents params: string showOnProps: boolean style?: Kb.Styles.StylesCrossPlatform } -function RemoteComponentLoader(p: Props) { - const storeRef = React.useRef>(undefined) - const {deserialize, name, params, showOnProps} = p - const [value, setValue] = React.useState() +function RemoteComponentLoader

(p: Props

) { + const {name, params, showOnProps} = p + const [value, setValue] = React.useState

(null) React.useEffect(() => { - if (!storeRef.current) { - storeRef.current = new RemoteStore({ - deserialize, - gotPropsCallback: () => { - if (showOnProps) { - showInactive?.() - } - }, - onUpdated: v => { - setValue(v) - }, - windowComponent: name, - windowParam: params, - }) + ipcRendererOn?.('KBprops', (_event: unknown, raw: unknown) => { + setValue(JSON.parse(raw as string) as P) + }) + R.remoteDispatch( + RemoteGen.createRemoteWindowWantsProps({component: name, param: params}) + ) + }, [name, params]) + + React.useEffect(() => { + if (value && showOnProps) { + showInactive?.() } - setValue(storeRef.current._value) - }, [deserialize, name, params, showOnProps]) + }, [value, showOnProps]) if (!value) return null @@ -84,9 +78,8 @@ const styles = Kb.Styles.styleSheetCreate( }) as const ) -export default function Loader(options: { - child: (p: DeserializeProps) => React.ReactNode - deserialize: (state?: DeserializeProps, props?: Partial) => DeserializeProps +export default function Loader

(options: { + child: (p: P) => React.ReactNode name: RemoteComponents params?: string style?: Kb.Styles.StylesCrossPlatform @@ -96,12 +89,11 @@ export default function Loader(options: { const node = document.getElementById('root') if (node) { ReactDOM.createRoot(node).render( - + name={options.name} params={options.params || ''} style={options.style} showOnProps={options.showOnProps ?? true} - deserialize={options.deserialize} child={options.child} /> ) diff --git a/shared/desktop/remote/store.desktop.tsx b/shared/desktop/remote/store.desktop.tsx deleted file mode 100644 index 1a09bc53bffd..000000000000 --- a/shared/desktop/remote/store.desktop.tsx +++ /dev/null @@ -1,52 +0,0 @@ -// This is a helper for remote windows. -// This acts as a fake store for remote windows -// On the main window we plumb through our props and we 'mirror' the props using this helper -// We start up and send an action to the main window which then sends us 'props' -import * as R from '@/constants/remote' -import * as RemoteGen from '@/actions/remote-gen' -import KB2 from '@/util/electron.desktop' - -const {ipcRendererOn} = KB2.functions - -class RemoteStore { - _value: DeserializeProps - _gotPropsCallback: (() => void) | undefined // let component know it loaded once so it can show itself. Set to null after calling once - _deserialize: (state?: DeserializeProps, props?: Partial) => DeserializeProps - _onUpdated: (a: DeserializeProps) => void - - _registerForRemoteUpdate = () => { - ipcRendererOn?.('KBprops', (_event: unknown, action: unknown) => { - const old = this._value - this._value = this._deserialize(old, JSON.parse(action as string) as Partial) - if (this._gotPropsCallback) { - this._gotPropsCallback() - this._gotPropsCallback = undefined - } - this._onUpdated(this._value) - }) - } - - constructor(props: { - windowComponent: string - windowParam: string - gotPropsCallback: () => void - deserialize: (state?: DeserializeProps, props?: Partial) => DeserializeProps - onUpdated: (v: DeserializeProps) => void - }) { - this._onUpdated = props.onUpdated - this._value = props.deserialize(undefined, undefined) - this._deserialize = props.deserialize - this._gotPropsCallback = props.gotPropsCallback - this._registerForRemoteUpdate() - - // Search for the main window and ask it directly for our props - R.remoteDispatch( - RemoteGen.createRemoteWindowWantsProps({ - component: props.windowComponent, - param: props.windowParam, - }) - ) - } -} - -export default RemoteStore diff --git a/shared/desktop/remote/use-serialize-props.desktop.tsx b/shared/desktop/remote/use-serialize-props.desktop.tsx index 3327ea8f6428..0f6b5a1980f2 100644 --- a/shared/desktop/remote/use-serialize-props.desktop.tsx +++ b/shared/desktop/remote/use-serialize-props.desktop.tsx @@ -1,10 +1,8 @@ // This hook sends props to a remote window // Listens for requests from the main process (which proxies requests from other windows) to kick off an update -// If asked we'll send all props, otherwise we do a shallow compare and send the different ones import * as React from 'react' import * as C from '@/constants' import KB2 from '@/util/electron.desktop' -import isEqual from 'lodash/isEqual' import {useConfigState} from '@/stores/config' const {rendererNewProps} = KB2.functions @@ -15,36 +13,24 @@ if (debugSerializer) { console.log('\n\n\n\n\n\nDEBUGGING REMOTE SERIALIZER') } -export default function useSerializeProps( - p: ProxyProps, - serializer: (p: ProxyProps) => Partial, +export default function useSerializeProps

( + p: P, windowComponent: string, windowParam: string ) { - const lastSent = React.useRef>({}) + const lastSent = React.useRef('') const lastForceUpdate = React.useRef(-1) const currentForceUpdate = useConfigState( s => s.remoteWindowNeedsProps.get(windowComponent)?.get(windowParam) ?? 0 ) const throttledSend = C.useThrottledCallback( - (p: ProxyProps, forceUpdate: boolean) => { - const lastToSend: {[key: string]: unknown} = forceUpdate ? {} : lastSent.current - const serialized = serializer(p) - const toSend = {...serialized} as {[key: string]: unknown} - // clear undefineds / exact dupes - Object.keys(toSend).forEach(k => { - if (toSend[k] === undefined || isEqual(toSend[k], lastToSend[k])) { - delete toSend[k] // eslint-disable-line - } - }) - - if (Object.keys(toSend).length) { - const propsStr = JSON.stringify(toSend) - debugSerializer && console.log('[useSerializeProps]: throttled send', propsStr.length, toSend) - rendererNewProps?.({propsStr, windowComponent, windowParam}) - } - lastSent.current = serialized + (p: P, forceUpdate: boolean) => { + const propsStr = JSON.stringify(p) + if (!forceUpdate && propsStr === lastSent.current) return + debugSerializer && console.log('[useSerializeProps]: throttled send', propsStr.length) + rendererNewProps?.({propsStr, windowComponent, windowParam}) + lastSent.current = propsStr lastForceUpdate.current = currentForceUpdate }, 1000, diff --git a/shared/menubar/chat-container.desktop.tsx b/shared/menubar/chat-container.desktop.tsx deleted file mode 100644 index f3bf24623b7e..000000000000 --- a/shared/menubar/chat-container.desktop.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import * as React from 'react' -import * as C from '@/constants' -import * as Chat from '@/stores/chat' -import * as R from '@/constants/remote' -import * as RemoteGen from '../actions/remote-gen' -import * as Kb from '@/common-adapters' -import type * as T from '@/constants/types' -import type {DeserializeProps} from './remote-serializer.desktop' -import {SmallTeam} from '../chat/inbox/row/small-team' - -type RowProps = Pick & { - conversationIDKey: T.Chat.ConversationIDKey -} - -const RemoteSmallTeam = (props: RowProps) => { - const {conversationsToSend, conversationIDKey} = props - const conversation = conversationsToSend.find(c => c.conversationIDKey === conversationIDKey) - const onSelectConversation = () => { - R.remoteDispatch(RemoteGen.createOpenChatFromWidget({conversationIDKey})) - } - - return ( - - ) -} - -const ChatPreview = (p: Pick & {convLimit?: number}) => { - const {conversationsToSend, convLimit} = p - const convRows = conversationsToSend - .slice(0, convLimit ? convLimit : conversationsToSend.length) - .map(c => c.conversationIDKey) - - const openInbox = React.useCallback(() => { - R.remoteDispatch(RemoteGen.createShowMain()) - R.remoteDispatch(RemoteGen.createSwitchTab({tab: C.Tabs.chatTab})) - }, []) - - return ( - - {convRows.map(id => ( - - - - ))} - - - - - ) -} - -const styles = Kb.Styles.styleSheetCreate(() => ({ - buttonContainer: { - marginBottom: Kb.Styles.globalMargins.tiny, - marginTop: Kb.Styles.globalMargins.tiny, - }, - chatContainer: { - backgroundColor: Kb.Styles.globalColors.white, - color: Kb.Styles.globalColors.black, - }, -})) - -export default ChatPreview diff --git a/shared/menubar/files-container.desktop.tsx b/shared/menubar/files-container.desktop.tsx deleted file mode 100644 index aea7e75c0361..000000000000 --- a/shared/menubar/files-container.desktop.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import {useProfileState} from '@/stores/profile' -import * as R from '@/constants/remote' -import * as T from '@/constants/types' -import * as RemoteGen from '../actions/remote-gen' -import * as FsUtil from '@/util/kbfs' -import * as TimestampUtil from '@/util/timestamp' -import {FilesPreview} from './files.desktop' -import type {DeserializeProps} from './remote-serializer.desktop' -import {useCurrentUserState} from '@/stores/current-user' - -const FilesContainer = (p: Pick) => { - const {remoteTlfUpdates} = p - const username = useCurrentUserState(s => s.username) - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) - return ( - { - const tlf = T.FS.pathToString(c.tlf) - const {participants, teamname} = FsUtil.tlfToParticipantsOrTeamname(tlf) - const tlfType = T.FS.getPathVisibility(c.tlf) || T.FS.TlfType.Private - return { - onClickAvatar: () => showUserProfile(c.writer), - onSelectPath: () => c.tlf && R.remoteDispatch(RemoteGen.createOpenFilesFromWidget({path: c.tlf})), - participants: participants || [], - path: c.tlf, - teamname: teamname || '', - timestamp: TimestampUtil.formatTimeForConversationList(c.timestamp), - tlf, - // Default to private visibility--this should never happen though. - tlfType, - updates: c.updates.map(({path, uploading}) => { - return { - onClick: () => path && R.remoteDispatch(RemoteGen.createOpenFilesFromWidget({path})), - path, - tlfType, - uploading, - } - }), - username, - writer: c.writer, - } - })} - /> - ) -} -export default FilesContainer diff --git a/shared/menubar/index.desktop.tsx b/shared/menubar/index.desktop.tsx index c81b75b075ee..4edd56295c83 100644 --- a/shared/menubar/index.desktop.tsx +++ b/shared/menubar/index.desktop.tsx @@ -4,8 +4,8 @@ import * as T from '@/constants/types' import * as Kb from '@/common-adapters' import * as React from 'react' import * as RemoteGen from '@/actions/remote-gen' -import ChatContainer from './chat-container.desktop' -import FilesPreview from './files-container.desktop' +import * as FsUtil from '@/util/kbfs' +import * as TimestampUtil from '@/util/timestamp' import KB2 from '@/util/electron.desktop' import OutOfDate from './out-of-date' import Upload from '@/fs/footer/upload' @@ -14,29 +14,72 @@ import {Loading} from '@/fs/simple-screens' import {isLinux, isDarwin} from '@/constants/platform' import {type _InnerMenuItem} from '@/common-adapters/floating-menu/menu-layout' import {useUploadCountdown} from '@/fs/footer/use-upload-countdown' -import type {DeserializeProps} from './remote-serializer.desktop' import {useColorScheme} from 'react-native' const {hideWindow, ctlQuit} = KB2.functions -export type Props = Pick & { +export type Conversation = { + conversationIDKey: string + teamType?: T.Chat.TeamType + tlfname?: string + teamname?: string + timestamp?: number + channelname?: string + snippetDecorated?: string + hasBadge?: true + hasUnread?: true + participants?: Array +} + +export type RemoteTlfUpdates = { + timestamp: number + tlf: T.FS.Path + updates: Array<{path: T.FS.Path; uploading: boolean}> + writer: string +} + +type KbfsDaemonStatus = { + readonly rpcStatus: T.FS.KbfsDaemonRpcStatus + readonly onlineStatus: T.FS.KbfsDaemonOnlineStatus +} + +export type Props = { + conversationsToSend: ReadonlyArray daemonHandshakeState: T.Config.DaemonHandshakeState diskSpaceStatus: T.FS.DiskSpaceStatus - loggedIn: boolean - kbfsDaemonStatus: T.FS.KbfsDaemonStatus + endEstimate?: number + fileName?: string + files: number + httpSrvAddress: string + httpSrvToken: string + kbfsDaemonStatus: KbfsDaemonStatus kbfsEnabled: boolean + loggedIn: boolean + navBadges: {[tab: string]: number} outOfDate: T.Config.OutOfDate + remoteTlfUpdates: ReadonlyArray showingDiskSpaceBanner: boolean + totalSyncingBytes: number username: string - navBadges: ReadonlyMap windowShownCount: number + darkMode: boolean +} - // UploadCountdownHOCProps - endEstimate?: number - files: number - fileName?: string - totalSyncingBytes: number +// Simple avatar via httpSrv +const HttpAvatar = (p: { + name: string + isTeam?: boolean + size: number + httpSrvAddress: string + httpSrvToken: string + style?: React.CSSProperties +}) => { + const isDarkMode = useColorScheme() === 'dark' + const typ = p.isTeam ? 'team' : 'user' + const src = `http://${p.httpSrvAddress}/av?typ=${typ}&name=${p.name}&format=square_${p.size}&mode=${isDarkMode ? 'dark' : 'light'}&token=${p.httpSrvToken}` + return } +const avatarStyle: React.CSSProperties = {borderRadius: '50%', flexShrink: 0} const ArrowTick = () => { const isDarkMode = useColorScheme() === 'dark' @@ -50,6 +93,7 @@ const ArrowTick = () => { /> ) } + type UWCDProps = { endEstimate?: number files: number @@ -60,24 +104,154 @@ type UWCDProps = { } const UploadWithCountdown = (p: UWCDProps) => { const {endEstimate, files, fileName, totalSyncingBytes, isOnline, smallMode} = p + const np = useUploadCountdown({endEstimate, fileName, files, isOnline, smallMode, totalSyncingBytes}) + return +} - const np = useUploadCountdown({ - endEstimate, - fileName, - files, - isOnline, - smallMode, - totalSyncingBytes, - }) +// Inline chat row (replaces SmallTeam + ChatProvider) +const ChatRow = (p: {conv: Conversation; httpSrvAddress: string; httpSrvToken: string; username: string}) => { + const {conv, httpSrvAddress, httpSrvToken, username} = p + const isTeam = conv.teamType !== 'adhoc' + const name = isTeam ? conv.tlfname || '' : conv.participants?.filter(u => u !== username).join(', ') || conv.tlfname || '' + const avatarName = isTeam ? conv.tlfname || '' : conv.participants?.find(u => u !== username) || '' - return + return ( + R.remoteDispatch(RemoteGen.createOpenChatFromWidget({conversationIDKey: conv.conversationIDKey}))} + style={styles.chatRow} + > + + + + + + {isTeam && conv.channelname ? `${name}#${conv.channelname}` : name} + + {conv.hasBadge && } + + {!!conv.snippetDecorated && ( + + {conv.snippetDecorated} + + )} + + + + ) +} + +const ChatPreview = (p: {conversationsToSend: ReadonlyArray; convLimit?: number; httpSrvAddress: string; httpSrvToken: string; username: string}) => { + const {conversationsToSend, convLimit, httpSrvAddress, httpSrvToken, username} = p + const convs = conversationsToSend.slice(0, convLimit ?? conversationsToSend.length) + + const openInbox = React.useCallback(() => { + R.remoteDispatch(RemoteGen.createShowMain()) + R.remoteDispatch(RemoteGen.createSwitchTab({tab: C.Tabs.chatTab})) + }, []) + + return ( + + {convs.map(c => ( + + ))} + + + + + ) +} + +// Inline file updates (replaces FilesContainer + files.desktop.tsx with store-connected components) +const FileUpdate = (p: {path: T.FS.Path; uploading: boolean; onClick: () => void}) => ( + + + + {p.uploading && ( + + + + )} + + {T.FS.getPathName(p.path)} + + + +) + +const FilesPreview = (p: {remoteTlfUpdates: ReadonlyArray; httpSrvAddress: string; httpSrvToken: string}) => { + const {remoteTlfUpdates, httpSrvAddress, httpSrvToken} = p + return ( + + + + Recent files + + + + {remoteTlfUpdates.map(update => { + const tlf = T.FS.pathToString(update.tlf) + const {participants, teamname} = FsUtil.tlfToParticipantsOrTeamname(tlf) + const tlfType = T.FS.getPathVisibility(update.tlf) || T.FS.TlfType.Private + return ( + + + + + {update.writer} + + {TimestampUtil.formatTimeForConversationList(update.timestamp)} + + + + in  + update.tlf && R.remoteDispatch(RemoteGen.createOpenFilesFromWidget({path: update.tlf}))} + > + {tlfType === T.FS.TlfType.Team + ? teamname + : tlfType === T.FS.TlfType.Public + ? `${(participants || []).join(',')} (public)` + : (participants || []).join(',')} + + + + {update.updates.map(u => ( + u.path && R.remoteDispatch(RemoteGen.createOpenFilesFromWidget({path: u.path}))} + /> + ))} + + + + ) + })} + + + ) } const useMenuItems = ( p: Props & {showBadges?: boolean; openApp: (tab?: C.Tabs.AppTab) => void} ): ReadonlyArray<_InnerMenuItem> => { const {showBadges, navBadges, daemonHandshakeState, username, kbfsEnabled, openApp} = p - const countMap = navBadges const startingUp = daemonHandshakeState !== 'done' const ret = React.useMemo(() => { @@ -87,9 +261,7 @@ const useMenuItems = ( onClick: () => { const version = __VERSION__ openUrl( - `https://github.com/keybase/client/issues/new?body=Keybase%20GUI%20Version:%20${encodeURIComponent( - version - )}` + `https://github.com/keybase/client/issues/new?body=Keybase%20GUI%20Version:%20${encodeURIComponent(version)}` ) }, title: 'Report a bug', @@ -110,7 +282,6 @@ const useMenuItems = ( R.remoteDispatch(RemoteGen.createDumpLogs({reason: 'quitting through menu'})) } } - // In case dump log doesn't exit for us hideWindow?.() setTimeout(() => { ctlQuit?.() @@ -131,29 +302,17 @@ const useMenuItems = ( { onClick: () => openApp(C.Tabs.gitTab), title: 'Git', - view: , + view: , }, { onClick: () => openApp(C.Tabs.devicesTab), title: 'Devices', - view: ( - - ), + view: , }, { onClick: () => openApp(C.Tabs.settingsTab), title: 'Settings', - view: ( - - ), + view: , }, 'Divider' as const, ...openAppItem, @@ -172,7 +331,7 @@ const useMenuItems = ( ] as const } return [...openAppItem, ...common] as const - }, [username, countMap, kbfsEnabled, openApp, showBadges, startingUp]) + }, [username, navBadges, kbfsEnabled, openApp, showBadges, startingUp]) return ret } @@ -203,7 +362,7 @@ const IconBar = (p: Props & {showBadges?: boolean}) => { ) const {showPopup, popup, popupAnchor} = Kb.usePopup2(makePopup) - const badgeCountInMenu = badgesInMenu.reduce((acc, val) => navBadges.get(val) ?? 0 + acc, 0) + const badgeCountInMenu = badgesInMenu.reduce((acc, val) => (navBadges[val] ?? 0) + acc, 0) const isDarkMode = useColorScheme() === 'dark' return ( { const {endEstimate, files, kbfsDaemonStatus, totalSyncingBytes, fileName} = p const {outOfDate, windowShownCount, conversationsToSend, remoteTlfUpdates} = p + const {httpSrvAddress, httpSrvToken, username} = p const refreshUserFileEdits = C.useThrottledCallback(() => { R.remoteDispatch(RemoteGen.createUserFileEditsLoad()) @@ -265,9 +425,19 @@ const LoggedIn = (p: Props) => { <> - + {kbfsDaemonStatus.rpcStatus === T.FS.KbfsDaemonRpcStatus.Connected ? ( - + ) : ( @@ -299,10 +469,8 @@ const LoggedOut = (p: {daemonHandshakeState: T.Config.DaemonHandshakeState; logg ? 'Connecting interface to crypto engine... This may take a few seconds.' : 'Starting up Keybase...' - const navigateAppend = C.useRouterState(s => s.dispatch.navigateAppend) const logIn = () => { R.remoteDispatch(RemoteGen.createShowMain()) - navigateAppend(C.Tabs.loginTab) } return ( <> @@ -383,9 +551,9 @@ const iconMap = { type Tabs = (typeof badgeTypesInHeader)[number] | (typeof badgesInMenu)[number] -const BadgeIcon = (p: {tab: Tabs; countMap: ReadonlyMap; openApp: (t: Tabs) => void}) => { +const BadgeIcon = (p: {tab: Tabs; countMap: {[tab: string]: number}; openApp: (t: Tabs) => void}) => { const {tab, countMap, openApp} = p - const count = countMap.get(tab) + const count = countMap[tab] const iconType = iconMap[tab] const isDarkMode = useColorScheme() === 'dark' @@ -435,6 +603,56 @@ const styles = Kb.Styles.styleSheetCreate(() => ({ right: -2, top: -4, }, + buttonContainer: { + marginBottom: Kb.Styles.globalMargins.tiny, + marginTop: Kb.Styles.globalMargins.tiny, + }, + chatBadge: { + backgroundColor: Kb.Styles.globalColors.blue, + borderRadius: 4, + height: 8, + marginLeft: Kb.Styles.globalMargins.xtiny, + width: 8, + }, + chatContainer: { + backgroundColor: Kb.Styles.globalColors.white, + color: Kb.Styles.globalColors.black, + }, + chatRow: Kb.Styles.platformStyles({ + isElectron: { + ...Kb.Styles.desktopStyles.clickable, + }, + }), + chatRowInner: { + alignItems: 'center', + paddingBottom: Kb.Styles.globalMargins.xtiny, + paddingLeft: Kb.Styles.globalMargins.tiny, + paddingRight: Kb.Styles.globalMargins.tiny, + paddingTop: Kb.Styles.globalMargins.xtiny, + }, + chatRowName: {flexShrink: 1}, + chatRowNameContainer: {alignItems: 'center'}, + chatRowText: {flexGrow: 1, flexShrink: 1, overflow: 'hidden'}, + chatSnippet: {color: Kb.Styles.globalColors.black_50}, + fileFullWidth: {width: '100%'}, + fileIcon: { + flexShrink: 0, + height: 16, + marginRight: Kb.Styles.globalMargins.xtiny, + position: 'relative', + top: 1, + width: 16, + }, + fileIconBadge: {height: 12, width: 12}, + fileIconBadgeBox: {marginLeft: -12, marginRight: 12, marginTop: 12, width: 0, zIndex: 100}, + fileName: Kb.Styles.platformStyles({ + common: {flexShrink: 1}, + isElectron: {wordBreak: 'break-all'}, + }), + fileUpdateRow: { + marginTop: Kb.Styles.globalMargins.xtiny, + paddingRight: Kb.Styles.globalMargins.large, + }, flexOne: {flexGrow: 1}, footer: {width: 360}, headerBadgesContainer: { @@ -447,6 +665,28 @@ const styles = Kb.Styles.styleSheetCreate(() => ({ marginBottom: 12, }, navIcons: {paddingLeft: Kb.Styles.globalMargins.xtiny, paddingRight: Kb.Styles.globalMargins.xtiny}, + tlfContainer: { + backgroundColor: Kb.Styles.globalColors.white, + color: Kb.Styles.globalColors.black, + paddingBottom: Kb.Styles.globalMargins.tiny, + paddingTop: Kb.Styles.globalMargins.tiny, + }, + tlfParticipants: {fontSize: 12}, + tlfRowContainer: { + paddingBottom: Kb.Styles.globalMargins.tiny, + paddingLeft: Kb.Styles.globalMargins.tiny, + paddingTop: Kb.Styles.globalMargins.tiny, + }, + tlfSectionHeader: { + backgroundColor: Kb.Styles.globalColors.blueGrey, + color: Kb.Styles.globalColors.black_50, + paddingBottom: Kb.Styles.globalMargins.xtiny, + paddingLeft: Kb.Styles.globalMargins.tiny, + paddingTop: Kb.Styles.globalMargins.xtiny, + }, + tlfSectionHeaderContainer: {backgroundColor: Kb.Styles.globalColors.white}, + tlfTime: {marginRight: Kb.Styles.globalMargins.tiny}, + tlfTopLine: {justifyContent: 'space-between'}, topRow: { borderTopLeftRadius: Kb.Styles.globalMargins.xtiny, borderTopRightRadius: Kb.Styles.globalMargins.xtiny, diff --git a/shared/menubar/main2.desktop.tsx b/shared/menubar/main2.desktop.tsx index 9c64b6c145b9..dfd5c442cbc2 100644 --- a/shared/menubar/main2.desktop.tsx +++ b/shared/menubar/main2.desktop.tsx @@ -1,7 +1,18 @@ -import Menubar from './remote-container.desktop' +import * as React from 'react' import * as Kb from '@/common-adapters' +import Menubar from './index.desktop' import load from '../desktop/remote/component-loader.desktop' -import {deserialize, type SerializeProps, type DeserializeProps} from './remote-serializer.desktop' +import {useDarkModeState} from '@/stores/darkmode' +import type {Props} from './index.desktop' + +const DarkModeSync = ({darkMode, children}: {darkMode: boolean; children: React.ReactNode}) => { + const setSystemDarkMode = useDarkModeState(s => s.dispatch.setSystemDarkMode) + React.useEffect(() => { + const id = setTimeout(() => setSystemDarkMode(darkMode), 1) + return () => clearTimeout(id) + }, [setSystemDarkMode, darkMode]) + return <>{children} +} // This is to keep that arrow and gap on top w/ transparency const style = { @@ -13,9 +24,12 @@ const style = { position: 'relative', } as const -load({ - child: (p: DeserializeProps) => , - deserialize, +load({ + child: (p: Props) => ( + + + + ), name: 'menubar', showOnProps: false, style, diff --git a/shared/menubar/remote-container.desktop.tsx b/shared/menubar/remote-container.desktop.tsx deleted file mode 100644 index c21881678631..000000000000 --- a/shared/menubar/remote-container.desktop.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import * as React from 'react' -import * as Chat from '@/stores/chat' -import Menubar from './index.desktop' -import {useConfigState} from '@/stores/config' -import type {DeserializeProps} from './remote-serializer.desktop' -import {useAvatarState} from '@/common-adapters/avatar/store' -import {useUsersState} from '@/stores/users' -import {useFollowerState} from '@/stores/followers' -import {useCurrentUserState} from '@/stores/current-user' -import {useDaemonState} from '@/stores/daemon' -import {useDarkModeState} from '@/stores/darkmode' - -const RemoteContainer = (d: DeserializeProps) => { - const {avatarRefreshCounter, badgeMap, daemonHandshakeState, darkMode, diskSpaceStatus, endEstimate} = d - const {fileName, files, followers, following, httpSrvAddress, httpSrvToken, infoMap, remoteTlfUpdates} = d - const {kbfsDaemonStatus, kbfsEnabled, loggedIn, metaMap, navBadges, outOfDate, conversationsToSend} = d - const {showingDiskSpaceBanner, totalSyncingBytes, unreadMap, username, windowShownCountNum} = d - useAvatarState(s => s.dispatch.replace)(avatarRefreshCounter) - useDaemonState(s => s.dispatch.setState)(daemonHandshakeState) - useFollowerState(s => s.dispatch.replace)(followers, following) - useUsersState(s => s.dispatch.replace)(infoMap) - const replaceUsername = useCurrentUserState(s => s.dispatch.replaceUsername) - const setHTTPSrvInfo = useConfigState(s => s.dispatch.setHTTPSrvInfo) - const setOutOfDate = useConfigState(s => s.dispatch.setOutOfDate) - const setLoggedIn = useConfigState(s => s.dispatch.setLoggedIn) - const setSystemDarkMode = useDarkModeState(s => s.dispatch.setSystemDarkMode) - - // defer this so we don't update while rendering - React.useEffect(() => { - const id = setTimeout(() => { - setSystemDarkMode(darkMode) - }, 1) - return () => { - clearTimeout(id) - } - }, [setSystemDarkMode, darkMode]) - - React.useEffect(() => { - const id = setTimeout(() => { - replaceUsername(username) - }, 1) - return () => { - clearTimeout(id) - } - }, [replaceUsername, username]) - - React.useEffect(() => { - const id = setTimeout(() => { - setHTTPSrvInfo(httpSrvAddress, httpSrvToken) - }, 1) - return () => { - clearTimeout(id) - } - }, [setHTTPSrvInfo, httpSrvAddress, httpSrvToken]) - - React.useEffect(() => { - const id = setTimeout(() => { - setOutOfDate(outOfDate) - }, 1) - return () => { - clearTimeout(id) - } - }, [setOutOfDate, outOfDate]) - - React.useEffect(() => { - const id = setTimeout(() => { - setLoggedIn(loggedIn, false, true) - }, 1) - return () => { - clearTimeout(id) - } - }, [setLoggedIn, loggedIn]) - - React.useEffect(() => { - const id = setTimeout(() => { - for (const [id, unread] of unreadMap) { - Chat.getConvoState(id).dispatch.unreadUpdated(unread) - } - for (const [id, badge] of badgeMap) { - Chat.getConvoState(id).dispatch.badgesUpdated(badge) - } - for (const [id, next] of metaMap) { - Chat.getConvoState(id).dispatch.updateMeta(next) - } - }, 1) - return () => { - clearTimeout(id) - } - }, [unreadMap, badgeMap, metaMap]) - - return ( - - ) -} -export default RemoteContainer diff --git a/shared/menubar/remote-proxy.desktop.tsx b/shared/menubar/remote-proxy.desktop.tsx index 283a08e783d6..bff9633ed8e8 100644 --- a/shared/menubar/remote-proxy.desktop.tsx +++ b/shared/menubar/remote-proxy.desktop.tsx @@ -6,20 +6,15 @@ import * as T from '@/constants/types' import * as React from 'react' import KB2 from '@/util/electron.desktop' import useSerializeProps from '../desktop/remote/use-serialize-props.desktop' -import {intersect} from '@/util/set' -import {mapFilterByKey} from '@/util/map' -import {serialize, type ProxyProps, type RemoteTlfUpdates} from './remote-serializer.desktop' -import {useAvatarState} from '@/common-adapters/avatar/store' -import type * as NotifConstants from '@/stores/notifications' +import type {Props, Conversation, RemoteTlfUpdates} from './index.desktop' import {useColorScheme} from 'react-native' import * as FS from '@/stores/fs' import {useFSState} from '@/stores/fs' -import {useFollowerState} from '@/stores/followers' -import {useUsersState} from '@/stores/users' -import {useNotifState} from '@/stores/notifications' import {useCurrentUserState} from '@/stores/current-user' import {useDaemonState} from '@/stores/daemon' import {useDarkModeState} from '@/stores/darkmode' +import {useNotifState} from '@/stores/notifications' +import type * as NotifConstants from '@/stores/notifications' const {showTray} = KB2.functions @@ -36,13 +31,13 @@ function useWidgetBrowserWindow(p: WidgetProps) { }, [widgetBadge, desktopAppBadgeCount, systemDarkMode]) } -const Widget = (p: ProxyProps & WidgetProps) => { +const Widget = (p: Props & WidgetProps) => { const windowComponent = 'menubar' const windowParam = 'menubar' const {desktopAppBadgeCount, widgetBadge, ...toSend} = p useWidgetBrowserWindow({desktopAppBadgeCount, widgetBadge}) - useSerializeProps(toSend, serialize, windowComponent, windowParam) + useSerializeProps(toSend, windowComponent, windowParam) return null } @@ -82,16 +77,7 @@ const convoDiff = (a: Chat.ConvoState, b: Chat.ConvoState) => { return false } -const usernamesCache = new Map>() -// TODO could make this render less const MenubarRemoteProxy = React.memo(function MenubarRemoteProxy() { - const followerState = useFollowerState( - C.useShallow(s => { - const {followers, following} = s - return {followers, following} - }) - ) - const {following, followers} = followerState const username = useCurrentUserState(s => s.username) const configState = useConfigState( C.useShallow(s => { @@ -107,13 +93,12 @@ const MenubarRemoteProxy = React.memo(function MenubarRemoteProxy() { }) ) const {kbfsDaemonStatus, overallSyncStatus, pathItems, sfmi, tlfUpdates, uploads} = fsState - const {desktopAppBadgeCount, navBadges, widgetBadge} = useNotifState( + const {desktopAppBadgeCount, navBadges: navBadgesMap, widgetBadge} = useNotifState( C.useShallow(s => { const {desktopAppBadgeCount, navBadges, widgetBadge} = s return {desktopAppBadgeCount, navBadges, widgetBadge} }) ) - const infoMap = useUsersState(s => s.infoMap) const widgetList = Chat.useChatState(s => s.inboxLayout?.widgetList) const isDarkMode = useColorScheme() === 'dark' const {diskSpaceStatus, showingBanner} = overallSyncStatus @@ -124,10 +109,6 @@ const MenubarRemoteProxy = React.memo(function MenubarRemoteProxy() { [tlfUpdates, uploads] ) - // could handle this in a different way later but here we need to subscribe to all the convoStates - // normally we'd have a list and these would all subscribe within the component but this proxy isn't - // setup that way so instead we manually subscribe to all the substores and increment when a meta - // changes inside const [remakeChat, setRemakeChat] = React.useState(0) React.useEffect(() => { const unsubs = widgetList?.map(v => { @@ -145,7 +126,7 @@ const MenubarRemoteProxy = React.memo(function MenubarRemoteProxy() { } }, [widgetList]) - const conversationsToSend = React.useMemo( + const conversationsToSend: ReadonlyArray = React.useMemo( () => widgetList?.map(v => { remakeChat // implied dependency @@ -166,51 +147,15 @@ const MenubarRemoteProxy = React.memo(function MenubarRemoteProxy() { [widgetList, remakeChat] ) - // filter some data based on visible users - const usernames = React.useMemo(() => { - const _usernames = new Set() - tlfUpdates.forEach(update => _usernames.add(update.writer)) - conversationsToSend.forEach(c => { - if (c.teamType === 'adhoc') { - c.participants?.forEach(p => _usernames.add(p)) - } else { - c.tlfname && _usernames.add(c.tlfname) - } - }) - - const usernames = (() => { - const key = Array.from(_usernames).join(',') - const existing = usernamesCache.get(key) - if (existing) return existing - usernamesCache.set(key, _usernames) - return _usernames - })() - return usernames - }, [conversationsToSend, tlfUpdates]) - - const avatarRefreshCounter = useAvatarState(s => s.counts) - - const avatarRefreshCounterFiltered = React.useMemo( - () => mapFilterByKey(avatarRefreshCounter, usernames), - [avatarRefreshCounter, usernames] - ) - const followersFiltered = React.useMemo(() => intersect(followers, usernames), [followers, usernames]) - const followingFiltered = React.useMemo(() => intersect(following, usernames), [following, usernames]) - const infoMapFiltered = React.useMemo(() => mapFilterByKey(infoMap, usernames), [infoMap, usernames]) - + // Filter some data based on visible users. // We just use syncingPaths rather than merging with writingToJournal here // since journal status comes a bit slower, and merging the two causes // flakes on our perception of overall upload status. - - // Filter out folder paths. const filePaths = [...uploads.syncingPaths].filter( path => FS.getPathItem(pathItems, path).type !== T.FS.PathType.Folder ) const upDown = { - // We just use syncingPaths rather than merging with writingToJournal here - // since journal status comes a bit slower, and merging the two causes - // flakes on our perception of overall upload status. endEstimate: uploads.endEstimate ?? 0, filename: T.FS.getPathName(filePaths[1] || T.FS.stringToPath('')), files: filePaths.length, @@ -219,19 +164,24 @@ const MenubarRemoteProxy = React.memo(function MenubarRemoteProxy() { const daemonHandshakeState = useDaemonState(s => s.handshakeState) - const p: ProxyProps & WidgetProps = { + // Convert navBadges Map to plain object + const navBadges: {[tab: string]: number} = React.useMemo(() => { + const obj: {[tab: string]: number} = {} + for (const [k, v] of navBadgesMap) { + obj[k] = v + } + return obj + }, [navBadgesMap]) + + const p: Props & WidgetProps = { ...upDown, - avatarRefreshCounter: avatarRefreshCounterFiltered, conversationsToSend, daemonHandshakeState, darkMode: isDarkMode, desktopAppBadgeCount, diskSpaceStatus, - followers: followersFiltered, - following: followingFiltered, httpSrvAddress: httpSrv.address, httpSrvToken: httpSrv.token, - infoMap: infoMapFiltered, kbfsDaemonStatus, kbfsEnabled, loggedIn, @@ -241,7 +191,7 @@ const MenubarRemoteProxy = React.memo(function MenubarRemoteProxy() { showingDiskSpaceBanner: showingBanner, username, widgetBadge, - windowShownCountNum: windowShownCount.get('menu') ?? 0, + windowShownCount: windowShownCount.get('menu') ?? 0, } return diff --git a/shared/menubar/remote-serializer.desktop.tsx b/shared/menubar/remote-serializer.desktop.tsx deleted file mode 100644 index ed68603a06c0..000000000000 --- a/shared/menubar/remote-serializer.desktop.tsx +++ /dev/null @@ -1,273 +0,0 @@ -import type * as C from '@/constants' -import * as T from '@/constants/types' -import {produce} from 'immer' - -const emptySet = new Set() - -export type RemoteTlfUpdates = { - timestamp: number - tlf: T.FS.Path - updates: Array<{path: T.FS.Path; uploading: boolean}> - writer: string -} - -type Conversation = { - conversationIDKey: string - teamType?: T.Chat.TeamType - tlfname?: string - teamname?: string - timestamp?: number - channelname?: string - snippetDecorated?: string - hasBadge?: true - hasUnread?: true - participants?: Array -} - -type KbfsDaemonStatus = { - readonly rpcStatus: T.FS.KbfsDaemonRpcStatus - readonly onlineStatus: T.FS.KbfsDaemonOnlineStatus -} - -export type ProxyProps = { - daemonHandshakeState: T.Config.DaemonHandshakeState - avatarRefreshCounter: ReadonlyMap - conversationsToSend: ReadonlyArray - darkMode?: boolean - diskSpaceStatus: T.FS.DiskSpaceStatus - endEstimate?: number - files?: number - fileName?: string - followers: ReadonlySet - following: ReadonlySet - kbfsDaemonStatus: KbfsDaemonStatus - kbfsEnabled: boolean - loggedIn: boolean - remoteTlfUpdates: ReadonlyArray - showingDiskSpaceBanner?: boolean - outOfDate: T.Config.OutOfDate - totalSyncingBytes?: number - username: string - httpSrvAddress: string - httpSrvToken: string - windowShownCountNum: number - navBadges: ReadonlyMap - infoMap: ReadonlyMap -} - -export type SerializeProps = Omit< - ProxyProps, - 'avatarRefreshCounter' | 'followers' | 'following' | 'infoMap' | 'navBadges' | 'windowShownCount' -> & { - avatarRefreshCounterArr: Array<[string, number]> - followersArr: Array - followingArr: Array - infoMapArr: Array<[string, T.Users.UserInfo]> - navBadgesArr: Array<[C.Tabs.Tab, number]> - windowShownCountNum: number - // ensure we never send extra stuff - avatarRefreshCounter?: never - followers?: never - following?: never - infoMap?: never - navBadges?: never - windowShownCount?: never -} - -// props we don't send at all if they're falsey -type RemovedEmpties = 'darkMode' | 'fileName' | 'files' | 'totalSyncingBytes' | 'showingDiskSpaceBanner' - -export type DeserializeProps = Omit & { - avatarRefreshCounter: Map - darkMode: boolean - daemonHandshakeState: T.Config.DaemonHandshakeState - files: number - fileName: string - followers: Set - following: Set - totalSyncingBytes: number - showingDiskSpaceBanner: boolean - httpSrvAddress: string - httpSrvToken: string - metaMap: Map< - string, - { - teamname?: string - timestamp?: number - channelname?: string - snippetDecorated?: string - // its not important to show rekey/reset stuff in the widget - rekeyers?: Set - resetParticipants?: Set - wasFinalizedBy?: string - } - > - badgeMap: Map - unreadMap: Map - loggedIn: boolean - outOfDate: T.Config.OutOfDate - infoMap: Map - username: string - windowShownCountNum: number -} - -const initialState: DeserializeProps = { - avatarRefreshCounter: new Map(), - badgeMap: new Map(), - conversationsToSend: [], - daemonHandshakeState: 'starting', - darkMode: false, - diskSpaceStatus: T.FS.DiskSpaceStatus.Ok, - endEstimate: 0, - fileName: '', - files: 0, - followers: new Set(), - following: new Set(), - httpSrvAddress: '', - httpSrvToken: '', - infoMap: new Map(), - kbfsDaemonStatus: { - onlineStatus: T.FS.KbfsDaemonOnlineStatus.Unknown, - rpcStatus: T.FS.KbfsDaemonRpcStatus.Connected, - }, - kbfsEnabled: false, - loggedIn: false, - metaMap: new Map(), - navBadges: new Map(), - outOfDate: { - critical: false, - message: '', - outOfDate: false, - updating: false, - }, - remoteTlfUpdates: [], - showingDiskSpaceBanner: false, - totalSyncingBytes: 0, - unreadMap: new Map(), - username: '', - windowShownCountNum: 0, -} - -export const serialize = (p: ProxyProps): Partial => { - const {avatarRefreshCounter, followers, following, infoMap, navBadges, ...toSend} = p - return { - ...toSend, - avatarRefreshCounterArr: [...avatarRefreshCounter.entries()], - followersArr: [...followers], - followingArr: [...following], - infoMapArr: [...infoMap.entries()], - navBadgesArr: [...p.navBadges.entries()], - } -} - -export const deserialize = ( - state: DeserializeProps = initialState, - props?: Partial -): DeserializeProps => { - if (!props) { - return state - } - const {avatarRefreshCounterArr, conversationsToSend, daemonHandshakeState, diskSpaceStatus} = props - const {fileName, files, followersArr, followingArr, httpSrvAddress, httpSrvToken, infoMapArr} = props - const {endEstimate, kbfsDaemonStatus, kbfsEnabled, loggedIn, navBadgesArr, darkMode, outOfDate} = props - const {remoteTlfUpdates, showingDiskSpaceBanner, totalSyncingBytes, username, windowShownCountNum} = props - - return produce(state, s => { - if (avatarRefreshCounterArr !== undefined) { - s.avatarRefreshCounter = new Map(avatarRefreshCounterArr) - } - if (daemonHandshakeState !== undefined) { - s.daemonHandshakeState = daemonHandshakeState - } - if (followersArr !== undefined) { - s.followers = new Set(followersArr) - } - if (followingArr !== undefined) { - s.following = new Set(followingArr) - } - if (httpSrvAddress !== undefined) { - s.httpSrvAddress = httpSrvAddress - } - if (httpSrvToken !== undefined) { - s.httpSrvToken = httpSrvToken - } - if (loggedIn !== undefined) { - s.loggedIn = loggedIn - } - if (outOfDate !== undefined) { - s.outOfDate = outOfDate - } - if (username !== undefined) { - s.username = username - } - if (windowShownCountNum !== undefined) { - s.windowShownCountNum = windowShownCountNum - } - if (conversationsToSend !== undefined) { - s.conversationsToSend = T.castDraft(conversationsToSend) - } - if (darkMode !== undefined) { - s.darkMode = darkMode - } - if (diskSpaceStatus !== undefined) { - s.diskSpaceStatus = diskSpaceStatus - } - if (endEstimate !== undefined) { - s.endEstimate = endEstimate - } - if (fileName !== undefined) { - s.fileName = fileName - } - if (files !== undefined) { - s.files = files - } - if (kbfsDaemonStatus !== undefined) { - s.kbfsDaemonStatus = kbfsDaemonStatus - } - if (kbfsEnabled !== undefined) { - s.kbfsEnabled = kbfsEnabled - } - if (navBadgesArr !== undefined) { - s.navBadges = new Map(navBadgesArr) - } - if (remoteTlfUpdates !== undefined) { - s.remoteTlfUpdates = T.castDraft(remoteTlfUpdates) - } - if (showingDiskSpaceBanner !== undefined) { - s.showingDiskSpaceBanner = showingDiskSpaceBanner - } - if (totalSyncingBytes !== undefined) { - s.totalSyncingBytes = totalSyncingBytes - } - if (infoMapArr !== undefined) { - s.infoMap = new Map(infoMapArr) - } - - conversationsToSend?.forEach(c => { - const {conversationIDKey, hasUnread, hasBadge} = c - const {teamname, timestamp, channelname, snippetDecorated} = c - s.badgeMap.set(conversationIDKey, hasBadge ? 1 : 0) - s.unreadMap.set(conversationIDKey, hasUnread ? 1 : 0) - const meta = s.metaMap.get(conversationIDKey) ?? { - channelname: undefined, - rekeyers: undefined, - resetParticipants: undefined, - snippetDecorated: undefined, - teamname: undefined, - timestamp: undefined, - wasFinalizedBy: undefined, - } - meta.teamname = teamname - meta.timestamp = timestamp - meta.channelname = channelname - meta.snippetDecorated = snippetDecorated - - // its not important to show rekey/reset stuff in the widget - meta.rekeyers = emptySet - meta.resetParticipants = emptySet - meta.wasFinalizedBy = '' - - s.metaMap.set(conversationIDKey, meta) - }) - }) -} diff --git a/shared/pinentry/main2.desktop.tsx b/shared/pinentry/main2.desktop.tsx index 10e9342a0f3e..32ab4d7e9747 100644 --- a/shared/pinentry/main2.desktop.tsx +++ b/shared/pinentry/main2.desktop.tsx @@ -1,12 +1,46 @@ -import Pinentry from './remote-container.desktop' +import * as React from 'react' +import * as R from '@/constants/remote' +import * as RemoteGen from '@/actions/remote-gen' +import type * as T from '@/constants/types' +import Pinentry from './index.desktop' import load from '../desktop/remote/component-loader.desktop' -import {deserialize, type SerializeProps, type DeserializeProps} from './remote-serializer.desktop' +import {useDarkModeState} from '@/stores/darkmode' + +export type ProxyProps = { + cancelLabel?: string + darkMode: boolean + prompt: string + retryLabel?: string + showTyping?: T.RPCGen.Feature + submitLabel?: string + type: T.RPCGen.PassphraseType + windowTitle: string +} + +const DarkModeSync = ({darkMode, children}: {darkMode: boolean; children: React.ReactNode}) => { + const setSystemDarkMode = useDarkModeState(s => s.dispatch.setSystemDarkMode) + React.useEffect(() => { + const id = setTimeout(() => setSystemDarkMode(darkMode), 1) + return () => clearTimeout(id) + }, [setSystemDarkMode, darkMode]) + return <>{children} +} const sessionID = /\?param=(\w+)/.exec(window.location.search) -load({ - child: (p: DeserializeProps) => , - deserialize, +load({ + child: (p: ProxyProps) => { + const {darkMode, ...rest} = p + return ( + + R.remoteDispatch(RemoteGen.createPinentryOnCancel())} + onSubmit={(password: string) => R.remoteDispatch(RemoteGen.createPinentryOnSubmit({password}))} + /> + + ) + }, name: 'pinentry', params: sessionID?.[1] ?? '', }) diff --git a/shared/pinentry/remote-container.desktop.tsx b/shared/pinentry/remote-container.desktop.tsx deleted file mode 100644 index 316f528a9e14..000000000000 --- a/shared/pinentry/remote-container.desktop.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import * as React from 'react' -import * as RemoteGen from '../actions/remote-gen' -import * as R from '@/constants/remote' -import Pinentry from './index.desktop' -import type {DeserializeProps} from './remote-serializer.desktop' -import {useDarkModeState} from '@/stores/darkmode' - -const RemoteContainer = (d: DeserializeProps) => { - const {darkMode, ...rest} = d - const setSystemDarkMode = useDarkModeState(s => s.dispatch.setSystemDarkMode) - - React.useEffect(() => { - const id = setTimeout(() => { - setSystemDarkMode(darkMode) - }, 1) - return () => { - clearTimeout(id) - } - }, [setSystemDarkMode, darkMode]) - - return ( - R.remoteDispatch(RemoteGen.createPinentryOnCancel())} - onSubmit={(password: string) => R.remoteDispatch(RemoteGen.createPinentryOnSubmit({password}))} - /> - ) -} -export default RemoteContainer diff --git a/shared/pinentry/remote-proxy.desktop.tsx b/shared/pinentry/remote-proxy.desktop.tsx index ea897586694b..dc863302749e 100644 --- a/shared/pinentry/remote-proxy.desktop.tsx +++ b/shared/pinentry/remote-proxy.desktop.tsx @@ -4,9 +4,9 @@ import * as T from '@/constants/types' import * as React from 'react' import useBrowserWindow from '../desktop/remote/use-browser-window.desktop' import useSerializeProps from '../desktop/remote/use-serialize-props.desktop' -import {serialize, type ProxyProps} from './remote-serializer.desktop' import {useColorScheme} from 'react-native' import {usePinentryState} from '@/stores/pinentry' +import type {ProxyProps} from './main2.desktop' const windowOpts = {height: 230, width: 440} @@ -21,7 +21,7 @@ const Pinentry = (p: ProxyProps) => { windowTitle: 'Pinentry', }) - useSerializeProps(p, serialize, windowComponent, windowParam) + useSerializeProps(p, windowComponent, windowParam) return null } diff --git a/shared/pinentry/remote-serializer.desktop.tsx b/shared/pinentry/remote-serializer.desktop.tsx deleted file mode 100644 index ca291a3014af..000000000000 --- a/shared/pinentry/remote-serializer.desktop.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import * as T from '@/constants/types' -import type * as Constants from '@/stores/pinentry' -import {produce} from 'immer' - -export type ProxyProps = { - darkMode: boolean -} & Constants.Store - -export type SerializeProps = ProxyProps -export type DeserializeProps = ProxyProps - -const initialState: DeserializeProps = { - darkMode: false, - prompt: '', - showTyping: { - allow: false, - defaultValue: false, - label: '', - readonly: false, - }, - type: T.RPCGen.PassphraseType.none, - windowTitle: '', -} - -export const serialize = (p: ProxyProps): Partial => p - -export const deserialize = ( - state: DeserializeProps = initialState, - props?: Partial -): DeserializeProps => { - if (!props) return state - const {darkMode, prompt, showTyping, type, windowTitle} = props - return produce(state, s => { - if (darkMode !== undefined) { - s.darkMode = darkMode - } - if (prompt !== undefined) { - s.prompt = prompt - } - if (showTyping !== undefined) { - if (s.showTyping) { - s.showTyping.allow = showTyping.allow - s.showTyping.defaultValue = showTyping.defaultValue - s.showTyping.label = showTyping.label - s.showTyping.readonly = showTyping.readonly - } else { - s.showTyping = showTyping - } - } - if (type !== undefined) { - s.type = type - } - if (windowTitle !== undefined) { - s.windowTitle = windowTitle - } - }) -} diff --git a/shared/tracker/index.desktop.tsx b/shared/tracker/index.desktop.tsx index 196e43b50dd4..4077a4df6c52 100644 --- a/shared/tracker/index.desktop.tsx +++ b/shared/tracker/index.desktop.tsx @@ -1,25 +1,28 @@ -import * as C from '@/constants' import * as Kb from '@/common-adapters' -import type * as React from 'react' -import Assertion from './assertion' -import Bio from './bio' import type * as T from '@/constants/types' +import {useColorScheme} from 'react-native' -type Props = { - assertionKeys?: ReadonlyArray +export type Props = { + assertions?: ReadonlyArray bio?: string - followThem?: boolean + blocked: boolean + darkMode: boolean + followThem: boolean followersCount?: number followingCount?: number - followsYou?: boolean - guiID?: string + followsYou: boolean + fullname?: string + guiID: string + hidFromFollowers: boolean + httpSrvAddress: string + httpSrvToken: string isYou: boolean location?: string - onFollow: () => void + onAccept: () => void onChat: () => void onClose: () => void + onFollow: () => void onIgnoreFor24Hours: () => void - onAccept: () => void onReload: () => void reason: string state: T.Tracker.DetailsState @@ -27,29 +30,23 @@ type Props = { trackerUsername: string } +const avatarUrl = (httpSrvAddress: string, httpSrvToken: string, username: string, size: number, darkMode: boolean) => + `http://${httpSrvAddress}/av?typ=user&name=${username}&format=square_${size}&mode=${darkMode ? 'dark' : 'light'}&token=${httpSrvToken}` + +const teamAvatarUrl = (httpSrvAddress: string, httpSrvToken: string, teamname: string, size: number, darkMode: boolean) => + `http://${httpSrvAddress}/av?typ=team&name=${teamname}&format=square_${size}&mode=${darkMode ? 'dark' : 'light'}&token=${httpSrvToken}` + const getButtons = (props: Props) => { const buttonClose = ( - + ) const buttonAccept = ( - + ) const buttonChat = ( - + - + ) if (props.isYou) { @@ -66,108 +63,222 @@ const getButtons = (props: Props) => { ? [buttonClose, buttonChat] : [ buttonChat, - , + , ] case 'broken': return [ - , + , buttonAccept, ] case 'needsUpgrade': return [buttonChat, buttonAccept] case 'error': - return [ - , - ] + return [] default: break } return [] } -const TeamShowcase = ({name}: {name: string}) => ( - - - {name} - -) +// Inline bio rendering (store-free) +const Bio = (props: { + bio?: string + blocked: boolean + followThem: boolean + followersCount?: number + followingCount?: number + followsYou: boolean + fullname?: string + hidFromFollowers: boolean + location?: string + trackerUsername: string +}) => { + const {bio, blocked, followThem, followersCount, followingCount, followsYou, fullname, hidFromFollowers, location, trackerUsername} = props + let followText = '' + if (followThem) { + followText = followsYou ? 'YOU FOLLOW EACH OTHER' : 'YOU FOLLOW THEM' + } else if (followsYou) { + followText = 'FOLLOWS YOU' + } + return ( + + + + {fullname} + + + {!!followText && {followText}} + {followersCount !== undefined && ( + + {followersCount} + {' Followers · Following '} + {followingCount} + + )} + {!!bio && ( + + {bio} + + )} + {!!location && ( + + {location} + + )} + {blocked ? ( + + {"You blocked them. "} + {trackerUsername} + {" won't be able to chat with you or add you to teams."} + + ) : ( + hidFromFollowers && ( + + You hid them from your followers. + + ) + )} + + ) +} const _scoreAssertionKey = (a: string) => { switch (a) { - case 'pgp': - return 110 - case 'twitter': - return 100 - case 'facebook': - return 90 - case 'github': - return 80 - case 'reddit': - return 75 - case 'hackernews': - return 70 - case 'https': - return 60 - case 'http': - return 50 - case 'dns': - return 40 - case 'stellar': - return 30 - case 'btc': - return 20 - case 'zcash': - return 10 - default: - return 1 + case 'pgp': return 110 + case 'twitter': return 100 + case 'facebook': return 90 + case 'github': return 80 + case 'reddit': return 75 + case 'hackernews': return 70 + case 'https': return 60 + case 'http': return 50 + case 'dns': return 40 + case 'stellar': return 30 + case 'btc': return 20 + case 'zcash': return 10 + default: return 1 + } +} + +const sortAssertions = (a: T.Tracker.Assertion, b: T.Tracker.Assertion) => { + if (a.type === b.type) { + return a.value.localeCompare(b.value) } + return _scoreAssertionKey(b.type) - _scoreAssertionKey(a.type) } -const sortAssertionKeys = (a: string, b: string) => { - const pa = a.split(':') - const pb = b.split(':') +const assertionColorToColor = (c: T.Tracker.AssertionColor) => { + switch (c) { + case 'blue': return Kb.Styles.globalColors.blue + case 'red': return Kb.Styles.globalColors.red + case 'black': return Kb.Styles.globalColors.black + case 'green': return Kb.Styles.globalColors.green + case 'gray': return Kb.Styles.globalColors.black_50 + case 'yellow': + case 'orange': + default: return Kb.Styles.globalColors.red + } +} - const typeA = pa[0] - const typeB = pb[0] +const assertionColorToTextColor = (c: T.Tracker.AssertionColor) => { + switch (c) { + case 'blue': return Kb.Styles.globalColors.blueDark + case 'red': return Kb.Styles.globalColors.redDark + case 'black': return Kb.Styles.globalColors.black + case 'green': return Kb.Styles.globalColors.greenDark + case 'gray': return Kb.Styles.globalColors.black_50 + case 'yellow': + case 'orange': + default: return Kb.Styles.globalColors.redDark + } +} - if (typeA === typeB) { - return pa[1]?.localeCompare(pb[1] ?? '') ?? 0 +const stateToIcon = (state: T.Tracker.AssertionState) => { + switch (state) { + case 'checking': return 'iconfont-proof-pending' + case 'valid': return 'iconfont-proof-good' + case 'error': + case 'warning': + case 'revoked': return 'iconfont-proof-broken' + case 'suggestion': return 'iconfont-proof-placeholder' + default: return 'iconfont-proof-pending' } +} - if (!typeA || !typeB) return 0 +const siteIconToSrcSet = (set: T.Tracker.SiteIconSet) => + set.map(i => `url("${i.path}")`).reverse().join(', ') - const scoreA = _scoreAssertionKey(typeB) - const scoreB = _scoreAssertionKey(typeA) - return scoreA - scoreB +// Inline assertion rendering (store-free) +const AssertionRow = (props: {assertion: T.Tracker.Assertion}) => { + const {assertion: a} = props + const isDarkMode = useColorScheme() === 'dark' + const iconSet = isDarkMode ? a.siteIconDarkmode : a.siteIcon + return ( + + + {iconSet.length > 0 && ( + + )} + + { + // handled via openUrl in the full app, but in remote window just open directly + window.open(a.siteURL, '_blank') + } : undefined} + style={Kb.Styles.collapseStyles([ + styles.assertionValue, + a.state === 'revoked' && styles.strikeThrough, + {color: assertionColorToTextColor(a.color)}, + ])} + > + {a.value} + + @{a.type} + + + + {!!a.metas.length && ( + + {a.metas.map(m => ( + + ))} + + )} + + ) +} + +const TeamShowcase = (props: {name: string; httpSrvAddress: string; httpSrvToken: string}) => { + const isDarkMode = useColorScheme() === 'dark' + return ( + + + {props.name} + + ) } const Tracker = (props: Props) => { - let assertions: React.ReactNode - if (props.assertionKeys) { - const unsorted = [...props.assertionKeys] - const sorted = unsorted.sort(sortAssertionKeys) - assertions = sorted.map(a => ) - } else { - // TODO could do a loading thing before we know about the list at all? - assertions = null - } + const isDarkMode = useColorScheme() === 'dark' + + const sortedAssertions = props.assertions ? [...props.assertions].sort(sortAssertions) : null let backgroundColor: string if (['broken', 'error'].includes(props.state)) { @@ -178,24 +289,16 @@ const Tracker = (props: Props) => { const buttons = getButtons(props) - // In order to keep the 'effect' of the card sliding up on top of the text the text is below the scroll area. We still need the spacing so we draw the text inside the scroll but invisible - return ( {props.reason} - {/* The header box must go after the reason text, so that the - * close button's draggingClickable style goes on top of the - * reason's draggable style, which matters on Linux. */} + {/* Close button must go after reason text for z-ordering on Linux */} - + {props.reason} @@ -203,27 +306,41 @@ const Tracker = (props: Props) => { - + + + + {props.trackerUsername} + + - + {props.teamShowcase && ( {props.teamShowcase.map(t => ( - + ))} )} - {assertions} + {sortedAssertions?.map(a => )} {!!buttons.length && ( @@ -255,6 +372,15 @@ const reason = { const styles = Kb.Styles.styleSheetCreate( () => ({ + assertionRow: {flexShrink: 0, paddingBottom: 4, paddingTop: 4}, + assertionSite: {color: Kb.Styles.globalColors.black_20}, + assertionTextContainer: Kb.Styles.platformStyles({ + common: {flexGrow: 1, flexShrink: 1, marginTop: -1}, + }), + assertionValue: Kb.Styles.platformStyles({ + common: {letterSpacing: 0.2}, + isElectron: {wordBreak: 'break-all'}, + }), assertions: { backgroundColor: Kb.Styles.globalColors.white, flexShrink: 0, @@ -262,6 +388,7 @@ const styles = Kb.Styles.styleSheetCreate( paddingRight: Kb.Styles.globalMargins.small, paddingTop: Kb.Styles.globalMargins.small, }, + avatar: {borderRadius: '50%'} as const, avatarBackground: { backgroundColor: Kb.Styles.globalColors.white, bottom: 0, @@ -271,6 +398,24 @@ const styles = Kb.Styles.styleSheetCreate( top: avatarSize / 2, }, avatarContainer: {flexShrink: 0, position: 'relative'}, + bioContainer: {backgroundColor: Kb.Styles.globalColors.white, flexShrink: 0}, + bioText: Kb.Styles.platformStyles({ + common: { + paddingLeft: Kb.Styles.globalMargins.mediumLarge, + paddingRight: Kb.Styles.globalMargins.mediumLarge, + }, + isElectron: {wordBreak: 'break-word'} as const, + }), + blockedBackgroundText: { + backgroundColor: Kb.Styles.globalColors.red_20, + borderRadius: Kb.Styles.borderRadius, + margin: Kb.Styles.globalMargins.small, + paddingBottom: Kb.Styles.globalMargins.tiny, + paddingLeft: Kb.Styles.globalMargins.small, + paddingRight: Kb.Styles.globalMargins.small, + paddingTop: Kb.Styles.globalMargins.tiny, + }, + bold: {...Kb.Styles.globalStyles.fontBold}, buttons: Kb.Styles.platformStyles({ common: { ...Kb.Styles.globalStyles.fillAbsolute, @@ -293,6 +438,10 @@ const styles = Kb.Styles.styleSheetCreate( backgroundColor: Kb.Styles.globalColors.white, position: 'relative', }, + fullNameContainer: { + paddingLeft: Kb.Styles.globalMargins.mediumLarge, + paddingRight: Kb.Styles.globalMargins.mediumLarge, + }, header: { justifyContent: 'flex-end', paddingBottom: Kb.Styles.globalMargins.tiny, @@ -300,6 +449,7 @@ const styles = Kb.Styles.styleSheetCreate( position: 'absolute', zIndex: 9, }, + metaContainer: {flexShrink: 0, paddingLeft: 20 + Kb.Styles.globalMargins.tiny * 2 - 4}, nameWithIconContainer: {alignSelf: 'center'}, reason: Kb.Styles.platformStyles({ common: { @@ -324,10 +474,20 @@ const styles = Kb.Styles.styleSheetCreate( paddingBottom: Kb.Styles.globalMargins.small, }, }), + siteIcon: Kb.Styles.platformStyles({ + isElectron: { + backgroundSize: 'contain', + flexShrink: 0, + height: 16, + width: 16, + }, + }), spaceUnderButtons: { flexShrink: 0, height: barHeight, }, + strikeThrough: {textDecorationLine: 'line-through'}, + teamAvatar: {borderRadius: Kb.Styles.borderRadius}, teamShowcases: { backgroundColor: Kb.Styles.globalColors.white, flexShrink: 0, diff --git a/shared/tracker/main2.desktop.tsx b/shared/tracker/main2.desktop.tsx index bd33ab121239..daa4b37b7ad0 100644 --- a/shared/tracker/main2.desktop.tsx +++ b/shared/tracker/main2.desktop.tsx @@ -1,13 +1,61 @@ -import Tracker from './remote-container.desktop' +import * as C from '@/constants' import * as Kb from '@/common-adapters' +import * as R from '@/constants/remote' +import * as RemoteGen from '../actions/remote-gen' +import Tracker from './index.desktop' import load from '../desktop/remote/component-loader.desktop' -import {deserialize, type SerializeProps, type DeserializeProps} from './remote-serializer.desktop' +import {useDarkModeState} from '@/stores/darkmode' +import * as React from 'react' +import type {Props as TrackerProps} from './index.desktop' +import KB2 from '@/util/electron.desktop' + +const {closeWindow} = KB2.functions + +type ProxyProps = Omit + +const DarkModeSync = ({darkMode, children}: {darkMode: boolean; children: React.ReactNode}) => { + const setSystemDarkMode = useDarkModeState(s => s.dispatch.setSystemDarkMode) + React.useEffect(() => { + const id = setTimeout(() => setSystemDarkMode(darkMode), 1) + return () => clearTimeout(id) + }, [setSystemDarkMode, darkMode]) + return <>{children} +} const username = /\?param=(\w+)/.exec(window.location.search) -load({ - child: (p: DeserializeProps) => , - deserialize, +load({ + child: (p: ProxyProps) => ( + + R.remoteDispatch(RemoteGen.createTrackerChangeFollow({follow: true, guiID: p.guiID}))} + onChat={() => { + R.remoteDispatch(RemoteGen.createShowMain()) + R.remoteDispatch(RemoteGen.createPreviewConversation({participant: p.trackerUsername})) + }} + onClose={() => { + R.remoteDispatch(RemoteGen.createTrackerCloseTracker({guiID: p.guiID})) + closeWindow?.() + }} + onFollow={() => R.remoteDispatch(RemoteGen.createTrackerChangeFollow({follow: true, guiID: p.guiID}))} + onIgnoreFor24Hours={() => R.remoteDispatch(RemoteGen.createTrackerIgnore({guiID: p.guiID}))} + onReload={() => + R.remoteDispatch( + RemoteGen.createTrackerLoad({ + assertion: p.trackerUsername, + forceDisplay: true, + fromDaemon: false, + guiID: C.generateGUIID(), + ignoreCache: true, + inTracker: true, + reason: '', + }) + ) + } + /> + + ), name: 'tracker', params: username?.[1] ?? '', style: Kb.Styles.platformStyles({ diff --git a/shared/tracker/remote-container.desktop.tsx b/shared/tracker/remote-container.desktop.tsx deleted file mode 100644 index ffc71e7b7e02..000000000000 --- a/shared/tracker/remote-container.desktop.tsx +++ /dev/null @@ -1,150 +0,0 @@ -// Inside tracker we use an embedded Avatar which is connected. -import * as React from 'react' -import * as C from '@/constants' -import {useConfigState} from '@/stores/config' -import * as R from '@/constants/remote' -import * as RemoteGen from '../actions/remote-gen' -import type * as T from '@/constants/types' -import Tracker from './index.desktop' -import type {DeserializeProps} from './remote-serializer.desktop' -import KB2 from '@/util/electron.desktop' -import {useAvatarState} from '@/common-adapters/avatar/store' -import {useTrackerState} from '@/stores/tracker' -import {useUsersState} from '@/stores/users' -import {useFollowerState} from '@/stores/followers' -import {useCurrentUserState} from '@/stores/current-user' -import {useDarkModeState} from '@/stores/darkmode' - -const {closeWindow} = KB2.functions - -const noDetails: T.Tracker.Details = { - blocked: false, - guiID: '', - hidFromFollowers: false, - reason: '', - resetBrokeTrack: false, - state: 'checking', - username: '', -} - -const RemoteContainer = (d: DeserializeProps) => { - const {avatarRefreshCounter, darkMode, trackerUsername, tracker, followers, following, username} = d - const {httpSrvToken, httpSrvAddress, infoMap, blockMap} = d - const {usernameToDetails} = tracker - const details = usernameToDetails.get(trackerUsername) ?? noDetails - const {assertions, bio, followersCount, followingCount} = details - const {guiID, location, reason, state: trackerState, teamShowcase} = details - - const replaceAvatar = useAvatarState(s => s.dispatch.replace) - const replaceFollower = useFollowerState(s => s.dispatch.replace) - const replaceUsers = useUsersState(s => s.dispatch.replace) - const replaceCurrent = useCurrentUserState(s => s.dispatch.replaceUsername) - const replaceHTTP = useConfigState(s => s.dispatch.setHTTPSrvInfo) - const replaceTracker = useTrackerState(s => s.dispatch.replace) - const setSystemDarkMode = useDarkModeState(s => s.dispatch.setSystemDarkMode) - - React.useEffect(() => { - const id = setTimeout(() => { - setSystemDarkMode(darkMode) - }, 1) - return () => { - clearTimeout(id) - } - }, [setSystemDarkMode, darkMode]) - - React.useEffect(() => { - const id = setTimeout(() => { - replaceAvatar(avatarRefreshCounter) - }, 1) - return () => { - clearTimeout(id) - } - }, [replaceAvatar, avatarRefreshCounter]) - - React.useEffect(() => { - const id = setTimeout(() => { - replaceFollower(followers, following) - }, 1) - return () => { - clearTimeout(id) - } - }, [replaceFollower, followers, following]) - - React.useEffect(() => { - const id = setTimeout(() => { - replaceUsers(infoMap, blockMap) - }, 1) - return () => { - clearTimeout(id) - } - }, [replaceUsers, infoMap, blockMap]) - - React.useEffect(() => { - const id = setTimeout(() => { - replaceCurrent(username) - }, 1) - return () => { - clearTimeout(id) - } - }, [replaceCurrent, username]) - - React.useEffect(() => { - const id = setTimeout(() => { - replaceHTTP(httpSrvAddress, httpSrvToken) - }, 1) - return () => { - clearTimeout(id) - } - }, [replaceHTTP, httpSrvAddress, httpSrvToken]) - - React.useEffect(() => { - const id = setTimeout(() => { - replaceTracker(tracker.usernameToDetails) - }, 1) - return () => { - clearTimeout(id) - } - }, [replaceTracker, tracker.usernameToDetails]) - - return ( - R.remoteDispatch(RemoteGen.createTrackerChangeFollow({follow: true, guiID}))} - onChat={() => { - R.remoteDispatch(RemoteGen.createShowMain()) - R.remoteDispatch(RemoteGen.createPreviewConversation({participant: trackerUsername})) - }} - onClose={() => { - R.remoteDispatch(RemoteGen.createTrackerCloseTracker({guiID})) - // close immediately - closeWindow?.() - }} - onFollow={() => R.remoteDispatch(RemoteGen.createTrackerChangeFollow({follow: true, guiID}))} - onIgnoreFor24Hours={() => R.remoteDispatch(RemoteGen.createTrackerIgnore({guiID}))} - onReload={() => - R.remoteDispatch( - RemoteGen.createTrackerLoad({ - assertion: trackerUsername, - forceDisplay: true, - fromDaemon: false, - guiID: C.generateGUIID(), - ignoreCache: true, - inTracker: true, - reason: '', - }) - ) - } - reason={reason} - state={trackerState} - teamShowcase={teamShowcase} - trackerUsername={trackerUsername} - /> - ) -} -export default RemoteContainer diff --git a/shared/tracker/remote-proxy.desktop.tsx b/shared/tracker/remote-proxy.desktop.tsx index fc9dcdbba3b3..23c3891dfa57 100644 --- a/shared/tracker/remote-proxy.desktop.tsx +++ b/shared/tracker/remote-proxy.desktop.tsx @@ -1,26 +1,23 @@ // A mirror of the remote tracker windows. -import * as C from '@/constants' -import {useAvatarState} from '@/common-adapters/avatar/store' -import {useConfigState} from '@/stores/config' -import * as React from 'react' +import type * as React from 'react' import useSerializeProps from '../desktop/remote/use-serialize-props.desktop' import useBrowserWindow from '../desktop/remote/use-browser-window.desktop' -import {serialize, type ProxyProps} from './remote-serializer.desktop' -import {intersect} from '@/util/set' -import {mapFilterByKey} from '@/util/map' import {useColorScheme} from 'react-native' import {useTrackerState} from '@/stores/tracker' import {useUsersState} from '@/stores/users' import {useFollowerState} from '@/stores/followers' import {useCurrentUserState} from '@/stores/current-user' +import {useConfigState} from '@/stores/config' +import type {Props as TrackerProps} from './index.desktop' const MAX_TRACKERS = 5 const windowOpts = {hasShadow: false, height: 470, transparent: true, width: 320} +type ProxyProps = Omit + const RemoteTracker = (props: {trackerUsername: string}) => { const {trackerUsername} = props const details = useTrackerState(s => s.getDetails(trackerUsername)) - const infoMap = useUsersState(s => s.infoMap) const blockMap = useUsersState(s => s.blockMap) const followers = useFollowerState(s => s.followers) const following = useFollowerState(s => s.following) @@ -28,47 +25,29 @@ const RemoteTracker = (props: {trackerUsername: string}) => { const httpSrv = useConfigState(s => s.httpSrv) const {assertions, bio, followersCount, followingCount, fullname, guiID} = details const {hidFromFollowers, location, reason, teamShowcase} = details - const counts = new Map([ - [C.waitingKeyTracker, C.useWaitingState(s => s.counts.get(C.waitingKeyTracker) ?? 0)], - ]) - const errors = new Map([[C.waitingKeyTracker, C.useWaitingState(s => s.errors.get(C.waitingKeyTracker))]]) - const trackerUsernames = new Set([trackerUsername]) - const blocked = blockMap.get(trackerUsername)?.chatBlocked || false - - const avatarCount = useAvatarState(s => s.counts.get(trackerUsername) ?? 0) - - const avatarRefreshCounter = React.useMemo(() => { - return new Map([[trackerUsername, avatarCount]]) - }, [trackerUsername, avatarCount]) - const isDarkMode = useColorScheme() === 'dark' + const blocked = blockMap.get(trackerUsername)?.chatBlocked || false const p: ProxyProps = { - assertions, - avatarRefreshCounter, + assertions: assertions ? [...assertions.values()] : undefined, bio, - blockMap: mapFilterByKey(blockMap, trackerUsernames), blocked, - counts, darkMode: isDarkMode, - errors, - followers: intersect(followers, trackerUsernames), + followThem: following.has(trackerUsername), followersCount, - following: intersect(following, trackerUsernames), followingCount, + followsYou: followers.has(trackerUsername), fullname, guiID, hidFromFollowers, httpSrvAddress: httpSrv.address, httpSrvToken: httpSrv.token, - infoMap: mapFilterByKey(infoMap, trackerUsernames), + isYou: username === trackerUsername, location, reason, - resetBrokeTrack: false, state: details.state, teamShowcase, trackerUsername, - username, } const windowComponent = 'tracker' @@ -81,7 +60,7 @@ const RemoteTracker = (props: {trackerUsername: string}) => { windowTitle: `Tracker - ${trackerUsername}`, }) - useSerializeProps(p, serialize, windowComponent, windowParam) + useSerializeProps(p, windowComponent, windowParam) return null } diff --git a/shared/tracker/remote-serializer.desktop.tsx b/shared/tracker/remote-serializer.desktop.tsx deleted file mode 100644 index e84c363ce61e..000000000000 --- a/shared/tracker/remote-serializer.desktop.tsx +++ /dev/null @@ -1,206 +0,0 @@ -import * as T from '@/constants/types' -import type {RPCError} from '@/util/errors' -import {produce} from 'immer' - -// for convenience we flatten the props we send over the wire -type WaitingHoistedProps = 'counts' | 'errors' - -export type ProxyProps = { - avatarRefreshCounter: ReadonlyMap - followers: ReadonlySet - following: ReadonlySet - darkMode: boolean - trackerUsername: string - username: string - httpSrvAddress: string - httpSrvToken: string - infoMap: ReadonlyMap - blockMap: ReadonlyMap -} & T.Tracker.Details & - Pick - -export type SerializeProps = Omit< - ProxyProps, - | 'avatarRefreshCounter' - | 'assertions' - | 'blockMap' - | 'infoMap' - | 'following' - | 'followers' - | 'counts' - | 'errors' -> & { - assertionsArr: Array<[string, T.Tracker.Assertion]> - avatarRefreshCounterArr: Array<[string, number]> - countsArr: Array<[string, number]> - errorsArr: Array<[string, RPCError | undefined]> - followersArr: Array - followingArr: Array - infoMapArr: Array<[string, T.Users.UserInfo]> - blockMapArr: Array<[string, T.Users.BlockState]> - - avatarRefreshCounter?: never - assertions?: never - blockMap?: never - infoMap?: never - following?: never - followers?: never - counts?: never - errors?: never -} -export type DeserializeProps = { - avatarRefreshCounter: Map - darkMode: boolean - followers: Set - following: Set - infoMap: Map - blockMap: Map - teams: {teamNameToID: Map} - tracker: {usernameToDetails: Map} - trackerUsername: string - waiting: T.Waiting.State - username: string - httpSrvAddress: string - httpSrvToken: string -} - -const initialState: DeserializeProps = { - avatarRefreshCounter: new Map(), - blockMap: new Map(), - darkMode: false, - followers: new Set(), - following: new Set(), - httpSrvAddress: '', - httpSrvToken: '', - infoMap: new Map(), - teams: {teamNameToID: new Map()}, - tracker: {usernameToDetails: new Map()}, - trackerUsername: '', - username: '', - waiting: {counts: new Map(), errors: new Map()}, -} - -export const serialize = (p: ProxyProps): Partial => { - const { - assertions, - avatarRefreshCounter, - following, - followers, - infoMap, - blockMap, - counts, - errors, - trackerUsername, - ...toSend - } = p - return { - ...toSend, - assertionsArr: [...(assertions?.entries() ?? [])], - avatarRefreshCounterArr: [...avatarRefreshCounter.entries()], - blockMapArr: blockMap.has(trackerUsername) - ? [[trackerUsername, blockMap.get(trackerUsername) ?? {chatBlocked: false, followBlocked: false}]] - : [], - countsArr: [...counts.entries()], - errorsArr: [...errors.entries()], - followersArr: [...followers], - followingArr: [...following], - infoMapArr: [...infoMap.entries()], - trackerUsername, - } -} - -export const deserialize = ( - state: DeserializeProps = initialState, - props?: Partial -): DeserializeProps => { - if (!props) return state - - const {bio, darkMode, followingCount, followersCount, fullname} = props - const {guiID, hidFromFollowers, httpSrvAddress, httpSrvToken, location} = props - const {reason, stellarHidden, teamShowcase} = props - const {trackerUsername: _trackerUsername, username} = props - const {assertionsArr, avatarRefreshCounterArr, countsArr, errorsArr} = props - const {followersArr, followingArr, infoMapArr, blockMapArr, state: trackerState} = props - const trackerUsername = _trackerUsername ?? state.trackerUsername - - return produce(state, s => { - s.trackerUsername = trackerUsername - s.darkMode = darkMode ?? s.darkMode - if (avatarRefreshCounterArr !== undefined) { - s.avatarRefreshCounter = new Map(avatarRefreshCounterArr) - } - if (blockMapArr !== undefined) { - s.blockMap = new Map(blockMapArr) - } - if (followersArr !== undefined) { - s.followers = new Set(followersArr) - } - if (followingArr !== undefined) { - s.following = new Set(followingArr) - } - if (httpSrvAddress !== undefined) { - s.httpSrvAddress = httpSrvAddress - } - if (httpSrvToken !== undefined) { - s.httpSrvToken = httpSrvToken - } - if (infoMapArr !== undefined) { - s.infoMap = new Map(infoMapArr) - } - if (username !== undefined) { - s.username = username - } - if (countsArr !== undefined) { - s.waiting.counts = new Map(countsArr) - } - if (errorsArr !== undefined) { - s.waiting.errors = new Map(errorsArr) - } - if (blockMapArr !== undefined) { - s.blockMap = new Map(blockMapArr) - } - - const details = s.tracker.usernameToDetails.get(trackerUsername) ?? T.castDraft({} as T.Tracker.Details) - - details.username = trackerUsername - details.resetBrokeTrack = false - details.blocked = s.blockMap.get(trackerUsername)?.chatBlocked ?? details.blocked - if (assertionsArr) { - details.assertions = T.castDraft(new Map(assertionsArr)) - } - if (bio) { - details.bio = bio - } - if (followersCount) { - details.followersCount = followersCount - } - if (followingCount) { - details.followingCount = followingCount - } - if (fullname) { - details.fullname = fullname - } - if (guiID) { - details.guiID = guiID - } - if (hidFromFollowers) { - details.hidFromFollowers = hidFromFollowers - } - if (location) { - details.location = location - } - if (reason) { - details.reason = reason - } - if (trackerState) { - details.state = trackerState - } - if (stellarHidden) { - details.stellarHidden = stellarHidden - } - if (teamShowcase) { - details.teamShowcase = T.castDraft(teamShowcase) - } - s.tracker.usernameToDetails.set(trackerUsername, T.castDraft(details)) - }) -} diff --git a/shared/unlock-folders/main2.desktop.tsx b/shared/unlock-folders/main2.desktop.tsx index e09da73339a0..33431a9270ec 100644 --- a/shared/unlock-folders/main2.desktop.tsx +++ b/shared/unlock-folders/main2.desktop.tsx @@ -1,9 +1,70 @@ -import UnlockFolders from './remote-container.desktop' +import * as React from 'react' +import * as R from '@/constants/remote' +import * as RemoteGen from '../actions/remote-gen' +import UnlockFolders from './index.desktop' import load from '../desktop/remote/component-loader.desktop' -import {deserialize, type SerializeProps, type DeserializeProps} from './remote-serializer.desktop' +import {useDarkModeState} from '@/stores/darkmode' +import type {State as ConfigStore} from '@/stores/config' -load({ - child: (p: DeserializeProps) => , - deserialize, +export type ProxyProps = { + darkMode: boolean + devices: ConfigStore['unlockFoldersDevices'] + paperKeyError: string + waiting: boolean +} + +const DarkModeSync = ({darkMode, children}: {darkMode: boolean; children: React.ReactNode}) => { + const setSystemDarkMode = useDarkModeState(s => s.dispatch.setSystemDarkMode) + React.useEffect(() => { + const id = setTimeout(() => setSystemDarkMode(darkMode), 1) + return () => clearTimeout(id) + }, [setSystemDarkMode, darkMode]) + return <>{children} +} + +type Phase = 'promptOtherDevice' | 'paperKeyInput' | 'success' + +const UnlockFoldersWrapper = (p: ProxyProps) => { + const {darkMode, devices, waiting, paperKeyError: _error} = p + const [phase, setPhase] = React.useState('promptOtherDevice') + const [paperKeyError, setPaperKeyError] = React.useState(_error) + + const lastError = React.useRef(_error) + React.useEffect(() => { + if (_error !== lastError.current) { + lastError.current = _error + setPaperKeyError(_error) + } + }, [_error]) + + const lastPhase = React.useRef(phase) + React.useEffect(() => { + if (phase !== lastPhase.current) { + lastPhase.current = phase + setPaperKeyError('') + } + }, [phase]) + + return ( + + setPhase('promptOtherDevice')} + onClose={() => R.remoteDispatch(RemoteGen.createCloseUnlockFolders())} + onContinueFromPaperKey={(paperKey: string) => + R.remoteDispatch(RemoteGen.createUnlockFoldersSubmitPaperKey({paperKey})) + } + onFinish={() => R.remoteDispatch(RemoteGen.createCloseUnlockFolders())} + toPaperKeyInput={() => setPhase('paperKeyInput')} + /> + + ) +} + +load({ + child: (p: ProxyProps) => , name: 'unlock-folders', }) diff --git a/shared/unlock-folders/remote-container.desktop.tsx b/shared/unlock-folders/remote-container.desktop.tsx deleted file mode 100644 index 18db5590d794..000000000000 --- a/shared/unlock-folders/remote-container.desktop.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import * as R from '@/constants/remote' -import * as React from 'react' -import * as RemoteGen from '../actions/remote-gen' -import UnlockFolders from './index.desktop' -import type {DeserializeProps} from './remote-serializer.desktop' -import {useUnlockFoldersState as useUFState} from '@/stores/unlock-folders' -import {useDarkModeState} from '@/stores/darkmode' - -const RemoteContainer = (d: DeserializeProps) => { - const {darkMode, devices, waiting, paperKeyError: _error} = d - useUFState(s => s.dispatch.replace)(devices) - const phase = useUFState(s => s.phase) - const toPaperKeyInput = useUFState(s => s.dispatch.toPaperKeyInput) - const onBackFromPaperKey = useUFState(s => s.dispatch.onBackFromPaperKey) - const setSystemDarkMode = useDarkModeState(s => s.dispatch.setSystemDarkMode) - - const [paperKeyError, setPaperKeyError] = React.useState(_error) - const lastError = React.useRef(_error) - React.useEffect(() => { - if (_error !== lastError.current) { - lastError.current = _error - setPaperKeyError(_error) - } - }, [_error]) - - const lastPhase = React.useRef(phase) - React.useEffect(() => { - if (phase !== lastPhase.current) { - lastPhase.current = phase - setPaperKeyError('') - } - }, [phase]) - - const onClose = () => { - R.remoteDispatch(RemoteGen.createCloseUnlockFolders()) - } - - const onContinueFromPaperKey = (paperKey: string) => { - R.remoteDispatch(RemoteGen.createUnlockFoldersSubmitPaperKey({paperKey})) - } - - React.useEffect(() => { - const id = setTimeout(() => { - setSystemDarkMode(darkMode) - }, 1) - return () => { - clearTimeout(id) - } - }, [setSystemDarkMode, darkMode]) - - return ( - - ) -} -export default RemoteContainer diff --git a/shared/unlock-folders/remote-proxy.desktop.tsx b/shared/unlock-folders/remote-proxy.desktop.tsx index ebe89bf3d4b7..9bd077110182 100644 --- a/shared/unlock-folders/remote-proxy.desktop.tsx +++ b/shared/unlock-folders/remote-proxy.desktop.tsx @@ -2,9 +2,9 @@ import * as C from '@/constants' import * as React from 'react' import useBrowserWindow from '../desktop/remote/use-browser-window.desktop' import useSerializeProps from '../desktop/remote/use-serialize-props.desktop' -import {serialize, type ProxyProps} from './remote-serializer.desktop' import {useColorScheme} from 'react-native' import {useConfigState} from '@/stores/config' +import type {ProxyProps} from './main2.desktop' const windowOpts = {height: 300, width: 500} @@ -19,7 +19,7 @@ const UnlockFolders = React.memo(function UnlockFolders(p: ProxyProps) { windowTitle: 'UnlockFolders', }) - useSerializeProps(p, serialize, windowComponent, windowParam) + useSerializeProps(p, windowComponent, windowParam) return null }) diff --git a/shared/unlock-folders/remote-serializer.desktop.tsx b/shared/unlock-folders/remote-serializer.desktop.tsx deleted file mode 100644 index b46524dc672d..000000000000 --- a/shared/unlock-folders/remote-serializer.desktop.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import * as T from '@/constants/types' -import type * as ConfigConstants from '@/stores/config' -import {produce} from 'immer' - -export type ProxyProps = { - darkMode: boolean - devices: ConfigConstants.State['unlockFoldersDevices'] - paperKeyError: string - waiting: boolean -} - -export type SerializeProps = ProxyProps -export type DeserializeProps = ProxyProps - -const initialState: DeserializeProps = { - darkMode: false, - devices: [], - paperKeyError: '', - waiting: false, -} - -export const serialize = (p: ProxyProps): Partial => p - -export const deserialize = ( - state: DeserializeProps = initialState, - props?: Partial -): DeserializeProps => { - if (!props) return state - - const {darkMode, devices, paperKeyError, waiting} = props - return produce(state, s => { - if (darkMode !== undefined) { - s.darkMode = darkMode - } - if (devices !== undefined) { - s.devices = T.castDraft(devices) - } - if (paperKeyError !== undefined) { - s.paperKeyError = paperKeyError - } - if (waiting !== undefined) { - s.waiting = waiting - } - }) -} From bcda79410bcb340dffbbcfd8a9127421633ac46d Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Fri, 20 Feb 2026 15:53:02 -0500 Subject: [PATCH 2/7] WIP --- shared/desktop/remote/component-loader.desktop.tsx | 3 ++- shared/menubar/index.desktop.tsx | 3 ++- shared/tracker/index.desktop.tsx | 12 ++++++------ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/shared/desktop/remote/component-loader.desktop.tsx b/shared/desktop/remote/component-loader.desktop.tsx index 1bab42c3971a..8763927c53ef 100644 --- a/shared/desktop/remote/component-loader.desktop.tsx +++ b/shared/desktop/remote/component-loader.desktop.tsx @@ -36,7 +36,8 @@ function RemoteComponentLoader

(p: Props

) { React.useEffect(() => { ipcRendererOn?.('KBprops', (_event: unknown, raw: unknown) => { - setValue(JSON.parse(raw as string) as P) + const parsed = JSON.parse(raw as string) as P + setTimeout(() => setValue(parsed), 1) }) R.remoteDispatch( RemoteGen.createRemoteWindowWantsProps({component: name, param: params}) diff --git a/shared/menubar/index.desktop.tsx b/shared/menubar/index.desktop.tsx index 4edd56295c83..5baf79f93cda 100644 --- a/shared/menubar/index.desktop.tsx +++ b/shared/menubar/index.desktop.tsx @@ -6,6 +6,7 @@ import * as React from 'react' import * as RemoteGen from '@/actions/remote-gen' import * as FsUtil from '@/util/kbfs' import * as TimestampUtil from '@/util/timestamp' +import Filename from '@/fs/common/filename' import KB2 from '@/util/electron.desktop' import OutOfDate from './out-of-date' import Upload from '@/fs/footer/upload' @@ -76,7 +77,7 @@ const HttpAvatar = (p: { }) => { const isDarkMode = useColorScheme() === 'dark' const typ = p.isTeam ? 'team' : 'user' - const src = `http://${p.httpSrvAddress}/av?typ=${typ}&name=${p.name}&format=square_${p.size}&mode=${isDarkMode ? 'dark' : 'light'}&token=${p.httpSrvToken}` + const src = `http://${p.httpSrvAddress}/av?typ=${typ}&name=${p.name}&format=square_192&mode=${isDarkMode ? 'dark' : 'light'}&token=${p.httpSrvToken}&count=0` return } const avatarStyle: React.CSSProperties = {borderRadius: '50%', flexShrink: 0} diff --git a/shared/tracker/index.desktop.tsx b/shared/tracker/index.desktop.tsx index 4077a4df6c52..f04ef5c6eef5 100644 --- a/shared/tracker/index.desktop.tsx +++ b/shared/tracker/index.desktop.tsx @@ -30,11 +30,11 @@ export type Props = { trackerUsername: string } -const avatarUrl = (httpSrvAddress: string, httpSrvToken: string, username: string, size: number, darkMode: boolean) => - `http://${httpSrvAddress}/av?typ=user&name=${username}&format=square_${size}&mode=${darkMode ? 'dark' : 'light'}&token=${httpSrvToken}` +const avatarUrl = (httpSrvAddress: string, httpSrvToken: string, username: string, darkMode: boolean) => + `http://${httpSrvAddress}/av?typ=user&name=${username}&format=square_192&mode=${darkMode ? 'dark' : 'light'}&token=${httpSrvToken}&count=0` -const teamAvatarUrl = (httpSrvAddress: string, httpSrvToken: string, teamname: string, size: number, darkMode: boolean) => - `http://${httpSrvAddress}/av?typ=team&name=${teamname}&format=square_${size}&mode=${darkMode ? 'dark' : 'light'}&token=${httpSrvToken}` +const teamAvatarUrl = (httpSrvAddress: string, httpSrvToken: string, teamname: string, darkMode: boolean) => + `http://${httpSrvAddress}/av?typ=team&name=${teamname}&format=square_192&mode=${darkMode ? 'dark' : 'light'}&token=${httpSrvToken}&count=0` const getButtons = (props: Props) => { const buttonClose = ( @@ -264,7 +264,7 @@ const TeamShowcase = (props: {name: string; httpSrvAddress: string; httpSrvToken return ( { Date: Fri, 20 Feb 2026 16:31:14 -0500 Subject: [PATCH 3/7] WIP --- shared/chat/inbox/row/index.tsx | 1 - shared/chat/inbox/row/small-team/index.tsx | 36 ++-- shared/menubar/files.desktop.tsx | 218 --------------------- shared/menubar/index.desktop.tsx | 121 ++++++++---- shared/menubar/remote-proxy.desktop.tsx | 4 + 5 files changed, 105 insertions(+), 275 deletions(-) delete mode 100644 shared/menubar/files.desktop.tsx diff --git a/shared/chat/inbox/row/index.tsx b/shared/chat/inbox/row/index.tsx index c47b53e3576c..e2998e330067 100644 --- a/shared/chat/inbox/row/index.tsx +++ b/shared/chat/inbox/row/index.tsx @@ -25,7 +25,6 @@ const makeRow = (item: ChatInboxRowItem, navKey: string, selected: boolean) => { case 'small': return ( { } const SmallTeamInner = (p: Props) => { - const {layoutName, layoutIsTeam, layoutSnippet, isSelected, layoutTime, layoutSnippetDecoration, isInWidget} = p + const {layoutName, layoutIsTeam, layoutSnippet, isSelected, layoutTime, layoutSnippetDecoration} = p const you = useCurrentUserState(s => s.username) @@ -41,8 +40,8 @@ const SmallTeamInner = (p: Props) => { Chat.useChatContext( C.useShallow(s => { const typingSnippet = (() => { - const typers = !isInWidget ? s.typing : undefined - if (!typers?.size) return undefined + const typers = s.typing + if (!typers.size) return undefined if (typers.size === 1) { const [t] = typers return `${t} is typing...` @@ -59,10 +58,6 @@ const SmallTeamInner = (p: Props) => { ? (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, @@ -72,7 +67,7 @@ const SmallTeamInner = (p: Props) => { navigateToThread: s.dispatch.navigateToThread, snippet, snippetDecoration, - teamDisplayName, + teamDisplayName: metaTeamname, timestamp: meta.timestamp || layoutTime || 0, } }) @@ -106,13 +101,11 @@ const SmallTeamInner = (p: Props) => { navigateToThread('inboxSmall') })) - const backgroundColor = isInWidget - ? Kb.Styles.globalColors.white - : isSelected - ? Kb.Styles.globalColors.blue - : Kb.Styles.isPhone && !Kb.Styles.isTablet - ? Kb.Styles.globalColors.fastBlank - : Kb.Styles.globalColors.blueGrey + const backgroundColor = isSelected + ? Kb.Styles.globalColors.blue + : Kb.Styles.isPhone && !Kb.Styles.isTablet + ? Kb.Styles.globalColors.fastBlank + : Kb.Styles.globalColors.blueGrey const teamname = teamDisplayName ? teamDisplayName.split('#')[0] ?? '' : '' const participantOne = teamname ? '' : participants[0] ?? '' @@ -124,7 +117,7 @@ const SmallTeamInner = (p: Props) => { onClick={onSelectConversation} className={Kb.Styles.classNames('small-row', {selected: isSelected})} style={ - isInWidget || Kb.Styles.isTablet + Kb.Styles.isTablet ? Kb.Styles.collapseStyles([styles.container, {backgroundColor}]) : styles.container } @@ -146,7 +139,6 @@ const SmallTeamInner = (p: Props) => { { type TopLineProps = { isSelected: boolean - isInWidget: boolean hasUnread: boolean hasBadge: boolean backgroundColor: string @@ -180,8 +171,7 @@ type TopLineProps = { } const TopLine = (p: TopLineProps) => { - const {isSelected, isInWidget, hasUnread, hasBadge, backgroundColor, teamDisplayName, participants, timestamp} = p - const showGear = !isInWidget + const {isSelected, hasUnread, hasBadge, backgroundColor, teamDisplayName, participants, timestamp} = p const showBold = !isSelected && hasUnread const subColor = isSelected ? Kb.Styles.globalColors.white @@ -221,7 +211,7 @@ const TopLine = (p: TopLineProps) => { return ( - {showGear && showingPopup && popup} + {showingPopup && popup} {teamDisplayName ? ( @@ -251,7 +241,7 @@ const TopLine = (p: TopLineProps) => { {timestampText} - {!Kb.Styles.isMobile && showGear && ( + {!Kb.Styles.isMobile && ( void -} - -type FileUpdatesProps = { - updates: Array -} - -export type UserTlfUpdateRowProps = { - onClickAvatar: () => void - onSelectPath: () => void - path: T.FS.Path - writer: string - tlfType: T.FS.TlfType - participants: Array - teamname: string - timestamp: string - tlf: string - updates: Array - username: string -} - -type FilesPreviewProps = { - userTlfUpdates: Array -} - -export const FileUpdate = (props: FileUpdateProps) => ( - - - - {props.uploading && ( - - - - )} - - - -) - -type FileUpdatesHocProps = { - onShowAll: () => void - isShowingAll: boolean -} - -type ShowAllProps = FileUpdatesHocProps & { - numUpdates: number -} - -const FileUpdatesShowAll = (props: ShowAllProps) => ( - - - -) - -const defaultNumFileOptionsShown = 3 - -const FileUpdates = (props: FileUpdatesProps & FileUpdatesHocProps) => ( - - {props.updates.slice(0, props.isShowingAll ? props.updates.length : defaultNumFileOptionsShown).map(u => ( - - ))} - {props.updates.length > defaultNumFileOptionsShown && !props.isShowingAll && ( - - )} - -) - -const ComposedFileUpdates = (props: FileUpdatesProps) => { - const [isShowingAll, setIsShowingAll] = React.useState(false) - const onShowAll = () => setIsShowingAll(prev => !prev) - return -} - -const UserTlfUpdateRow = (props: UserTlfUpdateRowProps) => ( - - - - - - - {props.timestamp} - - - - - in  - - - {props.tlfType === T.FS.TlfType.Team ? ( - props.teamname - ) : props.tlfType === T.FS.TlfType.Public ? ( - - {props.participants.join(',')} - - - ) : ( - `${props.participants.join(',')}` - )} - - - - - -) - -export const FilesPreview = (props: FilesPreviewProps) => ( - - - - Recent files - - - - {props.userTlfUpdates.map(r => { - return - })} - - -) - -const styles = Kb.Styles.styleSheetCreate( - () => - ({ - buttonContainer: { - marginTop: Kb.Styles.globalMargins.tiny, - }, - fileUpdateRow: { - marginTop: Kb.Styles.globalMargins.xtiny, - paddingRight: Kb.Styles.globalMargins.large, - }, - fullWidth: { - // needed to avoid icon being pinched - width: '100%', - }, - iconBadge: { - height: 12, - width: 12, - }, - iconBadgeBox: { - marginLeft: -12, - marginRight: 12, - marginTop: 12, - width: 0, - zIndex: 100, - }, - iconStyle: { - flexShrink: 0, - height: 16, - marginRight: Kb.Styles.globalMargins.xtiny, - position: 'relative', - top: 1, - width: 16, - }, - tlfContainer: { - backgroundColor: Kb.Styles.globalColors.white, - color: Kb.Styles.globalColors.black, - paddingBottom: Kb.Styles.globalMargins.tiny, - paddingTop: Kb.Styles.globalMargins.tiny, - }, - tlfParticipants: { - fontSize: 12, - }, - tlfRowAvatar: { - marginRight: Kb.Styles.globalMargins.tiny, - }, - tlfRowContainer: { - paddingBottom: Kb.Styles.globalMargins.tiny, - paddingLeft: Kb.Styles.globalMargins.tiny, - paddingTop: Kb.Styles.globalMargins.tiny, - }, - tlfSectionHeader: { - backgroundColor: Kb.Styles.globalColors.blueGrey, - color: Kb.Styles.globalColors.black_50, - paddingBottom: Kb.Styles.globalMargins.xtiny, - paddingLeft: Kb.Styles.globalMargins.tiny, - paddingTop: Kb.Styles.globalMargins.xtiny, - }, - tlfSectionHeaderContainer: { - backgroundColor: Kb.Styles.globalColors.white, - }, - tlfTime: { - marginRight: Kb.Styles.globalMargins.tiny, - }, - tlfTopLine: { - justifyContent: 'space-between', - }, - }) as const -) diff --git a/shared/menubar/index.desktop.tsx b/shared/menubar/index.desktop.tsx index 5baf79f93cda..31937382863e 100644 --- a/shared/menubar/index.desktop.tsx +++ b/shared/menubar/index.desktop.tsx @@ -51,6 +51,7 @@ export type Props = { endEstimate?: number fileName?: string files: number + following: ReadonlyArray httpSrvAddress: string httpSrvToken: string kbfsDaemonStatus: KbfsDaemonStatus @@ -115,29 +116,50 @@ const ChatRow = (p: {conv: Conversation; httpSrvAddress: string; httpSrvToken: s const isTeam = conv.teamType !== 'adhoc' const name = isTeam ? conv.tlfname || '' : conv.participants?.filter(u => u !== username).join(', ') || conv.tlfname || '' const avatarName = isTeam ? conv.tlfname || '' : conv.participants?.find(u => u !== username) || '' + const timestamp = conv.timestamp ? TimestampUtil.formatTimeForConversationList(conv.timestamp) : '' return ( R.remoteDispatch(RemoteGen.createOpenChatFromWidget({conversationIDKey: conv.conversationIDKey}))} style={styles.chatRow} > - + - - {isTeam && conv.channelname ? `${name}#${conv.channelname}` : name} - - {conv.hasBadge && } + + + {isTeam && conv.channelname ? `${name}#${conv.channelname}` : name} + + {conv.hasBadge && } + + {!!timestamp && ( + + {timestamp} + + )} {!!conv.snippetDecorated && ( - + {conv.snippetDecorated} )} @@ -178,15 +200,43 @@ const FileUpdate = (p: {path: T.FS.Path; uploading: boolean; onClick: () => void )} - - {T.FS.getPathName(p.path)} - + ) -const FilesPreview = (p: {remoteTlfUpdates: ReadonlyArray; httpSrvAddress: string; httpSrvToken: string}) => { - const {remoteTlfUpdates, httpSrvAddress, httpSrvToken} = p +const defaultNumFileOptionsShown = 3 + +const FileUpdates = (p: {updates: ReadonlyArray<{path: T.FS.Path; uploading: boolean}>}) => { + const [isShowingAll, setIsShowingAll] = React.useState(false) + const shown = isShowingAll ? p.updates : p.updates.slice(0, defaultNumFileOptionsShown) + return ( + + {shown.map(u => ( + u.path && R.remoteDispatch(RemoteGen.createOpenFilesFromWidget({path: u.path}))} + /> + ))} + {p.updates.length > defaultNumFileOptionsShown && !isShowingAll && ( + + setIsShowingAll(true)} + small={true} + type="Dim" + /> + + )} + + ) +} + +const FilesPreview = (p: {remoteTlfUpdates: ReadonlyArray; following: ReadonlyArray; httpSrvAddress: string; httpSrvToken: string}) => { + const {remoteTlfUpdates, following, httpSrvAddress, httpSrvToken} = p + const followingSet = React.useMemo(() => new Set(following), [following]) return ( @@ -210,7 +260,13 @@ const FilesPreview = (p: {remoteTlfUpdates: ReadonlyArray; htt /> - {update.writer} + + {update.writer} + {TimestampUtil.formatTimeForConversationList(update.timestamp)} @@ -226,20 +282,16 @@ const FilesPreview = (p: {remoteTlfUpdates: ReadonlyArray; htt {tlfType === T.FS.TlfType.Team ? teamname : tlfType === T.FS.TlfType.Public - ? `${(participants || []).join(',')} (public)` + ? ( + + {(participants || []).join(',')} + + + ) : (participants || []).join(',')} - - {update.updates.map(u => ( - u.path && R.remoteDispatch(RemoteGen.createOpenFilesFromWidget({path: u.path}))} - /> - ))} - + ) @@ -410,7 +462,7 @@ const IconBar = (p: Props & {showBadges?: boolean}) => { const badgeTypesInHeader = [C.Tabs.peopleTab, C.Tabs.chatTab, C.Tabs.fsTab, C.Tabs.teamsTab] as const const badgesInMenu = [C.Tabs.gitTab, C.Tabs.devicesTab, C.Tabs.settingsTab] as const const LoggedIn = (p: Props) => { - const {endEstimate, files, kbfsDaemonStatus, totalSyncingBytes, fileName} = p + const {endEstimate, files, following, kbfsDaemonStatus, totalSyncingBytes, fileName} = p const {outOfDate, windowShownCount, conversationsToSend, remoteTlfUpdates} = p const {httpSrvAddress, httpSrvToken, username} = p @@ -436,6 +488,7 @@ const LoggedIn = (p: Props) => { {kbfsDaemonStatus.rpcStatus === T.FS.KbfsDaemonRpcStatus.Connected ? ( @@ -604,6 +657,7 @@ const styles = Kb.Styles.styleSheetCreate(() => ({ right: -2, top: -4, }, + bold: {...Kb.Styles.globalStyles.fontBold}, buttonContainer: { marginBottom: Kb.Styles.globalMargins.tiny, marginTop: Kb.Styles.globalMargins.tiny, @@ -627,14 +681,17 @@ const styles = Kb.Styles.styleSheetCreate(() => ({ chatRowInner: { alignItems: 'center', paddingBottom: Kb.Styles.globalMargins.xtiny, - paddingLeft: Kb.Styles.globalMargins.tiny, - paddingRight: Kb.Styles.globalMargins.tiny, + paddingLeft: Kb.Styles.globalMargins.xsmall, + paddingRight: Kb.Styles.globalMargins.xsmall, paddingTop: Kb.Styles.globalMargins.xtiny, }, chatRowName: {flexShrink: 1}, - chatRowNameContainer: {alignItems: 'center'}, - chatRowText: {flexGrow: 1, flexShrink: 1, overflow: 'hidden'}, + chatRowNameContainer: {alignItems: 'center', justifyContent: 'space-between'}, + chatRowNameLeft: {alignItems: 'center', flexShrink: 1, overflow: 'hidden'}, + chatRowText: {flexGrow: 1, flexShrink: 1, marginLeft: Kb.Styles.globalMargins.tiny, overflow: 'hidden'}, chatSnippet: {color: Kb.Styles.globalColors.black_50}, + chatSnippetUnread: {color: Kb.Styles.globalColors.black}, + chatTimestamp: {color: Kb.Styles.globalColors.black_50, flexShrink: 0, marginLeft: Kb.Styles.globalMargins.tiny}, fileFullWidth: {width: '100%'}, fileIcon: { flexShrink: 0, @@ -646,10 +703,6 @@ const styles = Kb.Styles.styleSheetCreate(() => ({ }, fileIconBadge: {height: 12, width: 12}, fileIconBadgeBox: {marginLeft: -12, marginRight: 12, marginTop: 12, width: 0, zIndex: 100}, - fileName: Kb.Styles.platformStyles({ - common: {flexShrink: 1}, - isElectron: {wordBreak: 'break-all'}, - }), fileUpdateRow: { marginTop: Kb.Styles.globalMargins.xtiny, paddingRight: Kb.Styles.globalMargins.large, @@ -666,6 +719,7 @@ const styles = Kb.Styles.styleSheetCreate(() => ({ marginBottom: 12, }, navIcons: {paddingLeft: Kb.Styles.globalMargins.xtiny, paddingRight: Kb.Styles.globalMargins.xtiny}, + showMoreContainer: {marginTop: Kb.Styles.globalMargins.tiny}, tlfContainer: { backgroundColor: Kb.Styles.globalColors.white, color: Kb.Styles.globalColors.black, @@ -688,6 +742,7 @@ const styles = Kb.Styles.styleSheetCreate(() => ({ tlfSectionHeaderContainer: {backgroundColor: Kb.Styles.globalColors.white}, tlfTime: {marginRight: Kb.Styles.globalMargins.tiny}, tlfTopLine: {justifyContent: 'space-between'}, + tlfWriterFollowing: {color: Kb.Styles.globalColors.greenDark}, topRow: { borderTopLeftRadius: Kb.Styles.globalMargins.xtiny, borderTopRightRadius: Kb.Styles.globalMargins.xtiny, diff --git a/shared/menubar/remote-proxy.desktop.tsx b/shared/menubar/remote-proxy.desktop.tsx index bff9633ed8e8..1ccbfa9213b8 100644 --- a/shared/menubar/remote-proxy.desktop.tsx +++ b/shared/menubar/remote-proxy.desktop.tsx @@ -11,6 +11,7 @@ import {useColorScheme} from 'react-native' import * as FS from '@/stores/fs' import {useFSState} from '@/stores/fs' import {useCurrentUserState} from '@/stores/current-user' +import {useFollowerState} from '@/stores/followers' import {useDaemonState} from '@/stores/daemon' import {useDarkModeState} from '@/stores/darkmode' import {useNotifState} from '@/stores/notifications' @@ -163,6 +164,8 @@ const MenubarRemoteProxy = React.memo(function MenubarRemoteProxy() { } const daemonHandshakeState = useDaemonState(s => s.handshakeState) + const followingSet = useFollowerState(s => s.following) + const following = React.useMemo(() => [...followingSet], [followingSet]) // Convert navBadges Map to plain object const navBadges: {[tab: string]: number} = React.useMemo(() => { @@ -180,6 +183,7 @@ const MenubarRemoteProxy = React.memo(function MenubarRemoteProxy() { darkMode: isDarkMode, desktopAppBadgeCount, diskSpaceStatus, + following, httpSrvAddress: httpSrv.address, httpSrvToken: httpSrv.token, kbfsDaemonStatus, From 5f863a3e777e09a184d377676b28391ea02de07f Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Fri, 20 Feb 2026 16:37:32 -0500 Subject: [PATCH 4/7] WIP --- shared/menubar/index.desktop.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shared/menubar/index.desktop.tsx b/shared/menubar/index.desktop.tsx index 31937382863e..9d4acd6bffcd 100644 --- a/shared/menubar/index.desktop.tsx +++ b/shared/menubar/index.desktop.tsx @@ -262,7 +262,7 @@ const FilesPreview = (p: {remoteTlfUpdates: ReadonlyArray; fol {update.writer} @@ -743,6 +743,7 @@ const styles = Kb.Styles.styleSheetCreate(() => ({ tlfTime: {marginRight: Kb.Styles.globalMargins.tiny}, tlfTopLine: {justifyContent: 'space-between'}, tlfWriterFollowing: {color: Kb.Styles.globalColors.greenDark}, + tlfWriterNotFollowing: {color: Kb.Styles.globalColors.blueDark}, topRow: { borderTopLeftRadius: Kb.Styles.globalMargins.xtiny, borderTopRightRadius: Kb.Styles.globalMargins.xtiny, From 386ada4f2e26e6d42af967bcb6f613bca678afed Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Fri, 20 Feb 2026 16:48:13 -0500 Subject: [PATCH 5/7] WIP --- shared/tracker/index.desktop.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/shared/tracker/index.desktop.tsx b/shared/tracker/index.desktop.tsx index f04ef5c6eef5..005511e350fb 100644 --- a/shared/tracker/index.desktop.tsx +++ b/shared/tracker/index.desktop.tsx @@ -1,5 +1,6 @@ import * as Kb from '@/common-adapters' import type * as T from '@/constants/types' +import openUrl from '@/util/open-url' import {useColorScheme} from 'react-native' export type Props = { @@ -228,10 +229,7 @@ const AssertionRow = (props: {assertion: T.Tracker.Assertion}) => { { - // handled via openUrl in the full app, but in remote window just open directly - window.open(a.siteURL, '_blank') - } : undefined} + onClick={a.siteURL ? () => openUrl(a.siteURL) : undefined} style={Kb.Styles.collapseStyles([ styles.assertionValue, a.state === 'revoked' && styles.strikeThrough, @@ -246,6 +244,7 @@ const AssertionRow = (props: {assertion: T.Tracker.Assertion}) => { type={stateToIcon(a.state)} fontSize={20} color={assertionColorToColor(a.color)} + onClick={a.proofURL ? () => openUrl(a.proofURL) : undefined} /> {!!a.metas.length && ( From 4d9b12cb9c6e7ec74f69fef32bf5bfc5f15f0725 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Fri, 20 Feb 2026 17:08:34 -0500 Subject: [PATCH 6/7] WIP --- shared/menubar/index.desktop.tsx | 85 ++++++++++++++------------------ 1 file changed, 38 insertions(+), 47 deletions(-) diff --git a/shared/menubar/index.desktop.tsx b/shared/menubar/index.desktop.tsx index 9d4acd6bffcd..686d578de00c 100644 --- a/shared/menubar/index.desktop.tsx +++ b/shared/menubar/index.desktop.tsx @@ -123,7 +123,7 @@ const ChatRow = (p: {conv: Conversation; httpSrvAddress: string; httpSrvToken: s onClick={() => R.remoteDispatch(RemoteGen.createOpenChatFromWidget({conversationIDKey: conv.conversationIDKey}))} style={styles.chatRow} > - + - - + + {isTeam && conv.channelname ? `${name}#${conv.channelname}` : name} @@ -144,7 +144,7 @@ const ChatRow = (p: {conv: Conversation; httpSrvAddress: string; httpSrvToken: s type="BodyTiny" style={Kb.Styles.collapseStyles([ styles.chatTimestamp, - conv.hasUnread && styles.bold, + conv.hasUnread && Kb.Styles.globalStyles.fontBold, ])} > {timestamp} @@ -157,7 +157,7 @@ const ChatRow = (p: {conv: Conversation; httpSrvAddress: string; httpSrvToken: s lineClamp={1} style={Kb.Styles.collapseStyles([ conv.hasUnread ? styles.chatSnippetUnread : styles.chatSnippet, - conv.hasUnread && styles.bold, + conv.hasUnread && Kb.Styles.globalStyles.fontBold, ])} > {conv.snippetDecorated} @@ -250,13 +250,12 @@ const FilesPreview = (p: {remoteTlfUpdates: ReadonlyArray; fol const {participants, teamname} = FsUtil.tlfToParticipantsOrTeamname(tlf) const tlfType = T.FS.getPathVisibility(update.tlf) || T.FS.TlfType.Private return ( - + @@ -434,16 +433,7 @@ const IconBar = (p: Props & {showBadges?: boolean}) => { )) : null} - + { return ( <> - + { httpSrvToken={httpSrvToken} /> ) : ( - + )} @@ -533,7 +523,8 @@ const LoggedOut = (p: {daemonHandshakeState: T.Config.DaemonHandshakeState; logg direction="vertical" fullWidth={true} fullHeight={true} - style={{alignItems: 'center', justifyContent: 'center', padding: Kb.Styles.globalMargins.small}} + centerChildren={true} + style={styles.loggedOutContainer} > - + {text} {fullyLoggedOut ? ( @@ -581,12 +572,12 @@ const MenubarRender = (p: Props) => { const TabView = (p: {title: string; iconType: Kb.IconType; count?: number}) => { const {count, iconType, title} = p return ( - - + + {!!count && } - + {title} @@ -616,12 +607,7 @@ const BadgeIcon = (p: {tab: Tabs; countMap: {[tab: string]: number}; openApp: (t } return ( - + ({ right: -2, top: -4, }, - bold: {...Kb.Styles.globalStyles.fontBold}, + badgeIconContainer: Kb.Styles.platformStyles({ + isElectron: {...Kb.Styles.desktopStyles.clickable, position: 'relative'}, + }), buttonContainer: { marginBottom: Kb.Styles.globalMargins.tiny, marginTop: Kb.Styles.globalMargins.tiny, @@ -666,7 +654,6 @@ const styles = Kb.Styles.styleSheetCreate(() => ({ backgroundColor: Kb.Styles.globalColors.blue, borderRadius: 4, height: 8, - marginLeft: Kb.Styles.globalMargins.xtiny, width: 8, }, chatContainer: { @@ -678,17 +665,11 @@ const styles = Kb.Styles.styleSheetCreate(() => ({ ...Kb.Styles.desktopStyles.clickable, }, }), - chatRowInner: { - alignItems: 'center', - paddingBottom: Kb.Styles.globalMargins.xtiny, - paddingLeft: Kb.Styles.globalMargins.xsmall, - paddingRight: Kb.Styles.globalMargins.xsmall, - paddingTop: Kb.Styles.globalMargins.xtiny, - }, + chatRowInner: Kb.Styles.padding(Kb.Styles.globalMargins.xtiny, Kb.Styles.globalMargins.xsmall), chatRowName: {flexShrink: 1}, - chatRowNameContainer: {alignItems: 'center', justifyContent: 'space-between'}, - chatRowNameLeft: {alignItems: 'center', flexShrink: 1, overflow: 'hidden'}, - chatRowText: {flexGrow: 1, flexShrink: 1, marginLeft: Kb.Styles.globalMargins.tiny, overflow: 'hidden'}, + chatRowNameContainer: {justifyContent: 'space-between'}, + chatRowNameLeft: {flexShrink: 1, overflow: 'hidden'}, + chatRowText: {flexGrow: 1, flexShrink: 1, overflow: 'hidden'}, chatSnippet: {color: Kb.Styles.globalColors.black_50}, chatSnippetUnread: {color: Kb.Styles.globalColors.black}, chatTimestamp: {color: Kb.Styles.globalColors.black_50, flexShrink: 0, marginLeft: Kb.Styles.globalMargins.tiny}, @@ -707,19 +688,29 @@ const styles = Kb.Styles.styleSheetCreate(() => ({ marginTop: Kb.Styles.globalMargins.xtiny, paddingRight: Kb.Styles.globalMargins.large, }, - flexOne: {flexGrow: 1}, footer: {width: 360}, + hamburgerContainer: Kb.Styles.platformStyles({ + isElectron: { + ...Kb.Styles.desktopStyles.clickable, + marginRight: Kb.Styles.globalMargins.tiny, + position: 'relative', + }, + }), headerBadgesContainer: { flex: 1, justifyContent: 'center', - marginLeft: 24 + 8, + marginLeft: Kb.Styles.globalMargins.mediumLarge, }, + loadingContainer: {height: 200}, + loggedOutContainer: {padding: Kb.Styles.globalMargins.small}, + loggedOutText: {alignSelf: 'center', marginTop: 6}, logo: { alignSelf: 'center', - marginBottom: 12, + marginBottom: Kb.Styles.globalMargins.xsmall, }, navIcons: {paddingLeft: Kb.Styles.globalMargins.xtiny, paddingRight: Kb.Styles.globalMargins.xtiny}, showMoreContainer: {marginTop: Kb.Styles.globalMargins.tiny}, + tabIconContainer: {position: 'relative'}, tlfContainer: { backgroundColor: Kb.Styles.globalColors.white, color: Kb.Styles.globalColors.black, @@ -750,8 +741,8 @@ const styles = Kb.Styles.styleSheetCreate(() => ({ flex: 1, maxHeight: 40, minHeight: 40, - paddingLeft: 8, - paddingRight: 8, + paddingLeft: Kb.Styles.globalMargins.tiny, + paddingRight: Kb.Styles.globalMargins.tiny, }, widgetContainer: { backgroundColor: Kb.Styles.globalColors.white, From 0a89011b8f098f87a80c52cadc8e8ca718078be5 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Fri, 20 Feb 2026 18:15:08 -0500 Subject: [PATCH 7/7] WIP --- .../remote/component-loader.desktop.tsx | 3 +- shared/menubar/remote-proxy.desktop.tsx | 75 ++++++++++++------- 2 files changed, 51 insertions(+), 27 deletions(-) diff --git a/shared/desktop/remote/component-loader.desktop.tsx b/shared/desktop/remote/component-loader.desktop.tsx index 8763927c53ef..03700094e744 100644 --- a/shared/desktop/remote/component-loader.desktop.tsx +++ b/shared/desktop/remote/component-loader.desktop.tsx @@ -36,7 +36,8 @@ function RemoteComponentLoader

(p: Props

) { React.useEffect(() => { ipcRendererOn?.('KBprops', (_event: unknown, raw: unknown) => { - const parsed = JSON.parse(raw as string) as P + const str = raw as string + const parsed = JSON.parse(str) as P setTimeout(() => setValue(parsed), 1) }) R.remoteDispatch( diff --git a/shared/menubar/remote-proxy.desktop.tsx b/shared/menubar/remote-proxy.desktop.tsx index 1ccbfa9213b8..c7b40230cf39 100644 --- a/shared/menubar/remote-proxy.desktop.tsx +++ b/shared/menubar/remote-proxy.desktop.tsx @@ -100,7 +100,25 @@ const MenubarRemoteProxy = React.memo(function MenubarRemoteProxy() { return {desktopAppBadgeCount, navBadges, widgetBadge} }) ) - const widgetList = Chat.useChatState(s => s.inboxLayout?.widgetList) + const {widgetList, inboxRefresh, ensureWidgetMetas} = Chat.useChatState( + C.useShallow(s => ({ + ensureWidgetMetas: s.dispatch.ensureWidgetMetas, + inboxRefresh: s.dispatch.inboxRefresh, + widgetList: s.inboxLayout?.widgetList, + })) + ) + // Ensure widgetList is populated by triggering an inbox refresh if needed + React.useEffect(() => { + if (loggedIn && !widgetList) { + inboxRefresh('widgetRefresh') + } + }, [loggedIn, widgetList, inboxRefresh]) + // Ensure conversation metadata is loaded for widget conversations + React.useEffect(() => { + if (widgetList) { + ensureWidgetMetas() + } + }, [widgetList, ensureWidgetMetas]) const isDarkMode = useColorScheme() === 'dark' const {diskSpaceStatus, showingBanner} = overallSyncStatus const kbfsEnabled = sfmi.driverStatus.type === T.FS.DriverStatusType.Enabled @@ -110,44 +128,49 @@ const MenubarRemoteProxy = React.memo(function MenubarRemoteProxy() { [tlfUpdates, uploads] ) - const [remakeChat, setRemakeChat] = React.useState(0) + const [conversationsToSend, setConversationsToSend] = React.useState>([]) React.useEffect(() => { - const unsubs = widgetList?.map(v => { + if (!widgetList) return + + const computeConversations = () => { + const result: Array = [] + widgetList.forEach(v => { + const cs = Chat.getConvoState(v.convID) + if (!cs.isMetaGood()) return + const {badge, unread, participants, meta: c} = cs + result.push({ + channelname: c.channelname, + conversationIDKey: v.convID, + snippetDecorated: c.snippetDecorated, + teamType: c.teamType, + timestamp: c.timestamp, + tlfname: c.tlfname, + ...(badge > 0 ? {hasBadge: true as const} : {}), + ...(unread > 0 ? {hasUnread: true as const} : {}), + ...(participants.name.length ? {participants: participants.name.slice(0, 3)} : {}), + }) + }) + setConversationsToSend(result) + } + + computeConversations() + + const unsubs = widgetList.map(v => { + Chat.getConvoState(v.convID) return Chat.chatStores.get(v.convID)?.subscribe((s, old) => { if (convoDiff(s, old)) { - setRemakeChat(c => c + 1) + computeConversations() } }) }) return () => { - for (const unsub of unsubs ?? []) { + for (const unsub of unsubs) { unsub?.() } } }, [widgetList]) - const conversationsToSend: ReadonlyArray = React.useMemo( - () => - widgetList?.map(v => { - remakeChat // implied dependency - const {badge, unread, participants, meta} = Chat.getConvoState(v.convID) - const c = meta - return { - channelname: c.channelname, - conversationIDKey: v.convID, - snippetDecorated: c.snippetDecorated, - teamType: c.teamType, - timestamp: c.timestamp, - tlfname: c.tlfname, - ...(badge > 0 ? {hasBadge: true as const} : {}), - ...(unread > 0 ? {hasUnread: true as const} : {}), - ...(participants.name.length ? {participants: participants.name.slice(0, 3)} : {}), - } - }) ?? [], - [widgetList, remakeChat] - ) - // Filter some data based on visible users. // We just use syncingPaths rather than merging with writingToJournal here // since journal status comes a bit slower, and merging the two causes