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 && ( = { - 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) => { + const str = raw as string + const parsed = JSON.parse(str) as P + setTimeout(() => setValue(parsed), 1) + }) + 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 +80,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 +91,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/files.desktop.tsx b/shared/menubar/files.desktop.tsx deleted file mode 100644 index ea320164a0b2..000000000000 --- a/shared/menubar/files.desktop.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import * as React from 'react' -import * as Kb from '@/common-adapters' -import * as T from '@/constants/types' -import {Filename} from '@/fs/common' - -type FileUpdateProps = { - path: T.FS.Path - tlfType: T.FS.TlfType - uploading: boolean - onClick: () => 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 c81b75b075ee..686d578de00c 100644 --- a/shared/menubar/index.desktop.tsx +++ b/shared/menubar/index.desktop.tsx @@ -4,8 +4,9 @@ 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 Filename from '@/fs/common/filename' import KB2 from '@/util/electron.desktop' import OutOfDate from './out-of-date' import Upload from '@/fs/footer/upload' @@ -14,29 +15,73 @@ 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 + following: ReadonlyArray + 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_192&mode=${isDarkMode ? 'dark' : 'light'}&token=${p.httpSrvToken}&count=0` + return } +const avatarStyle: React.CSSProperties = {borderRadius: '50%', flexShrink: 0} const ArrowTick = () => { const isDarkMode = useColorScheme() === 'dark' @@ -50,6 +95,7 @@ const ArrowTick = () => { /> ) } + type UWCDProps = { endEstimate?: number files: number @@ -60,24 +106,204 @@ 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) || '' + const timestamp = conv.timestamp ? TimestampUtil.formatTimeForConversationList(conv.timestamp) : '' - return + return ( + R.remoteDispatch(RemoteGen.createOpenChatFromWidget({conversationIDKey: conv.conversationIDKey}))} + style={styles.chatRow} + > + + + + + + + {isTeam && conv.channelname ? `${name}#${conv.channelname}` : name} + + {conv.hasBadge && } + + {!!timestamp && ( + + {timestamp} + + )} + + {!!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 && ( + + + + )} + + + +) + +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 ( + + + + 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(',')} + + + ) + : (participants || []).join(',')} + + + + + + ) + })} + + + ) } 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 +313,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 +334,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 +354,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 +383,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 +414,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 ( { )) : null} - + { 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 const refreshUserFileEdits = C.useThrottledCallback(() => { R.remoteDispatch(RemoteGen.createUserFileEditsLoad()) @@ -264,12 +467,23 @@ const LoggedIn = (p: Props) => { return ( <> - - + + {kbfsDaemonStatus.rpcStatus === T.FS.KbfsDaemonRpcStatus.Connected ? ( - + ) : ( - + )} @@ -299,10 +513,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 ( <> @@ -311,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 ? ( @@ -359,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} @@ -383,9 +596,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' @@ -394,12 +607,7 @@ const BadgeIcon = (p: {tab: Tabs; countMap: ReadonlyMap; openApp } return ( - + ({ right: -2, top: -4, }, - flexOne: {flexGrow: 1}, + badgeIconContainer: Kb.Styles.platformStyles({ + isElectron: {...Kb.Styles.desktopStyles.clickable, position: 'relative'}, + }), + buttonContainer: { + marginBottom: Kb.Styles.globalMargins.tiny, + marginTop: Kb.Styles.globalMargins.tiny, + }, + chatBadge: { + backgroundColor: Kb.Styles.globalColors.blue, + borderRadius: 4, + height: 8, + width: 8, + }, + chatContainer: { + backgroundColor: Kb.Styles.globalColors.white, + color: Kb.Styles.globalColors.black, + }, + chatRow: Kb.Styles.platformStyles({ + isElectron: { + ...Kb.Styles.desktopStyles.clickable, + }, + }), + chatRowInner: Kb.Styles.padding(Kb.Styles.globalMargins.xtiny, Kb.Styles.globalMargins.xsmall), + chatRowName: {flexShrink: 1}, + 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}, + 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}, + fileUpdateRow: { + marginTop: Kb.Styles.globalMargins.xtiny, + paddingRight: Kb.Styles.globalMargins.large, + }, 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, + 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'}, + tlfWriterFollowing: {color: Kb.Styles.globalColors.greenDark}, + tlfWriterNotFollowing: {color: Kb.Styles.globalColors.blueDark}, topRow: { borderTopLeftRadius: Kb.Styles.globalMargins.xtiny, borderTopRightRadius: Kb.Styles.globalMargins.xtiny, 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, 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..c7b40230cf39 100644 --- a/shared/menubar/remote-proxy.desktop.tsx +++ b/shared/menubar/remote-proxy.desktop.tsx @@ -6,20 +6,16 @@ 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 {useFollowerState} from '@/stores/followers' 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 +32,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 +78,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,14 +94,31 @@ 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 {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 @@ -124,34 +128,17 @@ 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) + const [conversationsToSend, setConversationsToSend] = React.useState>([]) React.useEffect(() => { - const unsubs = widgetList?.map(v => { - return Chat.chatStores.get(v.convID)?.subscribe((s, old) => { - if (convoDiff(s, old)) { - setRemakeChat(c => c + 1) - } - }) - }) - - return () => { - for (const unsub of unsubs ?? []) { - unsub?.() - } - } - }, [widgetList]) - - const conversationsToSend = React.useMemo( - () => - widgetList?.map(v => { - remakeChat // implied dependency - const {badge, unread, participants, meta} = Chat.getConvoState(v.convID) - const c = meta - return { + 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, @@ -161,56 +148,38 @@ const MenubarRemoteProxy = React.memo(function MenubarRemoteProxy() { ...(badge > 0 ? {hasBadge: true as const} : {}), ...(unread > 0 ? {hasUnread: true as const} : {}), ...(participants.name.length ? {participants: participants.name.slice(0, 3)} : {}), - } - }) ?? [], - [widgetList, remakeChat] - ) + }) + }) + setConversationsToSend(result) + } - // 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) - } + computeConversations() + + const unsubs = widgetList.map(v => { + Chat.getConvoState(v.convID) + return Chat.chatStores.get(v.convID)?.subscribe((s, old) => { + if (convoDiff(s, old)) { + computeConversations() + } + }) }) - 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]) + return () => { + for (const unsub of unsubs) { + unsub?.() + } + } + }, [widgetList]) + // 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, @@ -218,20 +187,28 @@ 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(() => { + const obj: {[tab: string]: number} = {} + for (const [k, v] of navBadgesMap) { + obj[k] = v + } + return obj + }, [navBadgesMap]) - const p: ProxyProps & WidgetProps = { + const p: Props & WidgetProps = { ...upDown, - avatarRefreshCounter: avatarRefreshCounterFiltered, conversationsToSend, daemonHandshakeState, darkMode: isDarkMode, desktopAppBadgeCount, diskSpaceStatus, - followers: followersFiltered, - following: followingFiltered, + following, httpSrvAddress: httpSrv.address, httpSrvToken: httpSrv.token, - infoMap: infoMapFiltered, kbfsDaemonStatus, kbfsEnabled, loggedIn, @@ -241,7 +218,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..005511e350fb 100644 --- a/shared/tracker/index.desktop.tsx +++ b/shared/tracker/index.desktop.tsx @@ -1,25 +1,29 @@ -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 openUrl from '@/util/open-url' +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 +31,23 @@ type Props = { trackerUsername: string } +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, 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 = ( - + ) const buttonAccept = ( - + ) const buttonChat = ( - + - + ) if (props.isYou) { @@ -66,108 +64,220 @@ 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 && ( + + )} + + openUrl(a.siteURL) : undefined} + style={Kb.Styles.collapseStyles([ + styles.assertionValue, + a.state === 'revoked' && styles.strikeThrough, + {color: assertionColorToTextColor(a.color)}, + ])} + > + {a.value} + + @{a.type} + + openUrl(a.proofURL) : undefined} + /> + + {!!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 +288,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 +305,41 @@ const Tracker = (props: Props) => { - + + + + {props.trackerUsername} + + - + {props.teamShowcase && ( {props.teamShowcase.map(t => ( - + ))} )} - {assertions} + {sortedAssertions?.map(a => )} {!!buttons.length && ( @@ -255,6 +371,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 +387,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 +397,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 +437,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 +448,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 +473,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 - } - }) -}