diff --git a/src/lib/errors.ts b/src/lib/errors.ts new file mode 100644 index 0000000000..c14a2dbe49 --- /dev/null +++ b/src/lib/errors.ts @@ -0,0 +1,4 @@ +export function isNetworkError(e: unknown) { + const str = String(e) + return str.includes('Aborted') || str.includes('Network request failed') +} diff --git a/src/lib/strings.ts b/src/lib/strings.ts index 66dd59708c..0474c330dc 100644 --- a/src/lib/strings.ts +++ b/src/lib/strings.ts @@ -1,6 +1,7 @@ import {AtUri} from '../third-party/uri' import {Entity} from '../third-party/api/src/client/types/app/bsky/feed/post' import {PROD_SERVICE} from '../state' +import {isNetworkError} from './errors' import TLDs from 'tlds' export const MAX_DISPLAY_NAME = 64 @@ -201,7 +202,7 @@ export function enforceLen(str: string, len: number): string { } export function cleanError(str: string): string { - if (str.includes('Network request failed')) { + if (isNetworkError(str)) { return 'Unable to connect. Please check your internet connection and try again.' } if (str.startsWith('Error: ')) { diff --git a/src/state/index.ts b/src/state/index.ts index 32efea3f3b..739742d4ab 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -27,10 +27,19 @@ export async function setupState() { console.error('Failed to load state from storage', e) } - await rootStore.session.setup() + console.log('Initial hydrate', rootStore.me) + rootStore.session + .connect() + .then(() => { + console.log('Session connected', rootStore.me) + return rootStore.fetchStateUpdate() + }) + .catch(e => { + console.log('Failed initial connect', e) + }) // @ts-ignore .on() is correct -prf api.sessionManager.on('session', () => { - if (!api.sessionManager.session && rootStore.session.isAuthed) { + if (!api.sessionManager.session && rootStore.session.hasSession) { // reset session rootStore.session.clear() } else if (api.sessionManager.session) { @@ -44,9 +53,6 @@ export async function setupState() { storage.save(ROOT_STATE_STORAGE_KEY, snapshot) }) - await rootStore.fetchStateUpdate() - console.log(rootStore.me) - // periodic state fetch setInterval(() => { rootStore.fetchStateUpdate() diff --git a/src/state/lib/api.ts b/src/state/lib/api.ts index 842905d1d9..f17d0337c9 100644 --- a/src/state/lib/api.ts +++ b/src/state/lib/api.ts @@ -12,6 +12,8 @@ import {APP_BSKY_GRAPH} from '../../third-party/api' import {RootStoreModel} from '../models/root-store' import {extractEntities} from '../../lib/strings' +const TIMEOUT = 10e3 // 10s + export function doPolyfill() { AtpApi.xrpc.fetch = fetchHandler } @@ -175,10 +177,14 @@ async function fetchHandler( reqBody = JSON.stringify(reqBody) } + const controller = new AbortController() + const to = setTimeout(() => controller.abort(), TIMEOUT) + const res = await fetch(reqUri, { method: reqMethod, headers: reqHeaders, body: reqBody, + signal: controller.signal, }) const resStatus = res.status @@ -197,6 +203,9 @@ async function fetchHandler( throw new Error('TODO: non-textual response body') } } + + clearTimeout(to) + return { status: resStatus, headers: resHeaders, diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts index 33db426a45..f5dce9e05e 100644 --- a/src/state/models/feed-view.ts +++ b/src/state/models/feed-view.ts @@ -381,6 +381,9 @@ export class FeedModel { } private async _update() { + if (!this.feed.length) { + return + } this._xLoading() let numToFetch = this.feed.length let cursor = undefined diff --git a/src/state/models/me.ts b/src/state/models/me.ts index e3405b80dd..fde387ebe4 100644 --- a/src/state/models/me.ts +++ b/src/state/models/me.ts @@ -2,6 +2,7 @@ import {makeAutoObservable, runInAction} from 'mobx' import {RootStoreModel} from './root-store' import {MembershipsViewModel} from './memberships-view' import {NotificationsViewModel} from './notifications-view' +import {isObj, hasProp} from '../lib/type-guards' export class MeModel { did?: string @@ -13,7 +14,11 @@ export class MeModel { notifications: NotificationsViewModel constructor(public rootStore: RootStoreModel) { - makeAutoObservable(this, {rootStore: false}, {autoBind: true}) + makeAutoObservable( + this, + {rootStore: false, serialize: false, hydrate: false}, + {autoBind: true}, + ) this.notifications = new NotificationsViewModel(this.rootStore, {}) } @@ -26,9 +31,42 @@ export class MeModel { this.memberships = undefined } + serialize(): unknown { + return { + did: this.did, + handle: this.handle, + displayName: this.displayName, + description: this.description, + } + } + + hydrate(v: unknown) { + if (isObj(v)) { + let did, handle, displayName, description + if (hasProp(v, 'did') && typeof v.did === 'string') { + did = v.did + } + if (hasProp(v, 'handle') && typeof v.handle === 'string') { + handle = v.handle + } + if (hasProp(v, 'displayName') && typeof v.displayName === 'string') { + displayName = v.displayName + } + if (hasProp(v, 'description') && typeof v.description === 'string') { + description = v.description + } + if (did && handle) { + this.did = did + this.handle = handle + this.displayName = displayName + this.description = description + } + } + } + async load() { const sess = this.rootStore.session - if (sess.isAuthed && sess.data) { + if (sess.hasSession && sess.data) { this.did = sess.data.did || '' this.handle = sess.data.handle const profile = await this.rootStore.api.app.bsky.actor.getProfile({ diff --git a/src/state/models/notifications-view.ts b/src/state/models/notifications-view.ts index e81f31a254..f820c71b7c 100644 --- a/src/state/models/notifications-view.ts +++ b/src/state/models/notifications-view.ts @@ -317,6 +317,9 @@ export class NotificationsViewModel { } private async _update() { + if (!this.notifications.length) { + return + } this._xLoading() let numToFetch = this.notifications.length let cursor = undefined diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts index af79ccc1e7..ad306ee9f1 100644 --- a/src/state/models/root-store.ts +++ b/src/state/models/root-store.ts @@ -14,6 +14,7 @@ import {ProfilesViewModel} from './profiles-view' import {LinkMetasViewModel} from './link-metas-view' import {MeModel} from './me' import {OnboardModel} from './onboard' +import {isNetworkError} from '../../lib/errors' export class RootStoreModel { session = new SessionModel(this) @@ -45,12 +46,18 @@ export class RootStoreModel { } async fetchStateUpdate() { - if (!this.session.isAuthed) { + if (!this.session.hasSession) { return } try { + if (!this.session.online) { + await this.session.connect() + } await this.me.fetchStateUpdate() - } catch (e) { + } catch (e: unknown) { + if (isNetworkError(e)) { + this.session.setOnline(false) // connection lost + } console.error('Failed to fetch latest state', e) } } @@ -58,6 +65,7 @@ export class RootStoreModel { serialize(): unknown { return { session: this.session.serialize(), + me: this.me.serialize(), nav: this.nav.serialize(), onboard: this.onboard.serialize(), } @@ -68,6 +76,9 @@ export class RootStoreModel { if (hasProp(v, 'session')) { this.session.hydrate(v.session) } + if (hasProp(v, 'me')) { + this.me.hydrate(v.me) + } if (hasProp(v, 'nav')) { this.nav.hydrate(v.nav) } diff --git a/src/state/models/session.ts b/src/state/models/session.ts index 0f1faeaba0..069e3db32b 100644 --- a/src/state/models/session.ts +++ b/src/state/models/session.ts @@ -7,6 +7,7 @@ import type { import type * as GetAccountsConfig from '../../third-party/api/src/client/types/com/atproto/server/getAccountsConfig' import {isObj, hasProp} from '../lib/type-guards' import {RootStoreModel} from './root-store' +import {isNetworkError} from '../../lib/errors' export type ServiceDescription = GetAccountsConfig.OutputSchema @@ -20,16 +21,20 @@ interface SessionData { export class SessionModel { data: SessionData | null = null + online = false + attemptingConnect = false + private _connectPromise: Promise | undefined constructor(public rootStore: RootStoreModel) { makeAutoObservable(this, { rootStore: false, serialize: false, hydrate: false, + _connectPromise: false, }) } - get isAuthed() { + get hasSession() { return this.data !== null } @@ -91,6 +96,13 @@ export class SessionModel { this.data = data } + setOnline(online: boolean, attemptingConnect?: boolean) { + this.online = online + if (typeof attemptingConnect === 'boolean') { + this.attemptingConnect = attemptingConnect + } + } + updateAuthTokens(session: Session) { if (this.data) { this.setState({ @@ -125,7 +137,14 @@ export class SessionModel { return true } - async setup(): Promise { + async connect(): Promise { + this._connectPromise ??= this._connect() + await this._connectPromise + this._connectPromise = undefined + } + + private async _connect(): Promise { + this.attemptingConnect = true if (!this.configureApi()) { return } @@ -133,14 +152,25 @@ export class SessionModel { try { const sess = await this.rootStore.api.com.atproto.session.get() if (sess.success && this.data && this.data.did === sess.data.did) { + this.setOnline(true, false) + if (this.rootStore.me.did !== sess.data.did) { + this.rootStore.me.clear() + } this.rootStore.me.load().catch(e => { console.error('Failed to fetch local user information', e) }) return // success } - } catch (e: any) {} + } catch (e: any) { + if (isNetworkError(e)) { + this.setOnline(false, false) // connection issue + return + } else { + this.clear() // invalid session cached + } + } - this.clear() // invalid session cached + this.setOnline(false, false) } async describeService(service: string): Promise { @@ -212,7 +242,7 @@ export class SessionModel { } async logout() { - if (this.isAuthed) { + if (this.hasSession) { this.rootStore.api.com.atproto.session.delete().catch((e: any) => { console.error('(Minor issue) Failed to delete session on the server', e) }) diff --git a/src/view/com/composer/ComposePost.tsx b/src/view/com/composer/ComposePost.tsx index 10d9f4b1ca..d53d04abf2 100644 --- a/src/view/com/composer/ComposePost.tsx +++ b/src/view/com/composer/ComposePost.tsx @@ -15,7 +15,10 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {UserAutocompleteViewModel} from '../../../state/models/user-autocomplete-view' import {Autocomplete} from './Autocomplete' import * as Toast from '../util/Toast' -import ProgressCircle from '../util/ProgressCircle' +// @ts-ignore no type definition -prf +import ProgressCircle from 'react-native-progress/Circle' +// @ts-ignore no type definition -prf +import ProgressPie from 'react-native-progress/Pie' import {TextLink} from '../util/Link' import {UserAvatar} from '../util/UserAvatar' import {useStores} from '../../../state' @@ -252,10 +255,26 @@ export const ComposePost = observer(function ComposePost({ {MAX_TEXT_LENGTH - text.length} - + {text.length > DANGER_TEXT_LENGTH ? ( + + ) : ( + + )} console.error('Failed to toggle upvote', record, e)) } + const onCopyPostText = () => { + Clipboard.setString(record.text) + Toast.show('Copied to clipboard') + } const onDeletePost = () => { item.delete().then( () => { @@ -130,6 +135,7 @@ export const PostThreadItem = observer(function PostThreadItem({ itemHref={itemHref} itemTitle={itemTitle} isAuthor={item.author.did === store.me.did} + onCopyPostText={onCopyPostText} onDeletePost={onDeletePost}> @@ -356,7 +363,7 @@ const styles = StyleSheet.create({ maxWidth: 240, }, postText: { - fontFamily: 'Helvetica Neue', + fontFamily: 'System', fontSize: 16, lineHeight: 20.8, // 1.3 of 16px }, diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index 736b40157c..23ec44c6b8 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -8,6 +8,7 @@ import { ViewStyle, } from 'react-native' import {observer} from 'mobx-react-lite' +import Clipboard from '@react-native-clipboard/clipboard' import {AtUri} from '../../../third-party/uri' import * as PostType from '../../../third-party/api/src/client/types/app/bsky/feed/post' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' @@ -110,6 +111,10 @@ export const Post = observer(function Post({ .toggleUpvote() .catch(e => console.error('Failed to toggle upvote', record, e)) } + const onCopyPostText = () => { + Clipboard.setString(record.text) + Toast.show('Copied to clipboard') + } const onDeletePost = () => { item.delete().then( () => { @@ -144,6 +149,7 @@ export const Post = observer(function Post({ authorDisplayName={item.author.displayName} timestamp={item.indexedAt} isAuthor={item.author.did === store.me.did} + onCopyPostText={onCopyPostText} onDeletePost={onDeletePost} /> {replyHref !== '' && ( @@ -206,7 +212,7 @@ const styles = StyleSheet.create({ paddingBottom: 8, }, postText: { - fontFamily: 'Helvetica Neue', + fontFamily: 'System', fontSize: 16, lineHeight: 20.8, // 1.3 of 16px }, diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 4d50531bd1..b34fe239d1 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -1,6 +1,7 @@ import React, {useMemo, useState} from 'react' import {observer} from 'mobx-react-lite' import {StyleSheet, Text, View} from 'react-native' +import Clipboard from '@react-native-clipboard/clipboard' import {AtUri} from '../../../third-party/uri' import * as PostType from '../../../third-party/api/src/client/types/app/bsky/feed/post' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' @@ -66,6 +67,10 @@ export const FeedItem = observer(function FeedItem({ .toggleUpvote() .catch(e => console.error('Failed to toggle upvote', record, e)) } + const onCopyPostText = () => { + Clipboard.setString(record.text) + Toast.show('Copied to clipboard') + } const onDeletePost = () => { item.delete().then( () => { @@ -147,6 +152,7 @@ export const FeedItem = observer(function FeedItem({ authorDisplayName={item.author.displayName} timestamp={item.indexedAt} isAuthor={item.author.did === store.me.did} + onCopyPostText={onCopyPostText} onDeletePost={onDeletePost} /> ) : undefined} @@ -249,7 +255,7 @@ const styles = StyleSheet.create({ minHeight: 36, }, postText: { - fontFamily: 'Helvetica Neue', + fontFamily: 'System', fontSize: 16, lineHeight: 20.8, // 1.3 of 16px }, diff --git a/src/view/com/util/DropdownBtn.tsx b/src/view/com/util/DropdownBtn.tsx index 98e2f3f2ba..b38a6ed997 100644 --- a/src/view/com/util/DropdownBtn.tsx +++ b/src/view/com/util/DropdownBtn.tsx @@ -79,6 +79,7 @@ export function PostDropdownBtn({ itemHref, itemTitle, isAuthor, + onCopyPostText, onDeletePost, }: { style?: StyleProp @@ -86,6 +87,7 @@ export function PostDropdownBtn({ itemHref: string itemTitle: string isAuthor: boolean + onCopyPostText: () => void onDeletePost: () => void }) { const store = useStores() @@ -100,6 +102,13 @@ export function PostDropdownBtn({ }, } : undefined, + { + icon: ['far', 'paste'], + label: 'Copy post text', + onPress() { + onCopyPostText() + }, + }, { icon: 'share', label: 'Share...', diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index 80dde0e061..1994580c17 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -14,6 +14,7 @@ interface PostMetaOpts { authorDisplayName: string | undefined timestamp: string isAuthor: boolean + onCopyPostText: () => void onDeletePost: () => void } @@ -40,6 +41,7 @@ export function PostMeta(opts: PostMetaOpts) { itemHref={opts.itemHref} itemTitle={opts.itemTitle} isAuthor={opts.isAuthor} + onCopyPostText={opts.onCopyPostText} onDeletePost={opts.onDeletePost}> diff --git a/src/view/com/util/ProgressCircle.native.tsx b/src/view/com/util/ProgressCircle.native.tsx deleted file mode 100644 index a09232b4b9..0000000000 --- a/src/view/com/util/ProgressCircle.native.tsx +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-ignore no type definition -prf -import ProgressCircle from 'react-native-progress/Circle' -export default ProgressCircle diff --git a/src/view/com/util/ProgressCircle.tsx b/src/view/com/util/ProgressCircle.tsx deleted file mode 100644 index 0e425a6e65..0000000000 --- a/src/view/com/util/ProgressCircle.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import {View} from 'react-native' -import {CircularProgressbar, buildStyles} from 'react-circular-progressbar' - -const ProgressCircle = ({ - color, - progress, -}: { - color?: string - progress: number -}) => { - return ( - - - - ) -} -export default ProgressCircle diff --git a/src/view/com/util/RichText.tsx b/src/view/com/util/RichText.tsx index 3f0e990878..66b0e2536f 100644 --- a/src/view/com/util/RichText.tsx +++ b/src/view/com/util/RichText.tsx @@ -23,6 +23,13 @@ export function RichText({ numberOfLines?: number }) { if (!entities?.length) { + if (/^\p{Extended_Pictographic}+$/u.test(text) && text.length <= 5) { + style = { + fontSize: 26, + lineHeight: 30, + } + return {text} + } return {text} } if (!style) style = [] @@ -98,13 +105,3 @@ function* toSegments(text: string, entities: Entity[]) { yield text.slice(cursor, text.length) } } - -function stripUsername(v: string): string { - return v.trim().replace('@', '') -} - -function isSameLink(a: string, b: string) { - a = a.startsWith('http') ? a : `https://${a}` - b = b.startsWith('http') ? b : `https://${b}` - return a === b -} diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx index 5d0ec2995e..12aa86a4fc 100644 --- a/src/view/com/util/ViewHeader.tsx +++ b/src/view/com/util/ViewHeader.tsx @@ -1,14 +1,21 @@ import React from 'react' -import {StyleSheet, Text, TouchableOpacity, View} from 'react-native' +import {observer} from 'mobx-react-lite' +import { + ActivityIndicator, + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {colors} from '../../lib/styles' +import {s, colors} from '../../lib/styles' import {MagnifyingGlassIcon} from '../../lib/icons' import {useStores} from '../../../state' const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10} const BACK_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10} -export function ViewHeader({ +export const ViewHeader = observer(function ViewHeader({ title, subtitle, onPost, @@ -27,43 +34,91 @@ export function ViewHeader({ const onPressSearch = () => { store.nav.navigate(`/search`) } + const onPressReconnect = () => { + store.session.connect().catch(e => { + // log for debugging but ignore otherwise + console.log(e) + }) + } return ( - - {store.nav.tab.canGoBack ? ( + <> + + {store.nav.tab.canGoBack ? ( + + + + ) : undefined} + + {title} + {subtitle ? ( + + {subtitle} + + ) : undefined} + - + onPress={onPressCompose} + hitSlop={HITSLOP} + style={styles.btn}> + + + + + + + {!store.session.online ? ( + + {store.session.attemptingConnect ? ( + <> + + + Connecting... + + + ) : ( + <> + + + + Unable to connect + + + Try again + + + )} ) : undefined} - - {title} - {subtitle ? ( - - {subtitle} - - ) : undefined} - - - - - - - - + ) -} +}) const styles = StyleSheet.create({ header: { @@ -108,4 +163,26 @@ const styles = StyleSheet.create({ position: 'relative', top: -1, }, + + offline: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.gray6, + paddingLeft: 15, + paddingRight: 10, + paddingVertical: 8, + borderRadius: 8, + marginHorizontal: 4, + marginTop: 4, + }, + offlineBtn: { + backgroundColor: colors.gray5, + borderRadius: 5, + paddingVertical: 5, + paddingHorizontal: 10, + }, + offlineBtnText: { + color: colors.white, + fontWeight: 'bold', + }, }) diff --git a/src/view/index.ts b/src/view/index.ts index bd0e33cbe7..71769471b8 100644 --- a/src/view/index.ts +++ b/src/view/index.ts @@ -38,6 +38,7 @@ import {faLock} from '@fortawesome/free-solid-svg-icons/faLock' import {faMagnifyingGlass} from '@fortawesome/free-solid-svg-icons/faMagnifyingGlass' import {faMessage} from '@fortawesome/free-regular-svg-icons/faMessage' import {faNoteSticky} from '@fortawesome/free-solid-svg-icons/faNoteSticky' +import {faPaste} from '@fortawesome/free-regular-svg-icons/faPaste' import {faPen} from '@fortawesome/free-solid-svg-icons/faPen' import {faPenNib} from '@fortawesome/free-solid-svg-icons/faPenNib' import {faPenToSquare} from '@fortawesome/free-solid-svg-icons/faPenToSquare' @@ -45,6 +46,7 @@ import {faPlus} from '@fortawesome/free-solid-svg-icons/faPlus' import {faShare} from '@fortawesome/free-solid-svg-icons/faShare' import {faShareFromSquare} from '@fortawesome/free-solid-svg-icons/faShareFromSquare' import {faShield} from '@fortawesome/free-solid-svg-icons/faShield' +import {faSignal} from '@fortawesome/free-solid-svg-icons/faSignal' import {faReply} from '@fortawesome/free-solid-svg-icons/faReply' import {faRetweet} from '@fortawesome/free-solid-svg-icons/faRetweet' import {faRss} from '@fortawesome/free-solid-svg-icons/faRss' @@ -100,6 +102,7 @@ export function setup() { faMagnifyingGlass, faMessage, faNoteSticky, + faPaste, faPen, faPenNib, faPenToSquare, @@ -110,6 +113,7 @@ export function setup() { faShare, faShareFromSquare, faShield, + faSignal, faUser, faUsers, faUserCheck, diff --git a/src/view/lib/styles.ts b/src/view/lib/styles.ts index 1ac6283a29..d3fc8c70fc 100644 --- a/src/view/lib/styles.ts +++ b/src/view/lib/styles.ts @@ -10,6 +10,9 @@ export const colors = { gray3: '#c1b9b9', gray4: '#968d8d', gray5: '#645454', + gray6: '#423737', + gray7: '#2D2626', + gray8: '#131010', blue0: '#bfe1ff', blue1: '#8bc7fd', @@ -131,6 +134,7 @@ export const s = StyleSheet.create({ flexRow: {flexDirection: 'row'}, flexCol: {flexDirection: 'column'}, flex1: {flex: 1}, + alignCenter: {alignItems: 'center'}, // position absolute: {position: 'absolute'}, diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 5925b6f806..a1d2027385 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -10,6 +10,8 @@ import {FeedModel} from '../../state/models/feed-view' import {ScreenParams} from '../routes' import {s, colors} from '../lib/styles' +const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20} + export const Home = observer(function Home({ navIdx, visible, @@ -95,7 +97,10 @@ export const Home = observer(function Home({ onPressTryAgain={onPressTryAgain} /> {defaultFeedView.hasNewLatest ? ( - + Load new posts diff --git a/src/view/screens/Login.tsx b/src/view/screens/Login.tsx index abd5274da2..4175e0a340 100644 --- a/src/view/screens/Login.tsx +++ b/src/view/screens/Login.tsx @@ -25,6 +25,7 @@ import {useStores, DEFAULT_SERVICE} from '../../state' import {ServiceDescription} from '../../state/models/session' import {ServerInputModel} from '../../state/models/shell-ui' import {ComAtprotoAccountCreate} from '../../third-party/api/index' +import {isNetworkError} from '../../lib/errors' enum ScreenState { SigninOrCreateAccount, @@ -186,7 +187,7 @@ const Signin = ({onPressBack}: {onPressBack: () => void}) => { setIsProcessing(false) if (errMsg.includes('Authentication Required')) { setError('Invalid username or password') - } else if (errMsg.includes('Network request failed')) { + } else if (isNetworkError(e)) { setError( 'Unable to contact your service. Please check your Internet connection.', ) @@ -210,16 +211,6 @@ const Signin = ({onPressBack}: {onPressBack: () => void}) => { - {error ? ( - - - - - - {error} - - - ) : undefined} void}) => { /> - + {error ? ( + + + + + + {error} + + + ) : undefined} + Back - {isProcessing ? ( + {!serviceDescription || isProcessing ? ( ) : ( Next )} + {!serviceDescription || isProcessing ? ( + Connecting... + ) : undefined} ) @@ -689,18 +693,19 @@ const styles = StyleSheet.create({ color: colors.white, }, error: { - borderTopWidth: 1, - borderTopColor: colors.blue1, + borderWidth: 1, + borderColor: colors.red5, + backgroundColor: colors.red4, flexDirection: 'row', alignItems: 'center', - marginTop: 5, - backgroundColor: colors.blue2, + marginTop: -5, + marginHorizontal: 20, + marginBottom: 15, + borderRadius: 8, paddingHorizontal: 8, - paddingVertical: 5, + paddingVertical: 8, }, errorFloating: { - borderWidth: 1, - borderColor: colors.blue1, marginBottom: 20, marginHorizontal: 20, borderRadius: 8, diff --git a/src/view/screens/Search.tsx b/src/view/screens/Search.tsx index 53e28c1c9e..ec32678c6d 100644 --- a/src/view/screens/Search.tsx +++ b/src/view/screens/Search.tsx @@ -1,5 +1,13 @@ import React, {useEffect, useState, useMemo, useRef} from 'react' -import {StyleSheet, Text, TextInput, TouchableOpacity, View} from 'react-native' +import { + Keyboard, + ScrollView, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, +} from 'react-native' import {ViewHeader} from '../com/util/ViewHeader' import {SuggestedFollows} from '../com/discover/SuggestedFollows' import {UserAvatar} from '../com/util/UserAvatar' @@ -50,13 +58,14 @@ export const Search = ({navIdx, visible, params}: ScreenParams) => { ref={textInput} placeholder="Type your query here..." selectTextOnFocus + returnKeyType="search" style={styles.input} onChangeText={onChangeQuery} /> {query ? ( - + {autocompleteView.searchRes.map((item, i) => ( { ))} - + ) : ( )} diff --git a/src/view/shell/desktop-web/index.tsx b/src/view/shell/desktop-web/index.tsx index 13acbbfed1..1949543495 100644 --- a/src/view/shell/desktop-web/index.tsx +++ b/src/view/shell/desktop-web/index.tsx @@ -9,7 +9,7 @@ export const DesktopWebShell: React.FC = observer(({children}) => { const store = useStores() return ( - {store.session.isAuthed ? ( + {store.session.hasSession ? ( <> {children} diff --git a/src/view/shell/mobile/index.tsx b/src/view/shell/mobile/index.tsx index d653944d19..e3e30decc7 100644 --- a/src/view/shell/mobile/index.tsx +++ b/src/view/shell/mobile/index.tsx @@ -231,7 +231,7 @@ export const MobileShell: React.FC = observer(() => { transform: [{scale: newTabInterp.value}], })) - if (!store.session.isAuthed) { + if (!store.session.hasSession) { return (