From 146f5f1684b765db91536b4dd8e41d46e76a3395 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Sun, 17 May 2026 17:17:00 -0400 Subject: [PATCH 01/28] Merge 5 stub platform pairs into unified .tsx files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pairs merged (desktop stub + native impl → single file with isMobile guard): - settings/make-icons.page - teams/add-members-wizard/add-contacts - teams/invite-by-contact/team-invite-by-contacts - provision/code-page/qr-scan/scanner - util/expo-image-picker Update explicit .native imports in input.native.tsx, misc.native.tsx, and fs-platform.native.tsx to point to the new merged files. --- .../input-area/normal/input.native.tsx | 2 +- .../code-page/qr-scan/scanner.desktop.tsx | 11 -- .../{scanner.native.tsx => scanner.tsx} | 7 +- shared/settings/make-icons.page.desktop.tsx | 128 ------------------ shared/settings/make-icons.page.native.tsx | 4 - shared/settings/make-icons.page.tsx | 86 ++++++++++++ shared/stores/fs-platform.native.tsx | 2 +- .../add-contacts.desktop.tsx | 4 - ...d-contacts.native.tsx => add-contacts.tsx} | 7 +- .../team-invite-by-contacts.desktop.tsx | 2 - ...native.tsx => team-invite-by-contacts.tsx} | 7 +- shared/util/expo-image-picker.desktop.tsx | 7 - ...icker.native.tsx => expo-image-picker.tsx} | 2 + shared/util/misc.native.tsx | 2 +- 14 files changed, 109 insertions(+), 162 deletions(-) delete mode 100644 shared/provision/code-page/qr-scan/scanner.desktop.tsx rename shared/provision/code-page/qr-scan/{scanner.native.tsx => scanner.tsx} (90%) delete mode 100644 shared/settings/make-icons.page.desktop.tsx delete mode 100644 shared/settings/make-icons.page.native.tsx create mode 100644 shared/settings/make-icons.page.tsx delete mode 100644 shared/teams/add-members-wizard/add-contacts.desktop.tsx rename shared/teams/add-members-wizard/{add-contacts.native.tsx => add-contacts.tsx} (95%) delete mode 100644 shared/teams/invite-by-contact/team-invite-by-contacts.desktop.tsx rename shared/teams/invite-by-contact/{team-invite-by-contacts.native.tsx => team-invite-by-contacts.tsx} (98%) delete mode 100644 shared/util/expo-image-picker.desktop.tsx rename shared/util/{expo-image-picker.native.tsx => expo-image-picker.tsx} (97%) diff --git a/shared/chat/conversation/input-area/normal/input.native.tsx b/shared/chat/conversation/input-area/normal/input.native.tsx index b19e8a77cd39..7447f044d39a 100644 --- a/shared/chat/conversation/input-area/normal/input.native.tsx +++ b/shared/chat/conversation/input-area/normal/input.native.tsx @@ -22,7 +22,7 @@ import { } from '@/common-adapters/reanimated' import {formatDurationShort} from '@/util/timestamp' import {getTextStyle} from '@/common-adapters/text.styles' -import {launchCameraAsync, launchImageLibraryAsync} from '@/util/expo-image-picker.native' +import {launchCameraAsync, launchImageLibraryAsync} from '@/util/expo-image-picker' import {onHWKeyPressed, registerPasteImage, removeOnHWKeyPressed} from 'react-native-kb' import {pickDocumentsAsync} from '@/util/expo-document-picker.native' import {standardTransformer} from '../suggestors/common' diff --git a/shared/provision/code-page/qr-scan/scanner.desktop.tsx b/shared/provision/code-page/qr-scan/scanner.desktop.tsx deleted file mode 100644 index c94238fbd2e0..000000000000 --- a/shared/provision/code-page/qr-scan/scanner.desktop.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import type * as React from 'react' -import type * as Kb from '@/common-adapters' - -type Props = { - onBarCodeRead: (code: string) => void - notAuthorizedView: React.ReactElement | null - style: Kb.Styles.StylesCrossPlatform -} - -const QRScanner = (_p: Props): null => null -export default QRScanner diff --git a/shared/provision/code-page/qr-scan/scanner.native.tsx b/shared/provision/code-page/qr-scan/scanner.tsx similarity index 90% rename from shared/provision/code-page/qr-scan/scanner.native.tsx rename to shared/provision/code-page/qr-scan/scanner.tsx index 2e3eaa821302..e22a9b95f37c 100644 --- a/shared/provision/code-page/qr-scan/scanner.native.tsx +++ b/shared/provision/code-page/qr-scan/scanner.tsx @@ -8,7 +8,7 @@ type Props = { style: Kb.Styles.StylesCrossPlatform } -const QRScanner = (p: Props): React.ReactElement | null => { +const QRScannerMobile = (p: Props): React.ReactElement | null => { const [scanned, setScanned] = React.useState(false) const [permission, requestPermission] = useCameraPermissions() @@ -48,4 +48,9 @@ const styles = Kb.Styles.styleSheetCreate(() => ({ }, })) +const QRScanner = (p: Props): React.ReactElement | null => { + if (!isMobile) return null + return +} + export default QRScanner diff --git a/shared/settings/make-icons.page.desktop.tsx b/shared/settings/make-icons.page.desktop.tsx deleted file mode 100644 index b109dcef4724..000000000000 --- a/shared/settings/make-icons.page.desktop.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import * as Kb from '@/common-adapters' -// import * as C from '@/constants' -// import * as React from 'react' -// import KB2 from '@/util/electron.desktop' - -// const {DEVwriteMenuIcons} = KB2.functions -// setTimeout(() => { -// C.useRouterState.getState().dispatch.navigateAppend('makeIcons') -// }, 1000) -// -// const Icon = (p: {badge: number}) => { -// const {badge} = p -// const special = badge > 9 -// return ( -// -// -//
-// -// -// -// -// -// {special ? '+' : String(badge)} -// -// -// -//
-//
-// ) -// } -const Icon = (_p: {badge: number}) => null -const DEVwriteMenuIcons = (() => {}) as undefined | (() => void) - -const Screen = __DEV__ - ? () => { - const onSave = () => { - const oldbg = document.body.style.backgroundColor - document.body.style.backgroundColor = 'transparent' - - const dte = document.getElementById('divToExport') - if (!dte) return - const copy = dte.cloneNode(true) as HTMLElement - copy.id = 'iconCopy' - document.body.appendChild(copy) - - const root = document.getElementById('root') - if (!root) return - root.remove() - setTimeout(() => { - DEVwriteMenuIcons?.() - setTimeout(() => { - document.body.appendChild(root) - document.body.style.backgroundColor = oldbg - copy.remove() - }, 500) - }, 100) - } - - const icons = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(badge => ) - - return ( - - -
- {icons} -
- - {icons} - -
- ) - } - : () => null - -export default Screen diff --git a/shared/settings/make-icons.page.native.tsx b/shared/settings/make-icons.page.native.tsx deleted file mode 100644 index e83337b261ff..000000000000 --- a/shared/settings/make-icons.page.native.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// Desktop-only page; not available on native -export default function MakeIconsPage() { - return null -} diff --git a/shared/settings/make-icons.page.tsx b/shared/settings/make-icons.page.tsx new file mode 100644 index 000000000000..4d1ef7d6be74 --- /dev/null +++ b/shared/settings/make-icons.page.tsx @@ -0,0 +1,86 @@ +import * as Kb from '@/common-adapters' + +type DOMElement = { + id: string + style: {backgroundColor: string} + cloneNode: (deep: boolean) => DOMElement + remove: () => void +} +type DOMDoc = { + body: { + style: {backgroundColor: string} + appendChild: (el: DOMElement) => void + } + getElementById: (id: string) => DOMElement | null +} +const doc = (globalThis as {document?: DOMDoc}).document + +const Icon = (_p: {badge: number}) => null +const DEVwriteMenuIcons = (() => {}) as undefined | (() => void) + +const Screen = __DEV__ + ? () => { + if (isMobile) return null + + const onSave = () => { + if (!doc) return + const oldbg = doc.body.style.backgroundColor + doc.body.style.backgroundColor = 'transparent' + + const dte = doc.getElementById('divToExport') + if (!dte) return + const copy = dte.cloneNode(true) + copy.id = 'iconCopy' + doc.body.appendChild(copy) + + const root = doc.getElementById('root') + if (!root) return + root.remove() + setTimeout(() => { + DEVwriteMenuIcons?.() + setTimeout(() => { + doc.body.appendChild(root) + doc.body.style.backgroundColor = oldbg + copy.remove() + }, 500) + }, 100) + } + + const icons = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(badge => ) + + return ( + + +
+ {icons} +
+ + {icons} + +
+ ) + } + : () => null + +export default Screen diff --git a/shared/stores/fs-platform.native.tsx b/shared/stores/fs-platform.native.tsx index ef6bc4bd235c..a84513e1502f 100644 --- a/shared/stores/fs-platform.native.tsx +++ b/shared/stores/fs-platform.native.tsx @@ -1,6 +1,6 @@ import * as Styles from '@/styles' import * as T from '@/constants/types' -import {launchImageLibraryAsync} from '@/util/expo-image-picker.native' +import {launchImageLibraryAsync} from '@/util/expo-image-picker' import {pickDocumentsAsync} from '@/util/expo-document-picker.native' import {androidAddCompleteDownload, fsCacheDir, fsDownloadDir} from 'react-native-kb' import logger from '@/logger' diff --git a/shared/teams/add-members-wizard/add-contacts.desktop.tsx b/shared/teams/add-members-wizard/add-contacts.desktop.tsx deleted file mode 100644 index 32f52ba3075f..000000000000 --- a/shared/teams/add-members-wizard/add-contacts.desktop.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import type {AddMembersWizard} from './state' - -const AddContacts = (_: {wizard: AddMembersWizard}) => null -export default AddContacts diff --git a/shared/teams/add-members-wizard/add-contacts.native.tsx b/shared/teams/add-members-wizard/add-contacts.tsx similarity index 95% rename from shared/teams/add-members-wizard/add-contacts.native.tsx rename to shared/teams/add-members-wizard/add-contacts.tsx index 097cc6b7807b..2a27d20532c1 100644 --- a/shared/teams/add-members-wizard/add-contacts.native.tsx +++ b/shared/teams/add-members-wizard/add-contacts.tsx @@ -8,7 +8,7 @@ import {useModalHeaderState} from '@/stores/modal-header' import type {Contact} from '../common/contacts-list.native' import {addMembersToWizard, type AddMembersWizard} from './state' -const AddContacts = ({wizard}: {wizard: AddMembersWizard}) => { +const AddContactsMobile = ({wizard}: {wizard: AddMembersWizard}) => { const navigateUp = C.Router2.navigateUp const navUpToScreen = C.Router2.navUpToScreen const onBack = () => navigateUp() @@ -119,4 +119,9 @@ const AddContacts = ({wizard}: {wizard: AddMembersWizard}) => { ) } +const AddContacts = (props: {wizard: AddMembersWizard}) => { + if (!isMobile) return null + return +} + export default AddContacts diff --git a/shared/teams/invite-by-contact/team-invite-by-contacts.desktop.tsx b/shared/teams/invite-by-contact/team-invite-by-contacts.desktop.tsx deleted file mode 100644 index 0edab6a4786e..000000000000 --- a/shared/teams/invite-by-contact/team-invite-by-contacts.desktop.tsx +++ /dev/null @@ -1,2 +0,0 @@ -const TeamInviteByContact = (_p: {teamID: string}) => null -export default TeamInviteByContact diff --git a/shared/teams/invite-by-contact/team-invite-by-contacts.native.tsx b/shared/teams/invite-by-contact/team-invite-by-contacts.tsx similarity index 98% rename from shared/teams/invite-by-contact/team-invite-by-contacts.native.tsx rename to shared/teams/invite-by-contact/team-invite-by-contacts.tsx index 38e2f2dda8b0..5207f9aa1a6c 100644 --- a/shared/teams/invite-by-contact/team-invite-by-contacts.native.tsx +++ b/shared/teams/invite-by-contact/team-invite-by-contacts.tsx @@ -60,7 +60,7 @@ const generateSMSBody = (teamname: string, seitan: string): string => { return `Join the ${team} on Keybase. Copy this message into the "Teams" tab.\n\ntoken: ${seitan.toLowerCase()}\n\ninstall: keybase.io/_/go` } -const TeamInviteByContact = (props: Props) => { +const TeamInviteByContactMobile = (props: Props) => { const {teamID} = props const {contacts, region, errorMessage} = useContacts() const { @@ -214,4 +214,9 @@ const TeamInviteByContact = (props: Props) => { ) } +const TeamInviteByContact = (props: Props) => { + if (!isMobile) return null + return +} + export default TeamInviteByContact diff --git a/shared/util/expo-image-picker.desktop.tsx b/shared/util/expo-image-picker.desktop.tsx deleted file mode 100644 index 26dec0a5033c..000000000000 --- a/shared/util/expo-image-picker.desktop.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export type ImagePickerResult = {assets: null; canceled: true} -export type ImageInfo = {uri: string; width: number; height: number; type?: 'image' | 'video'} - -// eslint-disable-next-line @typescript-eslint/require-await -export const launchCameraAsync = async (): Promise => ({assets: null, canceled: true}) -// eslint-disable-next-line @typescript-eslint/require-await -export const launchImageLibraryAsync = async (): Promise => ({assets: null, canceled: true}) diff --git a/shared/util/expo-image-picker.native.tsx b/shared/util/expo-image-picker.tsx similarity index 97% rename from shared/util/expo-image-picker.native.tsx rename to shared/util/expo-image-picker.tsx index 8351e0bbe140..1c0805c5c01c 100644 --- a/shared/util/expo-image-picker.native.tsx +++ b/shared/util/expo-image-picker.tsx @@ -27,6 +27,7 @@ export const launchCameraAsync = async ( mediaType: 'photo' | 'video' | 'mixed', askPermAndRetry: boolean = true ): Promise => { + if (!isMobile) return canceled let res: ImagePicker.ImagePickerResult | undefined try { res = await ImagePicker.launchCameraAsync({ @@ -53,6 +54,7 @@ export const launchImageLibraryAsync = async ( askPermAndRetry: boolean = true, allowsMultipleSelection: boolean = false ): Promise => { + if (!isMobile) return canceled let res: ImagePicker.ImagePickerResult | undefined try { res = await ImagePicker.launchImageLibraryAsync({ diff --git a/shared/util/misc.native.tsx b/shared/util/misc.native.tsx index e16040691f23..e924c66933bb 100644 --- a/shared/util/misc.native.tsx +++ b/shared/util/misc.native.tsx @@ -1,6 +1,6 @@ import {navigateAppend} from '@/constants/router' import {pickDocumentsAsync} from './expo-document-picker.native' -import {launchImageLibraryAsync, type ImageInfo} from './expo-image-picker.native' +import {launchImageLibraryAsync, type ImageInfo} from './expo-image-picker' import type {OpenDialogOptions, SaveDialogOptions} from './electron.desktop' import * as SMS from 'expo-sms' import {Linking} from 'react-native' From b03f052d3c8e99117899e1cf08e16c0a9fa74fac Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Sun, 17 May 2026 18:32:10 -0400 Subject: [PATCH 02/28] Fix desktop build: move native-only imports inside mobile code path contacts-list.native and use-contacts.native import expo-contacts and expo-localization. Now that add-contacts.tsx and team-invite-by-contacts.tsx are plain .tsx files, those top-level imports would be bundled by desktop webpack. Move them into require() calls inside the mobile component bodies. --- shared/teams/add-members-wizard/add-contacts.tsx | 2 +- shared/teams/invite-by-contact/team-invite-by-contacts.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/shared/teams/add-members-wizard/add-contacts.tsx b/shared/teams/add-members-wizard/add-contacts.tsx index 2a27d20532c1..2715310f9cc3 100644 --- a/shared/teams/add-members-wizard/add-contacts.tsx +++ b/shared/teams/add-members-wizard/add-contacts.tsx @@ -3,12 +3,12 @@ import * as React from 'react' import * as Kb from '@/common-adapters' import * as T from '@/constants/types' import {pluralize} from '@/util/string' -import ContactsList, {useContacts, EnableContactsPopup} from '../common/contacts-list.native' import {useModalHeaderState} from '@/stores/modal-header' import type {Contact} from '../common/contacts-list.native' import {addMembersToWizard, type AddMembersWizard} from './state' const AddContactsMobile = ({wizard}: {wizard: AddMembersWizard}) => { + const {default: ContactsList, useContacts, EnableContactsPopup} = require('../common/contacts-list.native') as typeof import('../common/contacts-list.native') const navigateUp = C.Router2.navigateUp const navUpToScreen = C.Router2.navUpToScreen const onBack = () => navigateUp() diff --git a/shared/teams/invite-by-contact/team-invite-by-contacts.tsx b/shared/teams/invite-by-contact/team-invite-by-contacts.tsx index 5207f9aa1a6c..c2853aaa4a5b 100644 --- a/shared/teams/invite-by-contact/team-invite-by-contacts.tsx +++ b/shared/teams/invite-by-contact/team-invite-by-contacts.tsx @@ -1,7 +1,7 @@ import * as C from '@/constants' import * as React from 'react' import * as T from '@/constants/types' -import useContacts, {type Contact} from '../common/use-contacts.native' +import type {Contact} from '../common/use-contacts.native' import {InviteByContact, type ContactRowProps} from './index.native' import {getE164} from '@/util/phone-numbers' import {openSMS} from '@/util/misc' @@ -61,6 +61,7 @@ const generateSMSBody = (teamname: string, seitan: string): string => { } const TeamInviteByContactMobile = (props: Props) => { + const {default: useContacts} = require('../common/use-contacts.native') as typeof import('../common/use-contacts.native') const {teamID} = props const {contacts, region, errorMessage} = useContacts() const { From a27b071ddc9549637f70ceb578a2adc8f921b202 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Sun, 17 May 2026 19:13:11 -0400 Subject: [PATCH 03/28] Merge observer hooks and simple util platform pairs - use-intersection-observer: desktop impl with isMobile stub guard - use-resize-observer: desktop impl with isMobile stub guard - common-adapters/text-url: unified useClickURL with isMobile branch - app/main: two named components, export isMobile ? NativeMain : DesktopMain - globals.native.d.ts: add stubs for browser observer/DOM types used in desktop branches - Update explicit .desktop/.native imports in index files and main2.desktop.tsx --- shared/app/index.native.tsx | 2 +- shared/app/main.desktop.tsx | 24 --------- shared/app/main.native.tsx | 28 ---------- shared/app/main.tsx | 43 +++++++++++++++ .../conversation/list-area/index.desktop.tsx | 2 +- shared/common-adapters/index.desktop.tsx | 2 +- shared/common-adapters/index.native.tsx | 2 +- shared/common-adapters/text-url.desktop.tsx | 17 ------ shared/common-adapters/text-url.native.tsx | 19 ------- shared/common-adapters/text-url.tsx | 33 ++++++++++++ shared/desktop/renderer/main2.desktop.tsx | 8 +-- shared/globals.native.d.ts | 53 +++++++++++++++++++ .../util/use-intersection-observer.native.tsx | 14 ----- ...ktop.tsx => use-intersection-observer.tsx} | 32 +++++++++-- shared/util/use-resize-observer.native.tsx | 15 ------ ...er.desktop.tsx => use-resize-observer.tsx} | 36 +++++++++++-- 16 files changed, 198 insertions(+), 132 deletions(-) delete mode 100644 shared/app/main.desktop.tsx delete mode 100644 shared/app/main.native.tsx create mode 100644 shared/app/main.tsx delete mode 100644 shared/common-adapters/text-url.desktop.tsx delete mode 100644 shared/common-adapters/text-url.native.tsx create mode 100644 shared/common-adapters/text-url.tsx delete mode 100644 shared/util/use-intersection-observer.native.tsx rename shared/util/{use-intersection-observer.desktop.tsx => use-intersection-observer.tsx} (81%) delete mode 100644 shared/util/use-resize-observer.native.tsx rename shared/util/{use-resize-observer.desktop.tsx => use-resize-observer.tsx} (74%) diff --git a/shared/app/index.native.tsx b/shared/app/index.native.tsx index 7858e811c81a..433cf77a7e2e 100644 --- a/shared/app/index.native.tsx +++ b/shared/app/index.native.tsx @@ -4,7 +4,7 @@ import {useConfigState} from '@/stores/config' import {useShellState} from '@/stores/shell' import * as Kb from '@/common-adapters' import * as React from 'react' -import Main from './main.native' +import Main from './main' import {KeyboardProvider} from 'react-native-keyboard-controller' import Animated, {ReducedMotionConfig, ReduceMotion} from 'react-native-reanimated' import {AppRegistry, AppState, Appearance, Keyboard} from 'react-native' diff --git a/shared/app/main.desktop.tsx b/shared/app/main.desktop.tsx deleted file mode 100644 index 64e506f443fa..000000000000 --- a/shared/app/main.desktop.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import Router from '@/router-v2/router' -import ResetModal from '../login/reset/modal' -import GlobalError from './global-errors' -import OutOfDate from './out-of-date' -import RemoteProxies from '../desktop/remote/proxies.desktop' -import {FsStatusProvider} from '@/fs/common/status' -import {SystemFileManagerIntegrationProvider} from '@/fs/common/sfmi' - -const Main = function Main() { - return ( - - - - - - - - - - ) -} -// get focus so react doesn't hold onto old divs - -export default Main diff --git a/shared/app/main.native.tsx b/shared/app/main.native.tsx deleted file mode 100644 index c5f107199f63..000000000000 --- a/shared/app/main.native.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import Router from '@/router-v2/router' -import {PortalHost} from '@/common-adapters/portal.native' -import ResetModal from '../login/reset/modal' -import GlobalError from './global-errors' -import OutOfDate from './out-of-date' -import RuntimeStats from './runtime-stats' -import {BottomSheetModalProvider} from '@gorhom/bottom-sheet' -import {FsStatusProvider} from '@/fs/common/status' -import {SystemFileManagerIntegrationProvider} from '@/fs/common/sfmi' - -const Main = () => { - return ( - - - - - - - - - - - - - ) -} - -export default Main diff --git a/shared/app/main.tsx b/shared/app/main.tsx new file mode 100644 index 000000000000..4c54a604d546 --- /dev/null +++ b/shared/app/main.tsx @@ -0,0 +1,43 @@ +import Router from '@/router-v2/router' +import ResetModal from '../login/reset/modal' +import GlobalError from './global-errors' +import OutOfDate from './out-of-date' +import {FsStatusProvider} from '@/fs/common/status' +import {SystemFileManagerIntegrationProvider} from '@/fs/common/sfmi' + +const DesktopMain = function DesktopMain() { + const RemoteProxies = (require('../desktop/remote/proxies.desktop') as typeof import('../desktop/remote/proxies.desktop')).default + return ( + + + + + + + + + + ) +} + +const NativeMain = () => { + const {PortalHost} = require('@/common-adapters/portal.native') as typeof import('@/common-adapters/portal.native') + const RuntimeStats = (require('./runtime-stats') as typeof import('./runtime-stats')).default + const {BottomSheetModalProvider} = require('@gorhom/bottom-sheet') as typeof import('@gorhom/bottom-sheet') + return ( + + + + + + + + + + + + + ) +} + +export default isMobile ? NativeMain : DesktopMain diff --git a/shared/chat/conversation/list-area/index.desktop.tsx b/shared/chat/conversation/list-area/index.desktop.tsx index fb19b9b51a33..7fba01b8f5fa 100644 --- a/shared/chat/conversation/list-area/index.desktop.tsx +++ b/shared/chat/conversation/list-area/index.desktop.tsx @@ -13,7 +13,7 @@ import {findLast} from '@/util/arrays' import {MessageRow} from '../messages/wrapper' import {globalMargins} from '@/styles/shared' import {FocusContext, ScrollContext} from '../normal/context' -import useResizeObserver from '@/util/use-resize-observer.desktop' +import useResizeObserver from '@/util/use-resize-observer' import useIntersectionObserver from '@/util/use-intersection-observer' import {copyToClipboard} from '@/util/storeless-actions' import {useConversationCenter} from '../center-context' diff --git a/shared/common-adapters/index.desktop.tsx b/shared/common-adapters/index.desktop.tsx index 837dfe644f6b..fef64d91e17f 100644 --- a/shared/common-adapters/index.desktop.tsx +++ b/shared/common-adapters/index.desktop.tsx @@ -84,7 +84,7 @@ export {default as Switch} from './switch' export {default as Tabs} from './tabs' export {default as TeamWithPopup} from './team-with-popup' export {default as Text} from './text' -export {useClickURL} from './text-url.desktop' +export {useClickURL} from './text-url' export {default as Toast} from './toast' export {default as SimpleToast} from './simple-toast' export {default as TimelineMarker} from './timeline-marker' diff --git a/shared/common-adapters/index.native.tsx b/shared/common-adapters/index.native.tsx index f4696b81e416..1bcd5124fdf6 100644 --- a/shared/common-adapters/index.native.tsx +++ b/shared/common-adapters/index.native.tsx @@ -84,7 +84,7 @@ export {default as Switch} from './switch' export {default as Tabs} from './tabs' export {default as TeamWithPopup} from './team-with-popup' export {default as Text} from './text' -export {useClickURL} from './text-url.native' +export {useClickURL} from './text-url' export {default as Toast} from './toast' export {default as SimpleToast} from './simple-toast' export {default as TimelineMarker} from './timeline-marker' diff --git a/shared/common-adapters/text-url.desktop.tsx b/shared/common-adapters/text-url.desktop.tsx deleted file mode 100644 index c48a64905b2d..000000000000 --- a/shared/common-adapters/text-url.desktop.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import {openURL} from '@/util/misc' -import KB2 from '@/util/electron.desktop' -const {showContextMenu} = KB2.functions - -export function useClickURL(url: string | undefined) { - if (!url) return {} as const - return { - onClick: (e: React.BaseSyntheticEvent) => { - e.stopPropagation() - void openURL(url) - }, - onContextMenu: (e: React.BaseSyntheticEvent) => { - e.stopPropagation() - showContextMenu?.(url) - }, - } as const -} diff --git a/shared/common-adapters/text-url.native.tsx b/shared/common-adapters/text-url.native.tsx deleted file mode 100644 index a453b5cc46ca..000000000000 --- a/shared/common-adapters/text-url.native.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import {openURL} from '@/util/misc' -import * as Clipboard from 'expo-clipboard' -import {Alert} from 'react-native' - -export function useClickURL(url: string | undefined) { - if (!url) return {} as const - return { - onClick: () => { - openURL(url) - }, - onLongPress: () => { - Alert.alert('', url, [ - {onPress: () => openURL(url), text: 'Open'}, - {onPress: () => { void Clipboard.setStringAsync(url) }, text: 'Copy'}, - {text: 'Cancel'}, - ]) - }, - } as const -} diff --git a/shared/common-adapters/text-url.tsx b/shared/common-adapters/text-url.tsx new file mode 100644 index 000000000000..1764cbb76367 --- /dev/null +++ b/shared/common-adapters/text-url.tsx @@ -0,0 +1,33 @@ +import {openURL} from '@/util/misc' + +export function useClickURL(url: string | undefined) { + if (!url) return {} as const + if (isMobile) { + const {Alert} = require('react-native') as typeof import('react-native') + const Clipboard = require('expo-clipboard') as typeof import('expo-clipboard') + return { + onClick: () => { + openURL(url) + }, + onLongPress: () => { + Alert.alert('', url, [ + {onPress: () => openURL(url), text: 'Open'}, + {onPress: () => { void Clipboard.setStringAsync(url) }, text: 'Copy'}, + {text: 'Cancel'}, + ]) + }, + } as const + } + const KB2 = require('@/util/electron.desktop') as typeof import('@/util/electron.desktop') + const {showContextMenu} = KB2.default.functions + return { + onClick: (e: React.BaseSyntheticEvent) => { + e.stopPropagation() + void openURL(url) + }, + onContextMenu: (e: React.BaseSyntheticEvent) => { + e.stopPropagation() + showContextMenu?.(url) + }, + } as const +} diff --git a/shared/desktop/renderer/main2.desktop.tsx b/shared/desktop/renderer/main2.desktop.tsx index c32fab788d33..79b1cd80cb3f 100644 --- a/shared/desktop/renderer/main2.desktop.tsx +++ b/shared/desktop/renderer/main2.desktop.tsx @@ -1,6 +1,6 @@ /// // Entry point to the chrome part of the app -import Main from '@/app/main.desktop' +import Main from '@/app/main' // order of the above must NOT change. needed for patching / hot loading to be correct import * as C from '@/constants' import * as React from 'react' @@ -19,7 +19,7 @@ import ServiceDecoration from '@/common-adapters/markdown/service-decoration' import {useDarkModeState} from '@/stores/darkmode' import {initPlatformListener, onEngineIncoming} from '@/constants/init/index.desktop' import {eventFromRemoteWindows} from './remote-event-handler.desktop' -import type {default as NewMainType} from '../../app/main.desktop' +import type {default as NewMainType} from '../../app/main' import {dumpLogs} from '@/util/storeless-actions' setServiceDecoration(ServiceDecoration) @@ -167,12 +167,12 @@ const setupHMR = () => { const refreshMain = () => { try { - const {default: NewMain} = require('../../app/main.desktop') as {default: typeof NewMainType} + const {default: NewMain} = require('../../app/main') as {default: typeof NewMainType} render(NewMain) } catch {} } - module.hot.accept(['../../app/main.desktop'], refreshMain) + module.hot.accept(['../../app/main'], refreshMain) module.hot.accept(['../../common-adapters/index'], () => {}) } diff --git a/shared/globals.native.d.ts b/shared/globals.native.d.ts index 21ee84887305..606e034cc040 100644 --- a/shared/globals.native.d.ts +++ b/shared/globals.native.d.ts @@ -22,3 +22,56 @@ interface DataTransfer { readonly files: ReadonlyArray readonly types: ReadonlyArray } + +declare function requestAnimationFrame(callback: () => void): number + +// Minimal DOM element stubs for desktop-only branches of merged files +interface Element { + tagName?: string +} +interface HTMLElement extends Element {} + +// Minimal stubs for browser observer APIs used in desktop-only branches of merged files +interface IntersectionObserverEntry { + readonly boundingClientRect: DOMRectReadOnly | null + readonly intersectionRatio: number + readonly intersectionRect: DOMRectReadOnly | null + readonly isIntersecting: boolean + readonly rootBounds: DOMRectReadOnly | null + readonly target: Element + readonly time: number +} +type IntersectionObserverCallback = (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => void +interface IntersectionObserverInit { + root?: Element | null + rootMargin?: string + threshold?: number | number[] +} +declare class IntersectionObserver { + constructor(callback: IntersectionObserverCallback, options?: IntersectionObserverInit) + observe(target: Element): void + unobserve(target: Element): void + disconnect(): void + POLL_INTERVAL?: number | null + USE_MUTATION_OBSERVER?: boolean +} +interface ResizeObserverEntry { + readonly contentRect: DOMRectReadOnly + readonly target: Element +} +declare class ResizeObserver { + constructor(callback: (entries: ResizeObserverEntry[], observer: ResizeObserver) => void) + observe(target: Element): void + unobserve(target: Element): void + disconnect(): void +} +interface DOMRectReadOnly { + readonly x: number + readonly y: number + readonly width: number + readonly height: number + readonly top: number + readonly right: number + readonly bottom: number + readonly left: number +} diff --git a/shared/util/use-intersection-observer.native.tsx b/shared/util/use-intersection-observer.native.tsx deleted file mode 100644 index 4a19cda4a1ee..000000000000 --- a/shared/util/use-intersection-observer.native.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import type {MockIntersectionObserverEntry} from './use-intersection-observer.shared' -function useIntersectionObserver(): MockIntersectionObserverEntry { - return { - boundingClientRect: null, - intersectionRatio: null, - intersectionRect: null, - isIntersecting: false, - rootBounds: null, - target: null, - time: null, - } -} - -export default useIntersectionObserver diff --git a/shared/util/use-intersection-observer.desktop.tsx b/shared/util/use-intersection-observer.tsx similarity index 81% rename from shared/util/use-intersection-observer.desktop.tsx rename to shared/util/use-intersection-observer.tsx index 7ccc705ebcaf..7455b63f8174 100644 --- a/shared/util/use-intersection-observer.desktop.tsx +++ b/shared/util/use-intersection-observer.tsx @@ -2,9 +2,19 @@ import * as React from 'react' import type {IntersectionObserverOptions, MockIntersectionObserverEntry} from './use-intersection-observer.shared' -function useIntersectionObserver( - target: React.RefObject | T | null, - options: IntersectionObserverOptions = {} +const mobileStub: MockIntersectionObserverEntry = { + boundingClientRect: null, + intersectionRatio: null, + intersectionRect: null, + isIntersecting: false, + rootBounds: null, + target: null, + time: null, +} + +function useIntersectionObserverDesktop( + target: React.RefObject | T | null | undefined, + options: IntersectionObserverOptions ): MockIntersectionObserverEntry | IntersectionObserverEntry { const { root = null, @@ -68,6 +78,22 @@ function useIntersectionObserver( return entry } +function useIntersectionObserverMobile(): MockIntersectionObserverEntry { + return mobileStub +} + +function useIntersectionObserver( + target?: React.RefObject | T | null, + options: IntersectionObserverOptions = {} +): MockIntersectionObserverEntry | IntersectionObserverEntry { + if (isMobile) { + // eslint-disable-next-line react-hooks/rules-of-hooks + return useIntersectionObserverMobile() + } + // eslint-disable-next-line react-hooks/rules-of-hooks + return useIntersectionObserverDesktop(target, options) +} + function createIntersectionObserver({ root = null, pollInterval = null, diff --git a/shared/util/use-resize-observer.native.tsx b/shared/util/use-resize-observer.native.tsx deleted file mode 100644 index 7a9b0dbafe39..000000000000 --- a/shared/util/use-resize-observer.native.tsx +++ /dev/null @@ -1,15 +0,0 @@ -type FakeResizeObserver = { - disconnect: () => void - observe: () => void - unobserve: () => void -} - -function useResizeObserver(_target?: unknown, _callback?: unknown): FakeResizeObserver { - return { - disconnect: () => {}, - observe: () => {}, - unobserve: () => {}, - } -} - -export default useResizeObserver diff --git a/shared/util/use-resize-observer.desktop.tsx b/shared/util/use-resize-observer.tsx similarity index 74% rename from shared/util/use-resize-observer.desktop.tsx rename to shared/util/use-resize-observer.tsx index 0a87789e50b4..9e0685a0807a 100644 --- a/shared/util/use-resize-observer.desktop.tsx +++ b/shared/util/use-resize-observer.tsx @@ -3,7 +3,13 @@ import * as React from 'react' type InternalCallback = (entry: ResizeObserverEntry, observer: ResizeObserver) => unknown -function useResizeObserver( +type FakeResizeObserver = { + disconnect: () => void + observe: () => void + unobserve: () => void +} + +function useResizeObserverDesktop( target: React.RefObject | React.ForwardedRef | T | null, callback: InternalCallback ): ResizeObserver { @@ -33,16 +39,38 @@ function useResizeObserver( return resizeObserver.observer } +const fakeResizeObserver: FakeResizeObserver = { + disconnect: () => {}, + observe: () => {}, + unobserve: () => {}, +} + +function useResizeObserverMobile(_target?: unknown, _callback?: unknown): FakeResizeObserver { + return fakeResizeObserver +} + +function useResizeObserver( + target: React.RefObject | React.ForwardedRef | T | null, + callback: InternalCallback +): ResizeObserver | FakeResizeObserver { + if (isMobile) { + // eslint-disable-next-line react-hooks/rules-of-hooks + return useResizeObserverMobile(target, callback) + } + // eslint-disable-next-line react-hooks/rules-of-hooks + return useResizeObserverDesktop(target, callback) +} + function createResizeObserver() { let ticking = false let allEntries: ResizeObserverEntry[] = [] const callbacks: Map> = new Map() - const observer = new window.ResizeObserver((entries: ResizeObserverEntry[], obs: ResizeObserver) => { + const observer = new ResizeObserver((entries: ResizeObserverEntry[], obs: ResizeObserver) => { allEntries = allEntries.concat(entries) if (!ticking) { - window.requestAnimationFrame(() => { + requestAnimationFrame(() => { const triggered = new Set() // eslint-disable-next-line for (let i = 0; i < allEntries.length; i++) { @@ -53,7 +81,7 @@ function createResizeObserver() { if (triggered.has(entry.target)) continue triggered.add(entry.target) const cbs = callbacks.get(entry.target) - + cbs?.forEach(cb => cb(entry, obs)) } allEntries = [] From 2a46a04b42f89e0c871c3208a3e678445b66c9ad Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Sun, 17 May 2026 21:04:22 -0400 Subject: [PATCH 04/28] Fix Task 2 type errors: DOM stubs in globals.native.d.ts, avoid typeof import for desktop files --- shared/app/main.tsx | 3 +- shared/common-adapters/text-url.tsx | 6 ++- shared/globals.native.d.ts | 78 +++++++++++++++-------------- 3 files changed, 47 insertions(+), 40 deletions(-) diff --git a/shared/app/main.tsx b/shared/app/main.tsx index 4c54a604d546..205727a7d6e6 100644 --- a/shared/app/main.tsx +++ b/shared/app/main.tsx @@ -6,7 +6,8 @@ import {FsStatusProvider} from '@/fs/common/status' import {SystemFileManagerIntegrationProvider} from '@/fs/common/sfmi' const DesktopMain = function DesktopMain() { - const RemoteProxies = (require('../desktop/remote/proxies.desktop') as typeof import('../desktop/remote/proxies.desktop')).default + // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-explicit-any + const RemoteProxies = (require('../desktop/remote/proxies.desktop') as any).default as React.ComponentType return ( diff --git a/shared/common-adapters/text-url.tsx b/shared/common-adapters/text-url.tsx index 1764cbb76367..0436f7d64826 100644 --- a/shared/common-adapters/text-url.tsx +++ b/shared/common-adapters/text-url.tsx @@ -18,8 +18,10 @@ export function useClickURL(url: string | undefined) { }, } as const } - const KB2 = require('@/util/electron.desktop') as typeof import('@/util/electron.desktop') - const {showContextMenu} = KB2.default.functions + // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-explicit-any + const {showContextMenu} = (require('@/util/electron.desktop') as any).default.functions as { + showContextMenu?: (url: string) => void + } return { onClick: (e: React.BaseSyntheticEvent) => { e.stopPropagation() diff --git a/shared/globals.native.d.ts b/shared/globals.native.d.ts index 606e034cc040..4fc8f19a2118 100644 --- a/shared/globals.native.d.ts +++ b/shared/globals.native.d.ts @@ -23,55 +23,59 @@ interface DataTransfer { readonly types: ReadonlyArray } -declare function requestAnimationFrame(callback: () => void): number +// Stubs for DOM observer/element types used in merged platform files. +// Desktop build excludes this file so declare class is safe (no conflict with lib.dom). +interface Element {} +interface HTMLElement extends Element {} -// Minimal DOM element stubs for desktop-only branches of merged files -interface Element { - tagName?: string +interface DOMRectReadOnly { + readonly width: number + readonly height: number + readonly top: number + readonly left: number + readonly bottom: number + readonly right: number + readonly x: number + readonly y: number } -interface HTMLElement extends Element {} -// Minimal stubs for browser observer APIs used in desktop-only branches of merged files -interface IntersectionObserverEntry { - readonly boundingClientRect: DOMRectReadOnly | null - readonly intersectionRatio: number - readonly intersectionRect: DOMRectReadOnly | null - readonly isIntersecting: boolean - readonly rootBounds: DOMRectReadOnly | null +interface ResizeObserverEntry { readonly target: Element - readonly time: number + readonly contentRect: DOMRectReadOnly } -type IntersectionObserverCallback = (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => void +declare class ResizeObserver { + constructor(callback: (entries: ResizeObserverEntry[], observer: ResizeObserver) => void) + disconnect(): void + observe(target: Element): void + unobserve(target: Element): void +} + +type IntersectionObserverCallback = ( + entries: IntersectionObserverEntry[], + observer: IntersectionObserver +) => void interface IntersectionObserverInit { - root?: Element | null + root?: Element | Document | null rootMargin?: string threshold?: number | number[] } -declare class IntersectionObserver { - constructor(callback: IntersectionObserverCallback, options?: IntersectionObserverInit) - observe(target: Element): void - unobserve(target: Element): void - disconnect(): void - POLL_INTERVAL?: number | null - USE_MUTATION_OBSERVER?: boolean -} -interface ResizeObserverEntry { - readonly contentRect: DOMRectReadOnly +interface IntersectionObserverEntry { + readonly isIntersecting: boolean readonly target: Element + readonly time: number + readonly intersectionRatio: number + readonly rootBounds: DOMRectReadOnly | null + readonly boundingClientRect: DOMRectReadOnly + readonly intersectionRect: DOMRectReadOnly } -declare class ResizeObserver { - constructor(callback: (entries: ResizeObserverEntry[], observer: ResizeObserver) => void) +declare class IntersectionObserver { + readonly POLL_INTERVAL: number | null | undefined + readonly USE_MUTATION_OBSERVER: boolean | undefined + constructor(callback: IntersectionObserverCallback, options?: IntersectionObserverInit) observe(target: Element): void unobserve(target: Element): void disconnect(): void } -interface DOMRectReadOnly { - readonly x: number - readonly y: number - readonly width: number - readonly height: number - readonly top: number - readonly right: number - readonly bottom: number - readonly left: number -} + +declare function requestAnimationFrame(callback: FrameRequestCallback): number +type FrameRequestCallback = (time: number) => void From 86464452003cf18dfc2dff95ad245047aa2b355f Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Sun, 17 May 2026 21:46:45 -0400 Subject: [PATCH 05/28] merge platform files: image, list, bottom-sheet, floating-box, text-url, app/main, observers --- shared/app/main.tsx | 14 +-- shared/common-adapters/image.desktop.tsx | 39 -------- shared/common-adapters/image.native.tsx | 44 --------- shared/common-adapters/image.tsx | 90 +++++++++++++++++++ shared/common-adapters/index.desktop.tsx | 6 +- shared/common-adapters/index.native.tsx | 7 +- shared/common-adapters/list.desktop.tsx | 31 ------- shared/common-adapters/list.native.tsx | 37 -------- shared/common-adapters/list.tsx | 65 ++++++++++++++ .../popup/bottom-sheet.desktop.tsx | 15 ---- .../popup/bottom-sheet.native.tsx | 8 -- shared/common-adapters/popup/bottom-sheet.tsx | 67 ++++++++++++++ .../popup/floating-box/index.desktop.tsx | 27 ------ .../popup/floating-box/index.native.tsx | 36 -------- .../popup/floating-box/index.tsx | 73 +++++++++++++++ shared/common-adapters/text-url.tsx | 16 ++-- shared/router-v2/common.tsx | 2 +- .../teams/add-members-wizard/add-contacts.tsx | 25 +++++- .../team-invite-by-contacts.tsx | 37 +++++++- 19 files changed, 377 insertions(+), 262 deletions(-) delete mode 100644 shared/common-adapters/image.desktop.tsx delete mode 100644 shared/common-adapters/image.native.tsx create mode 100644 shared/common-adapters/image.tsx delete mode 100644 shared/common-adapters/list.desktop.tsx delete mode 100644 shared/common-adapters/list.native.tsx create mode 100644 shared/common-adapters/list.tsx delete mode 100644 shared/common-adapters/popup/bottom-sheet.desktop.tsx delete mode 100644 shared/common-adapters/popup/bottom-sheet.native.tsx create mode 100644 shared/common-adapters/popup/bottom-sheet.tsx delete mode 100644 shared/common-adapters/popup/floating-box/index.desktop.tsx delete mode 100644 shared/common-adapters/popup/floating-box/index.native.tsx create mode 100644 shared/common-adapters/popup/floating-box/index.tsx diff --git a/shared/app/main.tsx b/shared/app/main.tsx index 205727a7d6e6..80465a9e517f 100644 --- a/shared/app/main.tsx +++ b/shared/app/main.tsx @@ -1,3 +1,4 @@ +import type * as React from 'react' import Router from '@/router-v2/router' import ResetModal from '../login/reset/modal' import GlobalError from './global-errors' @@ -6,8 +7,7 @@ import {FsStatusProvider} from '@/fs/common/status' import {SystemFileManagerIntegrationProvider} from '@/fs/common/sfmi' const DesktopMain = function DesktopMain() { - // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-explicit-any - const RemoteProxies = (require('../desktop/remote/proxies.desktop') as any).default as React.ComponentType + const {default: RemoteProxies} = require('../desktop/remote/proxies.desktop') as {default: React.ComponentType} return ( @@ -22,9 +22,13 @@ const DesktopMain = function DesktopMain() { } const NativeMain = () => { - const {PortalHost} = require('@/common-adapters/portal.native') as typeof import('@/common-adapters/portal.native') - const RuntimeStats = (require('./runtime-stats') as typeof import('./runtime-stats')).default - const {BottomSheetModalProvider} = require('@gorhom/bottom-sheet') as typeof import('@gorhom/bottom-sheet') + const {PortalHost} = require('@/common-adapters/portal.native') as { + PortalHost: React.ComponentType<{name?: string; children?: React.ReactNode}> + } + const RuntimeStats = (require('./runtime-stats') as {default: React.ComponentType}).default + const {BottomSheetModalProvider} = require('@gorhom/bottom-sheet') as { + BottomSheetModalProvider: React.ComponentType<{children: React.ReactNode}> + } return ( diff --git a/shared/common-adapters/image.desktop.tsx b/shared/common-adapters/image.desktop.tsx deleted file mode 100644 index 4af2eebb26f1..000000000000 --- a/shared/common-adapters/image.desktop.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import * as React from 'react' -import * as Styles from '@/styles' -import type {Props} from './image.shared' -import LoadingStateView from './loading-state-view' - -const onDragStart = (e: React.BaseSyntheticEvent) => e.preventDefault() -const Image = (p: Props) => { - const {showLoadingStateUntilLoaded, src, onLoad, onError} = p - const [loading, setLoading] = React.useState(true) - const _onLoad = (e: React.BaseSyntheticEvent) => { - setLoading(false) - onLoad?.(e) - } - const style = { - ...p.style, - ...(showLoadingStateUntilLoaded && loading ? styles.absolute : {}), - opacity: showLoadingStateUntilLoaded && loading ? 0 : 1, - } as const - - return ( - <> - - {showLoadingStateUntilLoaded ? : null} - - ) -} - -const styles = Styles.styleSheetCreate(() => ({ - absolute: {position: 'absolute'}, -})) - -export default Image diff --git a/shared/common-adapters/image.native.tsx b/shared/common-adapters/image.native.tsx deleted file mode 100644 index fc0ec35223f4..000000000000 --- a/shared/common-adapters/image.native.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import * as React from 'react' -import LoadingStateView from './loading-state-view' -import type {Props} from './image.shared' -import {Image as ExpoImage, type ImageLoadEventData, type ImageErrorEventData} from 'expo-image' - -const Image = (p: Props) => { - const {showLoadingStateUntilLoaded, src, onLoad, onError, style, contentFit = 'contain', allowDownscaling} = p - const [loading, setLoading] = React.useState(!showLoadingStateUntilLoaded) - const [lastSrc, setLastSrc] = React.useState(src) - const _onLoad = (e?: ImageLoadEventData) => { - setLoading(false) - onLoad?.(e ?? ({} as any)) - } - - if (lastSrc !== src) { - setLastSrc(src) - setLoading(true) - } - - const _onError = (e?: ImageErrorEventData) => { - setLoading(false) - console.log('Image load error', e?.error) - onError?.() - } - - const recyclingKey = typeof src === 'string' ? src : Array.isArray(src) ? src[0]?.uri : String(src) - - return ( - <> - - {showLoadingStateUntilLoaded && loading ? : null} - - ) -} - -export default Image diff --git a/shared/common-adapters/image.tsx b/shared/common-adapters/image.tsx new file mode 100644 index 000000000000..45b2a07d4154 --- /dev/null +++ b/shared/common-adapters/image.tsx @@ -0,0 +1,90 @@ +import * as React from 'react' +import * as Styles from '@/styles' +import type {ImageLoadEventData, ImageErrorEventData} from 'expo-image' +import type {Props} from './image.shared' +import LoadingStateView from './loading-state-view' + +const onDragStart = (e: React.BaseSyntheticEvent) => e.preventDefault() + +const DesktopImage = (p: Props) => { + const {showLoadingStateUntilLoaded, src, onLoad, onError} = p + const [loading, setLoading] = React.useState(true) + const _onLoad = (e: React.BaseSyntheticEvent) => { + setLoading(false) + onLoad?.(e) + } + const style = { + ...p.style, + ...(showLoadingStateUntilLoaded && loading ? styles.absolute : {}), + opacity: showLoadingStateUntilLoaded && loading ? 0 : 1, + } as const + + return ( + <> + + {showLoadingStateUntilLoaded ? : null} + + ) +} + +type ExpoImageProps = { + source: Props['src'] + style?: Props['style'] + onLoad?: (e?: ImageLoadEventData) => void + contentFit?: Props['contentFit'] + onError?: (e?: ImageErrorEventData) => void + allowDownscaling?: boolean + recyclingKey?: string +} + +const NativeImage = (p: Props) => { + const {Image: ExpoImage} = require('expo-image') as {Image: React.ComponentType} + const {showLoadingStateUntilLoaded, src, onLoad, onError, style, contentFit = 'contain', allowDownscaling} = p + const [loading, setLoading] = React.useState(!showLoadingStateUntilLoaded) + const [lastSrc, setLastSrc] = React.useState(src) + const _onLoad = (e?: ImageLoadEventData) => { + setLoading(false) + onLoad?.(e ?? {}) + } + + if (lastSrc !== src) { + setLastSrc(src) + setLoading(true) + } + + const _onError = (e?: ImageErrorEventData) => { + setLoading(false) + console.log('Image load error', e?.error) + onError?.() + } + + const recyclingKey = typeof src === 'string' ? src : Array.isArray(src) ? src[0]?.uri : String(src) + + return ( + <> + + {showLoadingStateUntilLoaded && loading ? : null} + + ) +} + +const styles = Styles.styleSheetCreate(() => ({ + absolute: {position: 'absolute'}, +})) + +export default isMobile ? NativeImage : DesktopImage diff --git a/shared/common-adapters/index.desktop.tsx b/shared/common-adapters/index.desktop.tsx index fef64d91e17f..506f4c21aff2 100644 --- a/shared/common-adapters/index.desktop.tsx +++ b/shared/common-adapters/index.desktop.tsx @@ -13,7 +13,7 @@ export { BottomSheetBackdrop, BottomSheetScrollView, type BottomSheetBackdropProps, -} from './popup/bottom-sheet.desktop' +} from './popup/bottom-sheet' export {default as Animation} from './animation' export {default as Avatar} from './avatar/index' export {default as AvatarLine} from './avatar/avatar-line' @@ -46,12 +46,12 @@ export {default as FloatingPicker} from './floating-picker' export {usePopup2, type Popup2Parms} from './popup/use-popup' export {HeaderLeftButton} from './header-buttons' export {useHotKey} from './hot-key' -export {default as Image} from './image.desktop' +export {default as Image} from './image' export {default as ImageIcon} from './image-icon' export {default as InfoNote} from './info-note' export {default as Input3, type Input3Ref, type Input3Props} from './input3' export {KeyboardAvoidingView2} from './keyboard-avoiding-view' -export {default as List, type LegendListRef} from './list.desktop' +export {default as List, type LegendListRef} from './list' export { default as ListItem, largeHeight as largeListItemHeight, diff --git a/shared/common-adapters/index.native.tsx b/shared/common-adapters/index.native.tsx index 1bcd5124fdf6..e558d80f2d17 100644 --- a/shared/common-adapters/index.native.tsx +++ b/shared/common-adapters/index.native.tsx @@ -10,7 +10,7 @@ export { BottomSheetBackdrop, BottomSheetScrollView, type BottomSheetBackdropProps, -} from './popup/bottom-sheet.native' +} from './popup/bottom-sheet' export {default as Animation} from './animation' export {default as Avatar} from './avatar/index' export {default as AvatarLine} from './avatar/avatar-line' @@ -44,14 +44,13 @@ export {default as FloatingPicker} from './floating-picker' export {usePopup2, type Popup2Parms} from './popup/use-popup' export {HeaderLeftButton} from './header-buttons' export {useHotKey} from './hot-key' -export {default as Image} from './image.native' +export {default as Image} from './image' export {default as ImageIcon} from './image-icon' export {default as InfoNote} from './info-note' export {default as Input3} from './input3' export type {Input3Ref, Input3Props} from './input3' export {KeyboardAvoidingView2} from './keyboard-avoiding-view' -export {default as List} from './list.native' -export type {LegendListRef} from './list.shared' +export {default as List, type LegendListRef} from './list' export { default as ListItem, largeHeight as largeListItemHeight, diff --git a/shared/common-adapters/list.desktop.tsx b/shared/common-adapters/list.desktop.tsx deleted file mode 100644 index af47429f1559..000000000000 --- a/shared/common-adapters/list.desktop.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type {CSSProperties} from 'react' -import * as Styles from '@/styles' -import {LegendList} from '@legendapp/list/react' -import type {Props} from './list.shared' -export type {LegendListRef, Props} from './list.shared' -import {useListProps} from './list-common' - -function List({ref, ...p}: Props) { - const {empty, ...listProps} = useListProps(p as Props) - const {style} = p - if (empty) return null - - return ( - - ) -} - -export default List diff --git a/shared/common-adapters/list.native.tsx b/shared/common-adapters/list.native.tsx deleted file mode 100644 index 766b97e6af0f..000000000000 --- a/shared/common-adapters/list.native.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import {View} from 'react-native' -import * as Styles from '@/styles' -import {LegendList} from '@legendapp/list/react-native' -import type {Props} from './list.shared' -import {useListProps} from './list-common' - -function List({ref, ...p}: Props) { - const {empty, ...listProps} = useListProps(p as Props) - if (empty) return null - - return ( - - - - ) -} - -const styles = Styles.styleSheetCreate( - () => - ({ - outerView: { - flexGrow: 1, - position: 'relative', - }, - }) as const -) - -export default List diff --git a/shared/common-adapters/list.tsx b/shared/common-adapters/list.tsx new file mode 100644 index 000000000000..61fdac9174c0 --- /dev/null +++ b/shared/common-adapters/list.tsx @@ -0,0 +1,65 @@ +import type {CSSProperties} from 'react' +import {View} from 'react-native' +import * as Styles from '@/styles' +import type {Props} from './list.shared' +import type {LegendListComponent as LegendListWebType} from '@legendapp/list/react' +import type {LegendListComponent as LegendListNativeType} from '@legendapp/list/react-native' +import {useListProps} from './list-common' +export type {LegendListRef, Props} from './list.shared' + +const DesktopList = function List({ref, ...p}: Props) { + const {LegendList} = require('@legendapp/list/react') as {LegendList: LegendListWebType} + const {empty, ...listProps} = useListProps(p as Props) + const {style} = p + if (empty) return null + + return ( + + ) +} + +const NativeList = function List({ref, ...p}: Props) { + const {LegendList} = require('@legendapp/list/react-native') as {LegendList: LegendListNativeType} + const {empty, ...listProps} = useListProps(p as Props) + if (empty) return null + + return ( + + + + ) +} + +const styles = Styles.styleSheetCreate( + () => + ({ + outerView: { + flexGrow: 1, + position: 'relative', + }, + }) as const +) + +export default isMobile ? NativeList : DesktopList diff --git a/shared/common-adapters/popup/bottom-sheet.desktop.tsx b/shared/common-adapters/popup/bottom-sheet.desktop.tsx deleted file mode 100644 index ef1c3621d662..000000000000 --- a/shared/common-adapters/popup/bottom-sheet.desktop.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import * as React from 'react' - -// Desktop stubs for mobile-only bottom sheet components -export const BottomSheetView = (_p: {children?: React.ReactNode}) => null -export class BottomSheetModal extends React.Component> { - present() {} - forceClose() {} - override render() { - return null - } -} -export const BottomSheetBackdrop = (_p: Record) => null -export const BottomSheetScrollView = (_p: {style?: object; children?: React.ReactNode}) => null -export const BottomSheetHandle = () => null -export type BottomSheetBackdropProps = Record diff --git a/shared/common-adapters/popup/bottom-sheet.native.tsx b/shared/common-adapters/popup/bottom-sheet.native.tsx deleted file mode 100644 index e1fe35f06ee6..000000000000 --- a/shared/common-adapters/popup/bottom-sheet.native.tsx +++ /dev/null @@ -1,8 +0,0 @@ -export { - BottomSheetView, - BottomSheetModal, - BottomSheetBackdrop, - BottomSheetScrollView, - BottomSheetHandle, - type BottomSheetBackdropProps, -} from '@gorhom/bottom-sheet' diff --git a/shared/common-adapters/popup/bottom-sheet.tsx b/shared/common-adapters/popup/bottom-sheet.tsx new file mode 100644 index 000000000000..85ea0a055f3e --- /dev/null +++ b/shared/common-adapters/popup/bottom-sheet.tsx @@ -0,0 +1,67 @@ +import * as React from 'react' +import type {BottomSheetModalProps, BottomSheetBackdropProps, BottomSheetHandleProps} from '@gorhom/bottom-sheet' + +type NativeMethods = {present: () => void; forceClose: () => void} +type BackdropProps = BottomSheetBackdropProps & {disappearsOnIndex?: number; appearsOnIndex?: number; opacity?: number} +type HandleProps = BottomSheetHandleProps & {style?: object; indicatorStyle?: object; children?: React.ReactNode} +type ScrollViewProps = {style?: object; children?: React.ReactNode} +type GorhomModule = { + BottomSheetModal: React.ForwardRefExoticComponent> + BottomSheetView: React.ComponentType<{children?: React.ReactNode}> + BottomSheetBackdrop: React.ComponentType + BottomSheetScrollView: React.ComponentType + BottomSheetHandle: React.ComponentType +} + +const _gorhom: GorhomModule | null = isMobile ? (require('@gorhom/bottom-sheet') as GorhomModule) : null + +export const BottomSheetView = (_p: {children?: React.ReactNode}) => { + if (!isMobile) return null + const {BottomSheetView: NativeView} = _gorhom! + return {_p.children} +} + +export class BottomSheetModal extends React.Component { + private _native: NativeMethods | null = null + + present() { + this._native?.present() + } + + forceClose() { + this._native?.forceClose() + } + + override render() { + if (!isMobile) return null + const {BottomSheetModal: NativeModal} = _gorhom! + return ( + { + this._native = r + }} + /> + ) + } +} + +export const BottomSheetBackdrop = (_p: BackdropProps) => { + if (!isMobile) return null + const {BottomSheetBackdrop: NativeBackdrop} = _gorhom! + return +} + +export const BottomSheetScrollView = (_p: ScrollViewProps) => { + if (!isMobile) return null + const {BottomSheetScrollView: NativeScrollView} = _gorhom! + return +} + +export const BottomSheetHandle = (_p: HandleProps) => { + if (!isMobile) return null + const {BottomSheetHandle: NativeHandle} = _gorhom! + return +} + +export type {BottomSheetBackdropProps} from '@gorhom/bottom-sheet' diff --git a/shared/common-adapters/popup/floating-box/index.desktop.tsx b/shared/common-adapters/popup/floating-box/index.desktop.tsx deleted file mode 100644 index 1a1192c59570..000000000000 --- a/shared/common-adapters/popup/floating-box/index.desktop.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import type {Props} from './index.shared' -import {RelativeFloatingBox} from './relative-floating-box.desktop' -import noop from 'lodash/noop' - -const FloatingBox = (props: Props) => { - const {attachTo, disableEscapeKey, position, positionFallbacks, children, offset} = props - const {onHidden, remeasureHint, propagateOutsideClicks, containerStyle, matchDimension} = props - - return ( - - {children} - - ) -} - -export default FloatingBox diff --git a/shared/common-adapters/popup/floating-box/index.native.tsx b/shared/common-adapters/popup/floating-box/index.native.tsx deleted file mode 100644 index 61154266a2c1..000000000000 --- a/shared/common-adapters/popup/floating-box/index.native.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import * as React from 'react' -import {Box2} from '@/common-adapters/box' -import * as Styles from '@/styles' -import {Keyboard} from 'react-native' -import {Portal} from '../../portal.native' -import type {Props} from './index.shared' - -const Kb = { - Box2, - Portal, -} - -const FloatingBox = (p: Props) => { - const {hideKeyboard, children, containerStyle} = p - const [lastHK, setLastHK] = React.useState(hideKeyboard) - if (lastHK !== hideKeyboard) { - setLastHK(hideKeyboard) - if (hideKeyboard) { - Keyboard.dismiss() - } - } - - return ( - - - {children} - - - ) -} - -export default FloatingBox diff --git a/shared/common-adapters/popup/floating-box/index.tsx b/shared/common-adapters/popup/floating-box/index.tsx new file mode 100644 index 000000000000..72138d9f2b8f --- /dev/null +++ b/shared/common-adapters/popup/floating-box/index.tsx @@ -0,0 +1,73 @@ +import * as React from 'react' +import * as Styles from '@/styles' +import {Box2} from '@/common-adapters/box' +import {Keyboard} from 'react-native' +import noop from 'lodash/noop' +import type {Props} from './index.shared' + +type RFBProps = { + attachTo: Props['attachTo'] + disableEscapeKey: Props['disableEscapeKey'] + position: string + positionFallbacks: Props['positionFallbacks'] + matchDimension: boolean + onClosePopup: () => void + remeasureHint: Props['remeasureHint'] + propagateOutsideClicks: Props['propagateOutsideClicks'] + style: Props['containerStyle'] + offset: Props['offset'] + children?: React.ReactNode +} + +const DesktopFloatingBox = (props: Props) => { + const {RelativeFloatingBox} = require('./relative-floating-box.desktop') as { + RelativeFloatingBox: React.ComponentType + } + const {attachTo, disableEscapeKey, position, positionFallbacks, children, offset} = props + const {onHidden, remeasureHint, propagateOutsideClicks, containerStyle, matchDimension} = props + + return ( + + {children} + + ) +} + +type PortalProps = {hostName?: string; children?: React.ReactNode} + +const NativeFloatingBox = (p: Props) => { + const {Portal} = require('../../portal.native') as {Portal: React.ComponentType} + const {hideKeyboard, children, containerStyle} = p + const [lastHK, setLastHK] = React.useState(hideKeyboard) + if (lastHK !== hideKeyboard) { + setLastHK(hideKeyboard) + if (hideKeyboard) { + Keyboard.dismiss() + } + } + + return ( + + + {children} + + + ) +} + +export default isMobile ? NativeFloatingBox : DesktopFloatingBox diff --git a/shared/common-adapters/text-url.tsx b/shared/common-adapters/text-url.tsx index 0436f7d64826..00fc3338822e 100644 --- a/shared/common-adapters/text-url.tsx +++ b/shared/common-adapters/text-url.tsx @@ -1,10 +1,13 @@ import {openURL} from '@/util/misc' +import type {AlertStatic} from 'react-native' export function useClickURL(url: string | undefined) { if (!url) return {} as const if (isMobile) { - const {Alert} = require('react-native') as typeof import('react-native') - const Clipboard = require('expo-clipboard') as typeof import('expo-clipboard') + const {Alert} = require('react-native') as {Alert: AlertStatic} + const {setStringAsync} = require('expo-clipboard') as { + setStringAsync: (text: string) => Promise + } return { onClick: () => { openURL(url) @@ -12,20 +15,19 @@ export function useClickURL(url: string | undefined) { onLongPress: () => { Alert.alert('', url, [ {onPress: () => openURL(url), text: 'Open'}, - {onPress: () => { void Clipboard.setStringAsync(url) }, text: 'Copy'}, + {onPress: () => { void setStringAsync(url) }, text: 'Copy'}, {text: 'Cancel'}, ]) }, } as const } - // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-explicit-any - const {showContextMenu} = (require('@/util/electron.desktop') as any).default.functions as { - showContextMenu?: (url: string) => void + const {default: {functions: {showContextMenu}}} = require('@/util/electron.desktop') as { + default: {functions: {showContextMenu?: (url: string) => void}} } return { onClick: (e: React.BaseSyntheticEvent) => { e.stopPropagation() - void openURL(url) + void Promise.resolve(openURL(url)) }, onContextMenu: (e: React.BaseSyntheticEvent) => { e.stopPropagation() diff --git a/shared/router-v2/common.tsx b/shared/router-v2/common.tsx index 59153e191d31..6b331bdc0f00 100644 --- a/shared/router-v2/common.tsx +++ b/shared/router-v2/common.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import type * as React from 'react' import * as Kb from '@/common-adapters' import {TabActions, type NavigationContainerRef} from '@react-navigation/core' import type {ParamListBase} from '@react-navigation/native' diff --git a/shared/teams/add-members-wizard/add-contacts.tsx b/shared/teams/add-members-wizard/add-contacts.tsx index 2715310f9cc3..4d630f1c85e0 100644 --- a/shared/teams/add-members-wizard/add-contacts.tsx +++ b/shared/teams/add-members-wizard/add-contacts.tsx @@ -4,11 +4,32 @@ import * as Kb from '@/common-adapters' import * as T from '@/constants/types' import {pluralize} from '@/util/string' import {useModalHeaderState} from '@/stores/modal-header' -import type {Contact} from '../common/contacts-list.native' import {addMembersToWizard, type AddMembersWizard} from './state' +type Contact = { + id: string + name: string + pictureUri?: string + type: 'phone' | 'email' + value: string + valueFormatted?: string +} + +type ContactsListProps = { + onSelect: (contact: Contact, checked: boolean) => void + search: string + selectedEmails: Set + selectedPhones: Set +} + +type ContactsModule = { + default: React.ComponentType + useContacts: () => {contacts: Array; loading: boolean; noAccessPermanent: boolean} + EnableContactsPopup: React.ComponentType<{noAccess: boolean; onClose: () => void}> +} + const AddContactsMobile = ({wizard}: {wizard: AddMembersWizard}) => { - const {default: ContactsList, useContacts, EnableContactsPopup} = require('../common/contacts-list.native') as typeof import('../common/contacts-list.native') + const {default: ContactsList, useContacts, EnableContactsPopup} = require('../common/contacts-list.native') as ContactsModule const navigateUp = C.Router2.navigateUp const navUpToScreen = C.Router2.navUpToScreen const onBack = () => navigateUp() diff --git a/shared/teams/invite-by-contact/team-invite-by-contacts.tsx b/shared/teams/invite-by-contact/team-invite-by-contacts.tsx index c2853aaa4a5b..10923f32436a 100644 --- a/shared/teams/invite-by-contact/team-invite-by-contacts.tsx +++ b/shared/teams/invite-by-contact/team-invite-by-contacts.tsx @@ -1,13 +1,43 @@ import * as C from '@/constants' import * as React from 'react' import * as T from '@/constants/types' -import type {Contact} from '../common/use-contacts.native' -import {InviteByContact, type ContactRowProps} from './index.native' import {getE164} from '@/util/phone-numbers' import {openSMS} from '@/util/misc' import logger from '@/logger' import {useLoadedTeam} from '../team/use-loaded-team' +type Contact = { + id: string + name: string + pictureUri?: string + type: 'phone' | 'email' + value: string + valueFormatted?: string +} + +type ContactRowProps = Contact & { + id: string + alreadyInvited: boolean + loading: boolean + onClick: () => void +} + +type InviteByContactProps = { + selectedRole: T.Teams.TeamRoleType + onRoleChange: (newRole: T.Teams.TeamRoleType) => void + teamName: string + listItems: Array + errorMessage?: string +} + +type UseContactsResult = { + contacts: Array + errorMessage?: string + loading: boolean + noAccessPermanent: boolean + region: string +} + // Seitan invite names (labels) look like this: "[name] ([phone number])". Try // to derive E164 phone number based on seitan invite name and user's region. const extractPhoneNumber = (name: string, region: string): string => { @@ -61,7 +91,8 @@ const generateSMSBody = (teamname: string, seitan: string): string => { } const TeamInviteByContactMobile = (props: Props) => { - const {default: useContacts} = require('../common/use-contacts.native') as typeof import('../common/use-contacts.native') + const {InviteByContact} = require('./index.native') as {InviteByContact: React.ComponentType} + const {default: useContacts} = require('../common/use-contacts.native') as {default: () => UseContactsResult} const {teamID} = props const {contacts, region, errorMessage} = useContacts() const { From afa616f8c98c08df3df1794700da5865d7f483dc Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Sun, 17 May 2026 21:59:04 -0400 Subject: [PATCH 06/28] merge navigation: screen-layout, crypto sub-nav, relogin --- shared/crypto/sub-nav/index.native.tsx | 36 --- .../sub-nav/{index.desktop.tsx => index.tsx} | 66 +++- shared/globals.native.d.ts | 1 + shared/login/relogin/index.desktop.tsx | 154 ---------- shared/login/relogin/index.native.tsx | 135 --------- shared/login/relogin/index.tsx | 286 ++++++++++++++++++ shared/router-v2/router.desktop.tsx | 2 +- shared/router-v2/router.native.tsx | 2 +- .../router-v2/screen-layout-modal.desktop.tsx | 248 +++++++++++++++ shared/router-v2/screen-layout.tsx | 156 ++++++++++ 10 files changed, 745 insertions(+), 341 deletions(-) delete mode 100644 shared/crypto/sub-nav/index.native.tsx rename shared/crypto/sub-nav/{index.desktop.tsx => index.tsx} (66%) delete mode 100644 shared/login/relogin/index.desktop.tsx delete mode 100644 shared/login/relogin/index.native.tsx create mode 100644 shared/login/relogin/index.tsx create mode 100644 shared/router-v2/screen-layout-modal.desktop.tsx create mode 100644 shared/router-v2/screen-layout.tsx diff --git a/shared/crypto/sub-nav/index.native.tsx b/shared/crypto/sub-nav/index.native.tsx deleted file mode 100644 index c41261148096..000000000000 --- a/shared/crypto/sub-nav/index.native.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import * as C from '@/constants' -import * as Crypto from '@/constants/crypto' -import * as Kb from '@/common-adapters' -import NavRow from './nav-row' - -const CryptoSubNav = () => { - const {navigate} = C.useNav() - return ( - - {Crypto.Tabs.map(t => ( - navigate(t.tab)} - /> - ))} - - ) -} - -const styles = Kb.Styles.styleSheetCreate( - () => - ({ - container: { - backgroundColor: Kb.Styles.globalColors.blueGrey, - paddingLeft: Kb.Styles.globalMargins.small, - paddingRight: Kb.Styles.globalMargins.small, - paddingTop: Kb.Styles.globalMargins.xsmall, - }, - }) as const -) - -export default CryptoSubNav diff --git a/shared/crypto/sub-nav/index.desktop.tsx b/shared/crypto/sub-nav/index.tsx similarity index 66% rename from shared/crypto/sub-nav/index.desktop.tsx rename to shared/crypto/sub-nav/index.tsx index 7ac33e56c347..92f3ee1cff4a 100644 --- a/shared/crypto/sub-nav/index.desktop.tsx +++ b/shared/crypto/sub-nav/index.tsx @@ -1,8 +1,9 @@ import * as React from 'react' -import * as Kb from '@/common-adapters' +import * as C from '@/constants' import * as Crypto from '@/constants/crypto' +import * as Kb from '@/common-adapters' import * as Common from '@/router-v2/common' -import LeftNav from './left-nav.desktop' +import NavRow from './nav-row' import { useNavigationBuilder, TabRouter, @@ -10,11 +11,10 @@ import { } from '@react-navigation/core' import type {TypedNavigator, NavigatorTypeBagBase} from '@react-navigation/native' import {routeMapToScreenElements} from '@/router-v2/routes' -import {makeLayout} from '@/router-v2/screen-layout.desktop' +import {makeLayout} from '@/router-v2/screen-layout' import type {RouteDef, GetOptionsParams} from '@/constants/types/router' import {defineRouteMap} from '@/constants/types/router' -/* Desktop SubNav */ const cryptoSubRoutes = defineRouteMap({ [Crypto.decryptTab]: { screen: React.lazy(async () => { @@ -34,7 +34,6 @@ const cryptoSubRoutes = defineRouteMap({ return {default: SignIO} }), }, - [Crypto.verifyTab]: { screen: React.lazy(async () => { const {VerifyIO} = await import('../verify') @@ -42,6 +41,9 @@ const cryptoSubRoutes = defineRouteMap({ }), }, }) + +type LeftNavProps = {onClick: (tab: string) => void; selected: string} + function LeftTabNavigator({ initialRouteName, children, @@ -50,6 +52,7 @@ function LeftTabNavigator({ }: Parameters[1] & { backBehavior: 'initialRoute' | 'firstRoute' | 'history' | 'order' | 'none' }) { + const {default: LeftNav} = require('./left-nav.desktop') as {default: React.ComponentType} const {state, navigation, descriptors, NavigationContent} = useNavigationBuilder(TabRouter, { backBehavior, children, @@ -84,18 +87,13 @@ function LeftTabNavigator({ ) } -const styles = Kb.Styles.styleSheetCreate(() => ({ - box: {backgroundColor: Kb.Styles.globalColors.white}, - nav: {width: 180}, -})) - type NavType = NavigatorTypeBagBase & { ParamList: { [key in keyof typeof cryptoSubRoutes]: {} } } -export const createLeftTabNavigator = createNavigatorFactory(LeftTabNavigator) as unknown as () => TypedNavigator +const createLeftTabNavigator = createNavigatorFactory(LeftTabNavigator) as unknown as () => TypedNavigator const TabNavigator = createLeftTabNavigator() const makeOptions = (rd: RouteDef) => { return ({route, navigation}: GetOptionsParams) => { @@ -104,11 +102,51 @@ const makeOptions = (rd: RouteDef) => { return {...opt} } } -const cryptoScreens = routeMapToScreenElements(cryptoSubRoutes, TabNavigator.Screen, makeLayout, makeOptions, false, false, false) -const CryptoSubNavigator = () => ( +const cryptoScreens = routeMapToScreenElements( + cryptoSubRoutes, + TabNavigator.Screen, + makeLayout, + makeOptions, + false, + false, + false +) +const DesktopCryptoSubNavigator = () => ( {cryptoScreens} ) -export default CryptoSubNavigator +const NativeCryptoSubNav = () => { + const {navigate} = C.useNav() + return ( + + {Crypto.Tabs.map(t => ( + navigate(t.tab)} + /> + ))} + + ) +} + +const styles = Kb.Styles.styleSheetCreate( + () => + ({ + box: {backgroundColor: Kb.Styles.globalColors.white}, + container: { + backgroundColor: Kb.Styles.globalColors.blueGrey, + paddingLeft: Kb.Styles.globalMargins.small, + paddingRight: Kb.Styles.globalMargins.small, + paddingTop: Kb.Styles.globalMargins.xsmall, + }, + nav: {width: 180}, + }) as const +) + +export default isMobile ? NativeCryptoSubNav : DesktopCryptoSubNavigator diff --git a/shared/globals.native.d.ts b/shared/globals.native.d.ts index 4fc8f19a2118..41fceac320b8 100644 --- a/shared/globals.native.d.ts +++ b/shared/globals.native.d.ts @@ -27,6 +27,7 @@ interface DataTransfer { // Desktop build excludes this file so declare class is safe (no conflict with lib.dom). interface Element {} interface HTMLElement extends Element {} +interface HTMLDivElement extends HTMLElement {} interface DOMRectReadOnly { readonly width: number diff --git a/shared/login/relogin/index.desktop.tsx b/shared/login/relogin/index.desktop.tsx deleted file mode 100644 index c9c30bf84373..000000000000 --- a/shared/login/relogin/index.desktop.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import * as C from '@/constants' -import * as React from 'react' -import * as Kb from '@/common-adapters' -import UserCard from '../user-card' -import {errorBanner, SignupScreen} from '@/signup/common' -import type {Props} from './index.shared' - -const other = 'Someone else...' - -const UserRow = ({user, hasStoredSecret}: {user: string; hasStoredSecret: boolean}) => ( - - - {user} - - {hasStoredSecret && • Signed in} - -) - -const Login = (props: Props) => { - const _inputRef = React.useRef(null) - - const _onClickUserIdx = (selected: number) => { - const user = props.users.at(selected) - if (!user) { - props.onSomeoneElse() - } else { - props.selectedUserChange(user.username) - if (_inputRef.current) { - _inputRef.current.focus() - } - } - } - - const userRows = props.users - .concat({hasStoredSecret: false, uid: '', username: other}) - .map(u => ) - - const selectedIdx = props.users.findIndex(u => u.username === props.selectedUser) - return ( - - - - - {props.needPassword && ( - - - - )} - - - Forgot password? - - - - - - - - - ) -} - -const styles = Kb.Styles.styleSheetCreate( - () => - ({ - container: { - ...Kb.Styles.globalStyles.flexBoxColumn, - alignItems: 'center', - flex: 1, - justifyContent: 'center', - }, - contentBox: { - alignSelf: 'center', - maxWidth: 460, - padding: Kb.Styles.globalMargins.small, - }, - forgotPassword: { - marginTop: Kb.Styles.globalMargins.tiny, - }, - header: { - borderBottomWidth: 0, - }, - inputRow: { - marginBottom: 0, - marginTop: Kb.Styles.globalMargins.tiny, - width: '100%', - }, - loginSubmitButton: { - marginTop: 0, - maxHeight: 32, - width: '100%', - }, - other: {color: Kb.Styles.globalColors.black}, - provisioned: {color: Kb.Styles.globalColors.orange}, - userContainer: { - backgroundColor: Kb.Styles.globalColors.transparent, - flex: 1, - }, - userDropdown: { - backgroundColor: Kb.Styles.globalColors.white, - width: '100%', - }, - userOverlayStyle: { - backgroundColor: Kb.Styles.globalColors.white, - width: 348, - }, - userRow: { - alignItems: 'center', - marginLeft: Kb.Styles.globalMargins.xsmall, - minHeight: 40, - width: '100%', - }, - }) as const -) - -export default Login diff --git a/shared/login/relogin/index.native.tsx b/shared/login/relogin/index.native.tsx deleted file mode 100644 index 44cc9ffd1878..000000000000 --- a/shared/login/relogin/index.native.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import * as C from '@/constants' -import * as Kb from '@/common-adapters' -import {isAndroidNewerThanM} from '@/constants/platform' -import * as React from 'react' -import Dropdown from './dropdown.native' -import UserCard from '../user-card' -import type {Input3Props} from '@/common-adapters/input3.shared' -import type {Props} from './index.shared' - -const LoginRender = (props: Props) => { - const [scrollViewHeight, setScrollViewHeight] = React.useState(undefined) - const inputProps: Input3Props = { - autoFocus: true, - error: !!props.error, - keyboardType: props.showTyping && isAndroid ? 'visible-password' : 'default', - onChangeText: password => props.passwordChange(password), - onEnterKeyDown: () => props.onSubmit(), - placeholder: 'Password', - secureTextEntry: !props.showTyping, - } - - return ( - setScrollViewHeight(evt.nativeEvent.layout.height)} - style={Kb.Styles.globalStyles.flexOne} - > - - - {isAndroid && !C.isDeviceSecureAndroid && !isAndroidNewerThanM && ( - - - {"Since you don't have a lock screen, you'll have to type your password everytime."} - - - )} - {!!props.error && {props.error}} - - - {props.needPassword && ( - - - props.showTypingChange(check)} - style={styles.formElements} - /> - - )} - - - Forgot password? - - - Problems logging in? - - - - - - - - - - ) -} - -const styles = Kb.Styles.styleSheetCreate( - () => - ({ - card: { - marginTop: Kb.Styles.globalMargins.medium, - width: '100%', - }, - cardInner: Kb.Styles.platformStyles({ - isTablet: {paddingBottom: 0}, - }), - container: { - backgroundColor: Kb.Styles.globalColors.blueGrey, - }, - createAccountContainer: Kb.Styles.platformStyles({ - common: {padding: Kb.Styles.globalMargins.medium}, - isTablet: {maxWidth: 410, padding: Kb.Styles.globalMargins.small}, - }), - deviceNotSecureContainer: { - alignSelf: 'stretch', - backgroundColor: Kb.Styles.globalColors.yellow, - paddingBottom: Kb.Styles.globalMargins.tiny, - paddingTop: Kb.Styles.globalMargins.tiny, - }, - deviceNotSecureText: { - color: Kb.Styles.globalColors.brown_75, - }, - formElements: { - marginBottom: Kb.Styles.globalMargins.tiny, - }, - scrollView: { - backgroundColor: Kb.Styles.globalColors.blueGrey, - }, - }) as const -) - -export default LoginRender diff --git a/shared/login/relogin/index.tsx b/shared/login/relogin/index.tsx new file mode 100644 index 000000000000..e027817fde2c --- /dev/null +++ b/shared/login/relogin/index.tsx @@ -0,0 +1,286 @@ +import * as C from '@/constants' +import * as React from 'react' +import * as Kb from '@/common-adapters' +import UserCard from '../user-card' +import {errorBanner, SignupScreen} from '@/signup/common' +import {isAndroidNewerThanM} from '@/constants/platform' +import type {Props} from './index.shared' + +// Desktop login + +const other = 'Someone else...' + +const UserRow = ({user, hasStoredSecret}: {user: string; hasStoredSecret: boolean}) => ( + + + {user} + + {hasStoredSecret && • Signed in} + +) + +const DesktopLogin = (props: Props) => { + const _inputRef = React.useRef(null) + + const _onClickUserIdx = (selected: number) => { + const user = props.users.at(selected) + if (!user) { + props.onSomeoneElse() + } else { + props.selectedUserChange(user.username) + if (_inputRef.current) { + _inputRef.current.focus() + } + } + } + + const userRows = props.users + .concat({hasStoredSecret: false, uid: '', username: other}) + .map(u => ) + + const selectedIdx = props.users.findIndex(u => u.username === props.selectedUser) + return ( + + + + + {props.needPassword && ( + + + + )} + + + Forgot password? + + + + + + + + + ) +} + +const desktopStyles = Kb.Styles.styleSheetCreate( + () => + ({ + container: { + ...Kb.Styles.globalStyles.flexBoxColumn, + alignItems: 'center', + flex: 1, + justifyContent: 'center', + }, + contentBox: { + alignSelf: 'center', + maxWidth: 460, + padding: Kb.Styles.globalMargins.small, + }, + forgotPassword: { + marginTop: Kb.Styles.globalMargins.tiny, + }, + header: { + borderBottomWidth: 0, + }, + inputRow: { + marginBottom: 0, + marginTop: Kb.Styles.globalMargins.tiny, + width: '100%', + }, + loginSubmitButton: { + marginTop: 0, + maxHeight: 32, + width: '100%', + }, + other: {color: Kb.Styles.globalColors.black}, + provisioned: {color: Kb.Styles.globalColors.orange}, + userContainer: { + backgroundColor: Kb.Styles.globalColors.transparent, + flex: 1, + }, + userDropdown: { + backgroundColor: Kb.Styles.globalColors.white, + width: '100%', + }, + userOverlayStyle: { + backgroundColor: Kb.Styles.globalColors.white, + width: 348, + }, + userRow: { + alignItems: 'center', + marginLeft: Kb.Styles.globalMargins.xsmall, + minHeight: 40, + width: '100%', + }, + }) as const +) + +// Native login + +const NativeLoginRender = (props: Props) => { + type DropdownProps = {type: string; value: string; onClick: (option: string) => void; onOther: () => void; options: Props['users']} + const {default: Dropdown} = require('./dropdown.native') as {default: React.ComponentType} + const [scrollViewHeight, setScrollViewHeight] = React.useState(undefined) + const inputProps = { + autoFocus: true, + error: !!props.error, + keyboardType: props.showTyping && isAndroid ? 'visible-password' : 'default', + onChangeText: (password: string) => props.passwordChange(password), + onEnterKeyDown: () => props.onSubmit(), + placeholder: 'Password', + secureTextEntry: !props.showTyping, + } as const + + return ( + setScrollViewHeight(evt.nativeEvent.layout.height)} + style={Kb.Styles.globalStyles.flexOne} + > + + + {isAndroid && !C.isDeviceSecureAndroid && !isAndroidNewerThanM && ( + + + {"Since you don't have a lock screen, you'll have to type your password everytime."} + + + )} + {!!props.error && {props.error}} + + + {props.needPassword && ( + + + props.showTypingChange(check)} + style={nativeStyles.formElements} + /> + + )} + + + Forgot password? + + + Problems logging in? + + + + + + + + + + ) +} + +const nativeStyles = Kb.Styles.styleSheetCreate( + () => + ({ + card: { + marginTop: Kb.Styles.globalMargins.medium, + width: '100%', + }, + cardInner: Kb.Styles.platformStyles({ + isTablet: {paddingBottom: 0}, + }), + container: { + backgroundColor: Kb.Styles.globalColors.blueGrey, + }, + createAccountContainer: Kb.Styles.platformStyles({ + common: {padding: Kb.Styles.globalMargins.medium}, + isTablet: {maxWidth: 410, padding: Kb.Styles.globalMargins.small}, + }), + deviceNotSecureContainer: { + alignSelf: 'stretch', + backgroundColor: Kb.Styles.globalColors.yellow, + paddingBottom: Kb.Styles.globalMargins.tiny, + paddingTop: Kb.Styles.globalMargins.tiny, + }, + deviceNotSecureText: { + color: Kb.Styles.globalColors.brown_75, + }, + formElements: { + marginBottom: Kb.Styles.globalMargins.tiny, + }, + scrollView: { + backgroundColor: Kb.Styles.globalColors.blueGrey, + }, + }) as const +) + +export default isMobile ? NativeLoginRender : DesktopLogin diff --git a/shared/router-v2/router.desktop.tsx b/shared/router-v2/router.desktop.tsx index 47a50b949fd7..aa2bb1064d2b 100644 --- a/shared/router-v2/router.desktop.tsx +++ b/shared/router-v2/router.desktop.tsx @@ -21,7 +21,7 @@ import {createNativeStackNavigator} from '@react-navigation/native-stack' import {LoadedTeamsListProvider} from '@/teams/use-teams-list' import type {NativeStackNavigationOptions} from '@react-navigation/native-stack' -import {makeLayout} from './screen-layout.desktop' +import {makeLayout} from './screen-layout' import './router.css' const Tab = createLeftTabNavigator() diff --git a/shared/router-v2/router.native.tsx b/shared/router-v2/router.native.tsx index 8013d427b742..e3beb15f87d2 100644 --- a/shared/router-v2/router.native.tsx +++ b/shared/router-v2/router.native.tsx @@ -18,7 +18,7 @@ import {createNativeStackNavigator} from '@react-navigation/native-stack' import {isLiquidGlassSupported as _isLiquidGlassSupported} from '@callstack/liquid-glass' import type {NativeStackNavigationOptions} from '@react-navigation/native-stack' import type {SFSymbol} from 'sf-symbols-typescript' -import {makeLayout} from './screen-layout.native' +import {makeLayout} from './screen-layout' import {useRootKey} from './hooks.native' import {createLinkingConfig} from './linking' import type {RootParamList} from './route-params' diff --git a/shared/router-v2/screen-layout-modal.desktop.tsx b/shared/router-v2/screen-layout-modal.desktop.tsx new file mode 100644 index 000000000000..44b11cfe5aa1 --- /dev/null +++ b/shared/router-v2/screen-layout-modal.desktop.tsx @@ -0,0 +1,248 @@ +import * as Kb from '@/common-adapters' +import * as React from 'react' +import * as C from '@/constants' +import type {GetOptionsRet} from '@/constants/types/router' +import type {ParamListBase} from '@react-navigation/native' +import type {NativeStackNavigationProp} from '@react-navigation/native-stack' + +type ModalHeaderProps = { + title?: React.ReactNode + leftButton?: React.ReactNode + rightButton?: React.ReactNode +} + +const ModalHeader = (props: ModalHeaderProps) => ( + + + + {!!props.leftButton && props.leftButton} + + + {typeof props.title === 'string' ? ( + + {props.title} + + ) : ( + props.title + )} + + + {!!props.rightButton && props.rightButton} + + + +) + +const mouseResetValue = -9999 +const mouseDistanceThreshold = 5 + +const useMouseClick = (navigation: NativeStackNavigationProp, noClose?: boolean) => { + const backgroundRef = React.useRef(null) + const [mouseDownX, setMouseDownX] = React.useState(mouseResetValue) + const [mouseDownY, setMouseDownY] = React.useState(mouseResetValue) + const onMouseDown = (e: React.MouseEvent) => { + const {screenX, screenY, target} = e.nativeEvent + if (target !== backgroundRef.current) { + return + } + setMouseDownX(screenX) + setMouseDownY(screenY) + } + const onMouseUp = (e: React.MouseEvent) => { + const {screenX, screenY, target} = e.nativeEvent + if (target !== backgroundRef.current) { + return + } + const xDist = Math.abs(screenX - mouseDownX) + const yDist = Math.abs(screenY - mouseDownY) + if (xDist < mouseDistanceThreshold && yDist < mouseDistanceThreshold) { + if (!noClose) { + navigation.pop() + } + } + setMouseDownX(mouseResetValue) + setMouseDownY(mouseResetValue) + } + return [backgroundRef, onMouseUp, onMouseDown] as const +} + +export type ModalWrapperProps = { + children: React.ReactNode + navigationOptions?: GetOptionsRet + navigation: NativeStackNavigationProp +} + +export const ModalWrapper = (p: ModalWrapperProps) => { + const {navigationOptions, navigation, children} = p + const {overlayStyle, overlayAvoidTabs, overlayTransparent, overlayNoClose, modalFooter, modalStyle} = + navigationOptions ?? {} + + const headerTitle = navigationOptions?.['headerTitle'] ?? navigationOptions?.['title'] + const headerLeft = navigationOptions?.['headerLeft'] + const headerRight = navigationOptions?.['headerRight'] + const headerShown = navigationOptions?.['headerShown'] !== false + const hasHeader = headerShown && !!(headerTitle || headerLeft || headerRight) + + const [backgroundRef, onMouseUp, onMouseDown] = useMouseClick(navigation, overlayNoClose) + + const [topMostModal, setTopMostModal] = React.useState(true) + + C.Router2.useSafeFocusEffect(() => { + setTopMostModal(true) + return () => { + setTopMostModal(false) + } + }) + + React.useEffect(() => { + if (!topMostModal || overlayNoClose) return + const handler = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.stopImmediatePropagation() + navigation.pop() + } + } + window.addEventListener('keydown', handler, true) + return () => window.removeEventListener('keydown', handler, true) + }, [topMostModal, overlayNoClose, navigation]) + + const titleNode = + typeof headerTitle === 'function' + ? headerTitle({ + children: + typeof navigationOptions?.['title'] === 'string' ? navigationOptions['title'] : '', + tintColor: '', + }) + : headerTitle + const leftNode = typeof headerLeft === 'function' ? headerLeft({canGoBack: true}) : undefined + const rightNode = typeof headerRight === 'function' ? headerRight({tintColor: ''}) : undefined + + return ( + + {overlayAvoidTabs && ( + + )} + + + {hasHeader ? : null} + {children} + {modalFooter ? ( + + {modalFooter.content} + + ) : null} + {!overlayTransparent && !overlayNoClose && ( + navigation.pop()} + color={Kb.Styles.globalColors.whiteOrWhite_75} + hoverColor={Kb.Styles.globalColors.white_40OrWhite_40} + style={styles.closeIcon} + /> + )} + + + + ) +} + +const styles = Kb.Styles.styleSheetCreate(() => ({ + closeIcon: Kb.Styles.platformStyles({ + isElectron: { + cursor: 'pointer', + padding: Kb.Styles.globalMargins.tiny, + position: 'absolute', + right: Kb.Styles.globalMargins.tiny * -4, + top: 0, + }, + }), + header: { + borderBottomColor: Kb.Styles.globalColors.black_10, + borderBottomWidth: 1, + borderStyle: 'solid' as const, + minHeight: 48, + }, + headerLeft: { + flex: 1, + justifyContent: 'flex-start', + paddingLeft: Kb.Styles.globalMargins.xsmall, + paddingRight: Kb.Styles.globalMargins.xsmall, + }, + headerRight: { + flex: 1, + justifyContent: 'flex-end', + paddingLeft: Kb.Styles.globalMargins.xsmall, + paddingRight: Kb.Styles.globalMargins.xsmall, + }, + hidden: {display: 'none'}, + modalBox: Kb.Styles.platformStyles({ + isElectron: { + ...Kb.Styles.desktopStyles.boxShadow, + backgroundColor: Kb.Styles.globalColors.white, + borderRadius: Kb.Styles.borderRadius, + maxHeight: 560, + pointerEvents: 'auto', + position: 'relative', + width: 400, + }, + }), + modalFooter: Kb.Styles.platformStyles({ + common: { + ...Kb.Styles.padding(Kb.Styles.globalMargins.xsmall, Kb.Styles.globalMargins.small), + borderStyle: 'solid' as const, + borderTopColor: Kb.Styles.globalColors.black_10, + borderTopWidth: 1, + minHeight: 56, + }, + isElectron: { + borderBottomLeftRadius: Kb.Styles.borderRadius, + borderBottomRightRadius: Kb.Styles.borderRadius, + overflow: 'hidden', + }, + }), + modalFooterNoBorder: Kb.Styles.platformStyles({ + common: { + ...Kb.Styles.padding(Kb.Styles.globalMargins.xsmall, Kb.Styles.globalMargins.small), + minHeight: 56, + }, + isElectron: { + borderBottomLeftRadius: Kb.Styles.borderRadius, + borderBottomRightRadius: Kb.Styles.borderRadius, + overflow: 'hidden', + }, + }), + overlayAvoidTabs: Kb.Styles.platformStyles({ + isElectron: { + backgroundColor: undefined, + height: 0, + pointerEvents: 'none', + }, + }), + overlayContainer: { + ...Kb.Styles.globalStyles.fillAbsolute, + }, + overlayStyle: Kb.Styles.platformStyles({ + isElectron: {alignItems: 'center', flexGrow: 1, justifyContent: 'center', pointerEvents: 'none'}, + }), + overlayTransparent: {backgroundColor: undefined}, +})) diff --git a/shared/router-v2/screen-layout.tsx b/shared/router-v2/screen-layout.tsx new file mode 100644 index 000000000000..08c0f64501c6 --- /dev/null +++ b/shared/router-v2/screen-layout.tsx @@ -0,0 +1,156 @@ +import * as Kb from '@/common-adapters' +import * as React from 'react' +import {isTablet} from '@/constants/platform' +import {SafeAreaProvider, initialWindowMetrics} from 'react-native-safe-area-context' +import type {GetOptions, GetOptionsParams, GetOptionsRet} from '@/constants/types/router' +import type {NativeStackNavigationProp} from '@react-navigation/native-stack' +import type {ParamListBase} from '@react-navigation/native' + +type ModalWrapperProps = { + children: React.ReactNode + navigationOptions?: GetOptionsRet + navigation: NativeStackNavigationProp +} + +type LayoutProps = { + children: React.ReactNode + route: GetOptionsParams['route'] + navigation: GetOptionsParams['navigation'] +} + +// Native-only wrapper components + +const TabScreenWrapper = ({children}: {children: React.ReactNode}) => { + if (isAndroid) { + const {SafeAreaView: RNScreensSafeAreaView} = require('react-native-screens/experimental') as { + SafeAreaView: React.ComponentType<{edges: {bottom: boolean}; style: object; children?: React.ReactNode}> + } + return ( + + {children} + + ) + } + return ( + + {children} + + ) +} + +const StackScreenWrapper = ({children}: {children: React.ReactNode}) => ( + + {children} + +) + +const desktopMakeLayout = ( + isModal: boolean, + _isLoggedOut: boolean, + _isTabScreen: boolean, + getOptions?: GetOptions +) => { + return ({children, route, navigation}: LayoutProps) => { + const {ModalWrapper} = require('./screen-layout-modal.desktop') as { + ModalWrapper: React.ComponentType + } + const navigationOptions: GetOptionsRet | undefined = + typeof getOptions === 'function' ? getOptions({navigation, route}) : getOptions + + let body = children + + if (isModal) { + body = ( + + {body} + + ) + } + + body = {body} + body = {body} + + return body + } +} + +const nativeMakeLayout = ( + isModal: boolean, + isLoggedOut: boolean, + isTabScreen: boolean, + getOptions?: GetOptions +) => { + const modalOffset = isIOS ? 40 : 0 + return function Layout({children, route, navigation}: LayoutProps) { + const navigationOptions = typeof getOptions === 'function' ? getOptions({navigation, route}) : getOptions + const {modalFooter} = navigationOptions ?? {} + + const suspenseContent = {children} + + const wrappedContent = modalFooter ? ( + <> + {suspenseContent} + + {modalFooter.content} + + + ) : ( + suspenseContent + ) + + if (!isModal && !isLoggedOut && isTabScreen) { + return {wrappedContent} + } + if (!isModal && !isLoggedOut) { + return {wrappedContent} + } + + return ( + + + + {wrappedContent} + + + + ) + } +} + +export const makeLayout = isMobile ? nativeMakeLayout : desktopMakeLayout + +const styles = Kb.Styles.styleSheetCreate(() => ({ + keyboard: { + flexGrow: 1, + maxHeight: '100%', + position: 'relative', + }, + modalFooter: Kb.Styles.platformStyles({ + common: { + ...Kb.Styles.padding(Kb.Styles.globalMargins.xsmall, Kb.Styles.globalMargins.small), + borderStyle: 'solid' as const, + borderTopColor: Kb.Styles.globalColors.black_10, + borderTopWidth: 1, + minHeight: 56, + }, + }), + modalFooterNoBorder: Kb.Styles.platformStyles({ + common: { + ...Kb.Styles.padding(Kb.Styles.globalMargins.xsmall, Kb.Styles.globalMargins.small), + minHeight: 56, + }, + }), + tabScreen: { + flex: 1, + }, +})) From b763a346cc4fa781ab57a18bc49d82e7e89c1e71 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Sun, 17 May 2026 22:39:10 -0400 Subject: [PATCH 07/28] merge chat components: giphy, input, list-area, video, conversation, inbox --- .../chat/conversation/giphy/index.native.tsx | 47 - .../giphy/{index.desktop.tsx => index.tsx} | 92 +- .../input-area/normal/input.desktop.tsx | 680 ------- .../input-area/normal/input.native.tsx | 799 --------- .../conversation/input-area/normal/input.tsx | 1565 +++++++++++++++++ .../conversation/list-area/index.native.tsx | 353 ---- .../{index.desktop.tsx => index.tsx} | 539 ++++-- .../attachment/video/videoimpl.desktop.tsx | 99 -- .../attachment/video/videoimpl.native.tsx | 107 -- .../messages/attachment/video/videoimpl.tsx | 221 +++ .../unfurl-list/image/video.desktop.tsx | 78 - .../unfurl/unfurl-list/image/video.native.tsx | 104 -- .../text/unfurl/unfurl-list/image/video.tsx | 193 ++ .../conversation/normal/index.desktop.tsx | 121 -- .../chat/conversation/normal/index.native.tsx | 118 -- shared/chat/conversation/normal/index.tsx | 219 +++ shared/chat/inbox/index.native.tsx | 282 --- .../inbox/{index.desktop.tsx => index.tsx} | 340 +++- shared/router-v2/screen-layout.desktop.tsx | 282 --- shared/router-v2/screen-layout.native.tsx | 118 -- 20 files changed, 2996 insertions(+), 3361 deletions(-) delete mode 100644 shared/chat/conversation/giphy/index.native.tsx rename shared/chat/conversation/giphy/{index.desktop.tsx => index.tsx} (61%) delete mode 100644 shared/chat/conversation/input-area/normal/input.desktop.tsx delete mode 100644 shared/chat/conversation/input-area/normal/input.native.tsx create mode 100644 shared/chat/conversation/input-area/normal/input.tsx delete mode 100644 shared/chat/conversation/list-area/index.native.tsx rename shared/chat/conversation/list-area/{index.desktop.tsx => index.tsx} (52%) delete mode 100644 shared/chat/conversation/messages/attachment/video/videoimpl.desktop.tsx delete mode 100644 shared/chat/conversation/messages/attachment/video/videoimpl.native.tsx create mode 100644 shared/chat/conversation/messages/attachment/video/videoimpl.tsx delete mode 100644 shared/chat/conversation/messages/text/unfurl/unfurl-list/image/video.desktop.tsx delete mode 100644 shared/chat/conversation/messages/text/unfurl/unfurl-list/image/video.native.tsx create mode 100644 shared/chat/conversation/messages/text/unfurl/unfurl-list/image/video.tsx delete mode 100644 shared/chat/conversation/normal/index.desktop.tsx delete mode 100644 shared/chat/conversation/normal/index.native.tsx create mode 100644 shared/chat/conversation/normal/index.tsx delete mode 100644 shared/chat/inbox/index.native.tsx rename shared/chat/inbox/{index.desktop.tsx => index.tsx} (54%) delete mode 100644 shared/router-v2/screen-layout.desktop.tsx delete mode 100644 shared/router-v2/screen-layout.native.tsx diff --git a/shared/chat/conversation/giphy/index.native.tsx b/shared/chat/conversation/giphy/index.native.tsx deleted file mode 100644 index db191d9a09b7..000000000000 --- a/shared/chat/conversation/giphy/index.native.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import * as Kb from '@/common-adapters' -import {colors, darkColors} from '@/styles/colors' -import {WebView} from 'react-native-webview' -import noop from 'lodash/noop' -import {useHooks} from './hooks' -import {useColorScheme} from 'react-native' - -const GiphySearch = () => { - const p = useHooks() - const source = {uri: p.galleryURL} - const darkMode = useColorScheme() === 'dark' - const injectedJavaScript = ` -(function() { - window.document.querySelector("body").style.backgroundColor = "${ - darkMode ? darkColors.white : colors.white - }"; -})(); -` - - return ( - - {p.previews ? ( - - ) : ( - - - - )} - - ) -} - -const styles = Kb.Styles.styleSheetCreate( - () => - ({ - container: {height: 80}, - }) as const -) - -export default GiphySearch diff --git a/shared/chat/conversation/giphy/index.desktop.tsx b/shared/chat/conversation/giphy/index.tsx similarity index 61% rename from shared/chat/conversation/giphy/index.desktop.tsx rename to shared/chat/conversation/giphy/index.tsx index 807d8e8fc73c..d78aec4fa3a6 100644 --- a/shared/chat/conversation/giphy/index.desktop.tsx +++ b/shared/chat/conversation/giphy/index.tsx @@ -1,21 +1,38 @@ -import * as React from 'react' import * as Kb from '@/common-adapters' -import UnfurlImage from '../messages/text/unfurl/unfurl-list/image' -import {getMargins, scaledWidth} from './width' +import * as React from 'react' import {useHooks} from './hooks' -const gridHeight = 100 +// Stub type to avoid dom lib dependency in native tsconfig +type DivRef = { + getBoundingClientRect: () => DOMRect + clientWidth: number +} -const GiphySearch = () => { +const DesktopGiphySearch = () => { + const {getMargins, scaledWidth} = require('./width') as { + getMargins: (w: number, widths: Array) => Array + scaledWidth: (w: number) => number + } + const UnfurlImage = (require('../messages/text/unfurl/unfurl-list/image') as {default: React.ComponentType<{ + autoplayVideo: boolean + height: number + isVideo: boolean + onClick: () => void + style: object + url: string + width: number + }>}).default + const gridHeight = 100 const props = useHooks() const [width, setWidth] = React.useState(undefined) - const divRef = React.useRef(null) + const divRef = React.useRef(null) const learnMoreUrlProps = Kb.useClickURL('https://keybase.io/docs/chat/linkpreviews') React.useEffect(() => { if (!divRef.current) return - const cs = getComputedStyle(divRef.current) - setWidth(divRef.current.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight)) + const gc = (globalThis as {getComputedStyle?: (el: unknown) => {paddingLeft: string; paddingRight: string}}).getComputedStyle + const cs = gc?.(divRef.current) + setWidth(divRef.current.clientWidth - parseFloat(cs?.paddingLeft ?? '0') - parseFloat(cs?.paddingRight ?? '0')) }, []) let margins: Array = [] @@ -32,7 +49,7 @@ const GiphySearch = () => { } style={Kb.Styles.collapseStyles([ styles.scrollContainer, Kb.Styles.platformStyles({isElectron: {overflowY: width ? 'auto' : 'scroll'}}), @@ -89,6 +106,53 @@ const GiphySearch = () => { ) } +const NativeGiphySearch = () => { + const {colors, darkColors} = require('@/styles/colors') as { + colors: Record + darkColors: Record + } + const {WebView} = require('react-native-webview') as {WebView: React.ComponentType<{ + onMessage: () => void + injectedJavaScript: string + allowsInlineMediaPlayback: boolean + source: {uri: string} + automaticallyAdjustContentInsets: boolean + mediaPlaybackRequiresUserAction: boolean + }>} + const {useColorScheme} = require('react-native') as {useColorScheme: () => string | null} + const noop = (require('lodash/noop') as {default: () => void}).default + + const p = useHooks() + const source = {uri: p.galleryURL} + const darkMode = useColorScheme() === 'dark' + const injectedJavaScript = ` +(function() { + window.document.querySelector("body").style.backgroundColor = "${ + darkMode ? darkColors['white'] : colors['white'] + }"; +})(); +` + + return ( + + {p.previews ? ( + + ) : ( + + + + )} + + ) +} + const styles = Kb.Styles.styleSheetCreate( () => ({ @@ -116,7 +180,6 @@ const styles = Kb.Styles.styleSheetCreate( lineHeight: 17, }, }), - loadingContainer: { minHeight: 200, }, @@ -144,4 +207,11 @@ const styles = Kb.Styles.styleSheetCreate( }) as const ) -export default GiphySearch +const nativeStyles = Kb.Styles.styleSheetCreate( + () => + ({ + container: {height: 80}, + }) as const +) + +export default isMobile ? NativeGiphySearch : DesktopGiphySearch diff --git a/shared/chat/conversation/input-area/normal/input.desktop.tsx b/shared/chat/conversation/input-area/normal/input.desktop.tsx deleted file mode 100644 index 60b6fe6569b2..000000000000 --- a/shared/chat/conversation/input-area/normal/input.desktop.tsx +++ /dev/null @@ -1,680 +0,0 @@ -import * as C from '@/constants' -import * as T from '@/constants/types' -import * as Kb from '@/common-adapters' -import * as React from 'react' -import * as InputState from '../input-state' -import SetExplodingMessagePopup from './set-explode-popup' -import Typing from './typing' -import type {Props as InputLowLevelProps, TextInfo, RefType} from './input.shared' -import type {PlatformInputProps as Props} from './input.shared' -export type {Selection, RefType, TextInfo, PlatformInputProps} from './input.shared' -import {EmojiPickerDesktop} from '@/chat/emoji-picker/container' -import {KeyEventHandler} from '@/common-adapters/key-event-handler.desktop' -import {formatDurationShort} from '@/util/timestamp' -import {useSuggestors} from '../suggestors' -import {ScrollContext} from '@/chat/conversation/normal/context' -import {getTextStyle} from '@/common-adapters/text.styles' -import {useColorScheme} from 'react-native' -import KB2 from '@/util/electron.desktop' -import {useConversationThreadID} from '../../thread-context' - -const {getPathForFile} = KB2.functions - -const maybeParseInt = (input: string | number, radix: number): number => - typeof input === 'string' ? parseInt(input, radix) : input - -export function Input(p: InputLowLevelProps) { - const {style: _style, onChangeText: _onChangeText, multiline, ref} = p - const {textType = 'Body', rowsMax, rowsMin, padding, placeholder, onKeyUp: _onKeyUp} = p - const {allowKeyboardEvents, className, disabled, autoFocus, onKeyDown: _onKeyDown, onEnterKeyDown} = p - - const isDarkMode = useColorScheme() === 'dark' - - const [value, setValue] = React.useState('') - // this isn't a value react can set on the input, so we need to drive it manually - const selectionRef = React.useRef({end: 0, start: 0}) - const inputSingleRef = React.useRef(null) - const inputMultiRef = React.useRef(null) - - const onChangeTextRef = React.useRef(_onChangeText) - React.useEffect(() => { - onChangeTextRef.current = _onChangeText - }, [_onChangeText]) - const [onChange] = React.useState(() => (e: {target: HTMLInputElement | HTMLTextAreaElement}) => { - const s = e.target.value - setValue(s) - onChangeTextRef.current?.(s) - }) - const onSelect = (e: {currentTarget: HTMLInputElement | HTMLTextAreaElement}) => { - selectionRef.current = { - end: e.currentTarget.selectionEnd || 0, - start: e.currentTarget.selectionStart || 0, - } - } - - React.useImperativeHandle(ref, () => { - const i = multiline ? inputMultiRef.current : inputSingleRef.current - return { - blur: () => { - i?.blur() - }, - clear: () => { - if (i) { - i.value = '' - onChange({target: i}) - } - }, - focus: () => { - i?.focus() - }, - getBoundingClientRect: () => { - return i?.getBoundingClientRect() - }, - getSelection: () => { - return selectionRef.current - }, - isFocused: () => !!i && document.activeElement === i, - transformText: (fn: (textInfo: TextInfo) => TextInfo, reflectChange: boolean): void => { - const ti = fn({selection: selectionRef.current, text: value}) - // defer since we can do this in other renders - setTimeout(() => { - setValue(ti.text) - selectionRef.current = {end: ti.selection?.end ?? 0, start: ti.selection?.start ?? 0} - // defer this else we'll get onSelect called and wipe it out - setTimeout(() => { - if (i && ti.selection) { - if (typeof ti.selection.start === 'number') { - i.selectionStart = ti.selection.start - } - if (typeof ti.selection.end === 'number') { - i.selectionEnd = ti.selection.end - } - } - }, 10) - if (reflectChange) { - setTimeout(() => { - if (!i) return - onChange({target: i}) - }, 100) - } - }, 0) - }, - value, - } - }, [value, multiline, onChange]) - - const rows = multiline ? rowsMin || Math.min(2, rowsMax || 2) : 0 - const style = (() => { - const textStyle = getTextStyle(textType, isDarkMode) - if (multiline) { - const heightStyles: {minHeight: number; maxHeight?: number} = { - minHeight: - rows * (textStyle.lineHeight === undefined ? 20 : maybeParseInt(textStyle.lineHeight, 10) || 20) + - (padding ? Kb.Styles.globalMargins[padding] * 2 : 0), - } - - if (rowsMax) { - heightStyles.maxHeight = - rowsMax * (textStyle.lineHeight === undefined ? 20 : maybeParseInt(textStyle.lineHeight, 10) || 20) - } - - const paddingStyles = padding ? Kb.Styles.padding(Kb.Styles.globalMargins[padding]) : {} - - return Kb.Styles.collapseStyles([ - inputLowLevelStyles.noChrome, // noChrome comes before because we want lineHeight set in multiline - textStyle, - inputLowLevelStyles.multiline, - heightStyles, - paddingStyles, - _style, - ]) - } else { - return Kb.Styles.collapseStyles([ - textStyle, - inputLowLevelStyles.noChrome, // noChrome comes after to unset lineHeight in singleline - _style, - ]) - } - })() - - const isComposingIMERef = React.useRef(false) - - const onCompositionStart = () => { - isComposingIMERef.current = true - } - - const onCompositionEnd = () => { - isComposingIMERef.current = false - } - - const onKeyDown = (e: React.KeyboardEvent) => { - if (isComposingIMERef.current) { - return - } - _onKeyDown?.(e) - if (onEnterKeyDown && e.key === 'Enter' && !(e.shiftKey || e.ctrlKey || e.altKey)) { - onEnterKeyDown(e) - } - } - - const onKeyUp = (e: React.KeyboardEvent) => { - if (isComposingIMERef.current) { - return - } - _onKeyUp?.(e) - } - - const commonProps = { - autoFocus, - className, - onChange, - onCompositionEnd, - onCompositionStart, - onKeyDown, - onKeyUp, - onSelect, - placeholder, - value, - ...(disabled ? {readOnly: true} : {}), - ...((allowKeyboardEvents ?? true) ? {'data-allow-keyboard-shortcuts': 'true'} : {}), - } - - return multiline ? ( -