diff options
-rw-r--r-- | package.json | 3 | ||||
-rw-r--r-- | src/App.web.tsx | 10 | ||||
-rw-r--r-- | src/platform/urls.tsx | 35 | ||||
-rw-r--r-- | src/state/models/navigation.ts | 7 | ||||
-rw-r--r-- | src/state/models/root-store.ts | 1 | ||||
-rw-r--r-- | src/view/shell/web/DesktopHeader.tsx | 14 | ||||
-rw-r--r-- | src/view/shell/web/DesktopSearch.tsx | 165 | ||||
-rw-r--r-- | web/webpack.config.js | 21 | ||||
-rw-r--r-- | yarn.lock | 9 |
9 files changed, 232 insertions, 33 deletions
diff --git a/package.json b/package.json index 6aa3add95..e61a38811 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "android": "react-native run-android", "ios": "react-native run-ios --simulator=\"iPhone 14\"", - "web": "webpack-dev-server --config ./web/webpack.config.js -d inline-source-map --hot --color", + "web": "webpack-dev-server --config ./web/webpack.config.js -d inline-source-map --hot --color --history-api-fallback", "start": "react-native start", "clean-cache": "rm -rf node_modules/.cache/babel-loader/*", "test": "jest --forceExit --testTimeout=20000 --bail", @@ -39,6 +39,7 @@ "base64-js": "^1.5.1", "email-validator": "^2.0.4", "he": "^1.2.0", + "history": "^5.3.0", "lodash.chunk": "^4.2.0", "lodash.clonedeep": "^4.5.0", "lodash.isequal": "^4.5.0", diff --git a/src/App.web.tsx b/src/App.web.tsx index 4f9fd3e53..1c6efba8f 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -1,5 +1,6 @@ import React, {useState, useEffect} from 'react' import {SafeAreaProvider} from 'react-native-safe-area-context' +import {getInitialURL} from 'platform/urls' import * as view from './view/index' import {RootStoreModel, setupState, RootStoreProvider} from './state' import {WebShell} from './view/shell/web' @@ -13,7 +14,14 @@ function App() { // init useEffect(() => { view.setup() - setupState().then(setRootStore) + setupState().then(store => { + setRootStore(store) + getInitialURL().then(url => { + if (url) { + store.nav.handleLink(url) + } + }) + }) }, []) // show nothing prior to init diff --git a/src/platform/urls.tsx b/src/platform/urls.tsx index 0f24dd678..f544262d7 100644 --- a/src/platform/urls.tsx +++ b/src/platform/urls.tsx @@ -1,31 +1,20 @@ import {Linking} from 'react-native' -import {isIOS, isAndroid, isNative, isWeb} from './detection' +import {createBrowserHistory, createMemoryHistory} from 'history' +import {isNative, isWeb} from './detection' -export function makeAppUrl(path = '') { - if (isIOS) { - return `bskyapp://${path}` - } else if (isAndroid) { - return `bsky://app${path}` - } else { - // @ts-ignore window exists -prf - return `${window.location.origin}${path}` - } -} - -export function extractHashFragment(url: string): string { - return url.split('#')[1] || '' -} - -export async function getInitialURL(): Promise<string> { +export async function getInitialURL(): Promise<string | undefined> { if (isNative) { const url = await Linking.getInitialURL() if (url) { return url } - return makeAppUrl() + return undefined } else { // @ts-ignore window exists -prf - return window.location.toString() + if (window.location.pathname !== '/') { + return window.location.pathname + } + return undefined } } @@ -35,3 +24,11 @@ export function clearHash() { window.location.hash = '' } } + +export function getHistory() { + if (isWeb) { + return createBrowserHistory() + } else { + return createMemoryHistory() + } +} diff --git a/src/state/models/navigation.ts b/src/state/models/navigation.ts index feb03b367..9cbc678d0 100644 --- a/src/state/models/navigation.ts +++ b/src/state/models/navigation.ts @@ -2,6 +2,7 @@ import {RootStoreModel} from './root-store' import {makeAutoObservable} from 'mobx' import {TABS_ENABLED} from 'lib/build-flags' import {segmentClient} from 'lib/analytics' +import {getHistory} from 'platform/urls' let __id = 0 function genId() { @@ -41,6 +42,7 @@ export type HistoryPtr = string // `{tabId}-{historyId}` export class NavigationTabModel { id = genId() history: HistoryItem[] + browserHistory: any index = 0 isNewTab = false @@ -48,6 +50,7 @@ export class NavigationTabModel { this.history = [ {url: TabPurposeMainPath[fixedTabPurpose], ts: Date.now(), id: genId()}, ] + this.browserHistory = getHistory() makeAutoObservable(this, { serialize: false, hydrate: false, @@ -122,6 +125,7 @@ export class NavigationTabModel { this.history.push({url: fixedUrl, ts: Date.now(), id: genId()}) } this.history.push({url, title, ts: Date.now(), id: genId()}) + this.browserHistory.push(url) this.index = this.history.length - 1 } } @@ -142,6 +146,7 @@ export class NavigationTabModel { goBack() { if (this.canGoBack) { this.index-- + this.browserHistory.back() } } @@ -155,12 +160,14 @@ export class NavigationTabModel { goForward() { if (this.canGoForward) { this.index++ + this.browserHistory.forward() } } goToIndex(index: number) { if (index >= 0 && index <= this.history.length - 1) { this.index = index + this.browserHistory.go(index) } } diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts index 11d496351..229c7b3c0 100644 --- a/src/state/models/root-store.ts +++ b/src/state/models/root-store.ts @@ -120,7 +120,6 @@ export class RootStoreModel { async handleSessionChange(agent: AtpAgent) { this.log.debug('RootStoreModel:handleSessionChange') this.agent = agent - this.nav.clear() this.me.clear() await this.me.load() } diff --git a/src/view/shell/web/DesktopHeader.tsx b/src/view/shell/web/DesktopHeader.tsx index de0b2a218..9fb22a7eb 100644 --- a/src/view/shell/web/DesktopHeader.tsx +++ b/src/view/shell/web/DesktopHeader.tsx @@ -2,7 +2,6 @@ import React from 'react' import {observer} from 'mobx-react-lite' import {Pressable, StyleSheet, TouchableOpacity, View} from 'react-native' import {Text} from 'view/com/util/text/Text' -import {Link} from 'view/com/util/Link' import {UserAvatar} from 'view/com/util/UserAvatar' import {usePalette} from 'lib/hooks/usePalette' import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' @@ -17,6 +16,7 @@ import { MagnifyingGlassIcon, CogIcon, } from 'lib/icons' +import {DesktopSearch} from './DesktopSearch' interface NavItemProps { count?: number @@ -90,6 +90,7 @@ export const DesktopHeader = observer(function DesktopHeader({}: { const store = useStores() const pal = usePalette('default') const onPressCompose = () => store.shell.openComposer({}) + return ( <View style={[styles.header, pal.borderDark, pal.view]}> <Text type="title-xl" style={[pal.text, styles.title]}> @@ -128,15 +129,7 @@ export const DesktopHeader = observer(function DesktopHeader({}: { </Text> </TouchableOpacity> <View style={styles.space20} /> - <Link href="/search" style={[pal.view, pal.borderDark, styles.search]}> - <MagnifyingGlassIcon - size={18} - style={[pal.textLight, styles.searchIconWrapper]} - /> - <Text type="md-thin" style={pal.textLight}> - Search - </Text> - </Link> + <DesktopSearch /> <View style={styles.space15} /> <ProfileItem /> <NavItem @@ -157,6 +150,7 @@ const styles = StyleSheet.create({ paddingLeft: 30, paddingRight: 40, borderBottomWidth: 1, + zIndex: 1, }, spaceFlex: { diff --git a/src/view/shell/web/DesktopSearch.tsx b/src/view/shell/web/DesktopSearch.tsx new file mode 100644 index 000000000..8998219da --- /dev/null +++ b/src/view/shell/web/DesktopSearch.tsx @@ -0,0 +1,165 @@ +import React from 'react' +import {TextInput, View, StyleSheet, TouchableOpacity, Text} from 'react-native' +import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view' +import {observer} from 'mobx-react-lite' +import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' +import {MagnifyingGlassIcon} from 'lib/icons' +import {ProfileCard} from '../../com/profile/ProfileCard' + +export const DesktopSearch = observer(function DesktopSearch() { + const store = useStores() + const pal = usePalette('default') + const textInput = React.useRef<TextInput>(null) + const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false) + const [query, setQuery] = React.useState<string>('') + const autocompleteView = React.useMemo<UserAutocompleteViewModel>( + () => new UserAutocompleteViewModel(store), + [store], + ) + + const onChangeQuery = (text: string) => { + setQuery(text) + if (text.length > 0 && isInputFocused) { + autocompleteView.setActive(true) + autocompleteView.setPrefix(text) + } else { + autocompleteView.setActive(false) + } + } + + const onPressCancelSearch = () => { + setQuery('') + autocompleteView.setActive(false) + } + + return ( + <View + style={[ + styles.searchContainer, + pal.borderDark, + pal.view, + styles.container, + styles.search, + ]}> + <View style={[styles.container, styles.searchInputWrapper]}> + <MagnifyingGlassIcon + size={18} + style={[pal.textLight, styles.searchIconWrapper]} + /> + + <TextInput + testID="searchTextInput" + ref={textInput} + placeholder="Search" + placeholderTextColor={pal.colors.textLight} + selectTextOnFocus + returnKeyType="search" + value={query} + style={[pal.textLight, styles.headerSearchInput]} + onFocus={() => setIsInputFocused(true)} + onBlur={() => setIsInputFocused(false)} + onChangeText={onChangeQuery} + /> + + {query ? ( + <View style={styles.headerCancelBtn}> + <TouchableOpacity onPress={onPressCancelSearch}> + <Text>Cancel</Text> + </TouchableOpacity> + </View> + ) : undefined} + </View> + + <View + style={[ + {backgroundColor: pal.colors.background}, + styles.searchResultsContainer, + styles.container, + ]}> + {query && autocompleteView.searchRes.length ? ( + <> + {autocompleteView.searchRes.map(item => ( + <ProfileCard + key={item.did} + handle={item.handle} + displayName={item.displayName} + avatar={item.avatar} + /> + ))} + </> + ) : query && !autocompleteView.searchRes.length ? ( + <View> + <Text style={[pal.textLight, styles.searchPrompt]}> + No results found for {autocompleteView.prefix} + </Text> + </View> + ) : isInputFocused ? ( + <View> + <Text style={[pal.textLight, styles.searchPrompt]}> + Search for users on the network + </Text> + </View> + ) : null} + </View> + </View> + ) +}) + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + search: { + maxWidth: 300, + borderRadius: 20, + borderWidth: 1, + }, + searchIconWrapper: { + flexDirection: 'row', + width: 30, + justifyContent: 'center', + marginRight: 2, + }, + searchInputWrapper: { + flexDirection: 'row', + }, + searchContainer: { + flexWrap: 'wrap', + position: 'relative', + paddingHorizontal: 12, + paddingTop: 6, + paddingBottom: 6, + }, + headerMenuBtn: { + width: 40, + height: 30, + marginLeft: 6, + }, + searchResultsContainer: { + position: 'fixed', + flex: 1, + flexDirection: 'column', + borderRadius: 30, + paddingHorizontal: 12, + paddingVertical: 8, + marginTop: 50, + }, + headerSearchIcon: { + marginRight: 6, + alignSelf: 'center', + }, + headerSearchInput: { + flex: 1, + fontSize: 17, + width: '100%', + }, + headerCancelBtn: { + width: 60, + paddingLeft: 10, + }, + searchPrompt: { + textAlign: 'center', + paddingTop: 10, + }, +}) diff --git a/web/webpack.config.js b/web/webpack.config.js index e41f0a148..18056190d 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -81,6 +81,27 @@ module.exports = { ], }, + devServer: { + historyApiFallback: { + rewrites: [ + {from: /.*\/bundle.web.js/, to: '/bundle.web.js'}, + { + from: /.*^\/(.*.hot-update.json)$/, + to: function (context) { + return '/' + context.parsedUrl.pathname.split('/').pop() + }, + }, + { + from: /.*^\/(.*.hot-update.js)$/, + to: function (context) { + return '/' + context.parsedUrl.pathname.split('/').pop() + }, + }, + {from: /.*/, to: '/index.html'}, + ], + }, + }, + resolve: { alias: { 'react-native$': 'react-native-web', diff --git a/yarn.lock b/yarn.lock index 669978646..f3b968e70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1256,7 +1256,7 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@^7.0.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4": version "7.21.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== @@ -6906,6 +6906,13 @@ hermes-profile-transformer@^0.0.6: dependencies: source-map "^0.7.3" +history@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/history/-/history-5.3.0.tgz#1548abaa245ba47992f063a0783db91ef201c73b" + integrity sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ== + dependencies: + "@babel/runtime" "^7.7.6" + hoist-non-react-statics@^3.3.0: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" |