Resolve all remaining lint issues (#88)

* Rework 'navIdx' variables from number arrays to strings to avoid equality-check failures in react hooks

* Resolve all remaining lint issues

* Fix tests

* Use node v18 in gh action test
zio/stable
Paul Frazee 2023-01-24 13:00:11 -06:00 committed by GitHub
parent 3a90114f3a
commit f36c956536
60 changed files with 478 additions and 482 deletions

View File

@ -25,6 +25,10 @@ jobs:
name: Run tests name: Run tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Install node 18
uses: actions/setup-node@v3
with:
node-version: 18
- name: Check out Git repository - name: Check out Git repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Yarn install - name: Yarn install

View File

@ -6,7 +6,7 @@ describe('NavigationModel', () => {
beforeEach(() => { beforeEach(() => {
model = new NavigationModel() model = new NavigationModel()
model.setTitle([0, 0], 'title') model.setTitle('0-0', 'title')
}) })
afterAll(() => { afterAll(() => {
@ -44,7 +44,7 @@ describe('NavigationModel', () => {
}) })
it('should call the isCurrentScreen method', () => { it('should call the isCurrentScreen method', () => {
expect(model.isCurrentScreen(11, 0)).toEqual(false) expect(model.isCurrentScreen('11', 0)).toEqual(false)
}) })
it('should call the tab getter', () => { it('should call the tab getter', () => {

View File

@ -41,7 +41,6 @@ function* dateGen() {
yield new Date(start).toISOString() yield new Date(start).toISOString()
start += 1e3 start += 1e3
} }
return ''
} }
export async function createServer(): Promise<TestPDS> { export async function createServer(): Promise<TestPDS> {

View File

@ -4,8 +4,6 @@
* *
* @format * @format
*/ */
const metroResolver = require('metro-resolver')
const path = require('path')
module.exports = { module.exports = {
transformer: { transformer: {

View File

@ -10,6 +10,7 @@ import {ThemeProvider} from './view/lib/ThemeContext'
import * as view from './view/index' import * as view from './view/index'
import {RootStoreModel, setupState, RootStoreProvider} from './state' import {RootStoreModel, setupState, RootStoreProvider} from './state'
import {MobileShell} from './view/shell/mobile' import {MobileShell} from './view/shell/mobile'
import {s} from './view/lib/styles'
const App = observer(() => { const App = observer(() => {
const [rootStore, setRootStore] = useState<RootStoreModel | undefined>( const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
@ -39,7 +40,7 @@ const App = observer(() => {
} }
return ( return (
<GestureHandlerRootView style={{flex: 1}}> <GestureHandlerRootView style={s.h100pct}>
<RootSiblingParent> <RootSiblingParent>
<RootStoreProvider value={rootStore}> <RootStoreProvider value={rootStore}>
<ThemeProvider theme={rootStore.shell.darkMode ? 'dark' : 'light'}> <ThemeProvider theme={rootStore.shell.darkMode ? 'dark' : 'light'}>

View File

@ -78,7 +78,7 @@ export function extractEntities(
let ents: Entity[] = [] let ents: Entity[] = []
{ {
// mentions // mentions
const re = /(^|\s|\()(@)([a-zA-Z0-9\.-]+)(\b)/g const re = /(^|\s|\()(@)([a-zA-Z0-9.-]+)(\b)/g
while ((match = re.exec(text))) { while ((match = re.exec(text))) {
if (knownHandles && !knownHandles.has(match[3])) { if (knownHandles && !knownHandles.has(match[3])) {
continue // not a known handle continue // not a known handle
@ -133,7 +133,7 @@ interface DetectedLink {
type DetectedLinkable = string | DetectedLink type DetectedLinkable = string | DetectedLink
export function detectLinkables(text: string): DetectedLinkable[] { export function detectLinkables(text: string): DetectedLinkable[] {
const re = const re =
/((^|\s|\()@[a-z0-9\.-]*)|((^|\s|\()https?:\/\/[\S]+)|((^|\s|\()(?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*)/gi /((^|\s|\()@[a-z0-9.-]*)|((^|\s|\()https?:\/\/[\S]+)|((^|\s|\()(?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*)/gi
const segments = [] const segments = []
let match let match
let start = 0 let start = 0
@ -154,14 +154,12 @@ export function detectLinkables(text: string): DetectedLinkable[] {
matchValue = matchValue.slice(1) matchValue = matchValue.slice(1)
} }
{ // strip ending puncuation
// strip ending puncuation if (/[.,;!?]$/.test(matchValue)) {
if (/[.,;!?]$/.test(matchValue)) { matchValue = matchValue.slice(0, -1)
matchValue = matchValue.slice(0, -1) }
} if (/[)]$/.test(matchValue) && !matchValue.includes('(')) {
if (/[)]$/.test(matchValue) && !matchValue.includes('(')) { matchValue = matchValue.slice(0, -1)
matchValue = matchValue.slice(0, -1)
}
} }
if (start !== matchIndex) { if (start !== matchIndex) {
@ -185,8 +183,8 @@ export function makeValidHandle(str: string): string {
} }
export function createFullHandle(name: string, domain: string): string { export function createFullHandle(name: string, domain: string): string {
name = (name || '').replace(/[\.]+$/, '') name = (name || '').replace(/[.]+$/, '')
domain = (domain || '').replace(/^[\.]+/, '') domain = (domain || '').replace(/^[.]+/, '')
return `${name}.${domain}` return `${name}.${domain}`
} }

View File

@ -3,7 +3,7 @@ import {TABS_ENABLED} from '../../build-flags'
let __id = 0 let __id = 0
function genId() { function genId() {
return ++__id return String(++__id)
} }
// NOTE // NOTE
@ -24,10 +24,10 @@ interface HistoryItem {
url: string url: string
ts: number ts: number
title?: string title?: string
id: number id: string
} }
export type HistoryPtr = [number, number] export type HistoryPtr = string // `{tabId}-{historyId}`
export class NavigationTabModel { export class NavigationTabModel {
id = genId() id = genId()
@ -151,7 +151,7 @@ export class NavigationTabModel {
} }
} }
setTitle(id: number, title: string) { setTitle(id: string, title: string) {
this.history = this.history.map(h => { this.history = this.history.map(h => {
if (h.id === id) { if (h.id === id) {
return {...h, title} return {...h, title}
@ -174,7 +174,7 @@ export class NavigationTabModel {
} }
} }
hydrate(v: unknown) { hydrate(_v: unknown) {
// TODO fixme // TODO fixme
// if (isObj(v)) { // if (isObj(v)) {
// if (hasProp(v, 'history') && Array.isArray(v.history)) { // if (hasProp(v, 'history') && Array.isArray(v.history)) {
@ -241,7 +241,7 @@ export class NavigationModel {
return this.tabs.length return this.tabs.length
} }
isCurrentScreen(tabId: number, index: number) { isCurrentScreen(tabId: string, index: number) {
return this.tab.id === tabId && this.tab.index === index return this.tab.id === tabId && this.tab.index === index
} }
@ -257,7 +257,8 @@ export class NavigationModel {
} }
setTitle(ptr: HistoryPtr, title: string) { setTitle(ptr: HistoryPtr, title: string) {
this.tabs.find(t => t.id === ptr[0])?.setTitle(ptr[1], title) const [tid, hid] = ptr.split('-')
this.tabs.find(t => t.id === tid)?.setTitle(hid, title)
} }
handleLink(url: string) { handleLink(url: string) {
@ -338,7 +339,7 @@ export class NavigationModel {
} }
} }
hydrate(v: unknown) { hydrate(_v: unknown) {
// TODO fixme // TODO fixme
this.clear() this.clear()
/*if (isObj(v)) { /*if (isObj(v)) {

View File

@ -297,7 +297,7 @@ export const ComposePost = observer(function ComposePost({
) )
} }
}) })
}, [text, pal.link]) }, [text, pal.link, pal.text])
return ( return (
<KeyboardAvoidingView <KeyboardAvoidingView
@ -393,7 +393,7 @@ export const ComposePost = observer(function ComposePost({
ref={textInput} ref={textInput}
multiline multiline
scrollEnabled scrollEnabled
onChangeText={(text: string) => onChangeText(text)} onChangeText={(str: string) => onChangeText(str)}
onPaste={onPaste} onPaste={onPaste}
placeholder={selectTextInputPlaceholder} placeholder={selectTextInputPlaceholder}
placeholderTextColor={pal.colors.textLight} placeholderTextColor={pal.colors.textLight}
@ -475,7 +475,7 @@ export const ComposePost = observer(function ComposePost({
) )
}) })
const atPrefixRegex = /@([a-z0-9\.]*)$/i const atPrefixRegex = /@([a-z0-9.]*)$/i
function extractTextAutocompletePrefix(text: string) { function extractTextAutocompletePrefix(text: string) {
const match = atPrefixRegex.exec(text) const match = atPrefixRegex.exec(text)
if (match) { if (match) {

View File

@ -39,7 +39,7 @@ export const SuggestedFollows = observer(
// Using default import (React.use...) instead of named import (use...) to be able to mock store's data in jest environment // Using default import (React.use...) instead of named import (use...) to be able to mock store's data in jest environment
const view = React.useMemo<SuggestedActorsViewModel>( const view = React.useMemo<SuggestedActorsViewModel>(
() => new SuggestedActorsViewModel(store), () => new SuggestedActorsViewModel(store),
[], [store],
) )
useEffect(() => { useEffect(() => {
@ -54,7 +54,7 @@ export const SuggestedFollows = observer(
if (!view.isLoading && !view.hasError && !view.hasContent) { if (!view.isLoading && !view.hasError && !view.hasContent) {
onNoSuggestions?.() onNoSuggestions?.()
} }
}, [view, view.isLoading, view.hasError, view.hasContent]) }, [view, view.isLoading, view.hasError, view.hasContent, onNoSuggestions])
const onPressTryAgain = () => const onPressTryAgain = () =>
view view
@ -128,7 +128,7 @@ export const SuggestedFollows = observer(
keyExtractor={item => item._reactKey} keyExtractor={item => item._reactKey}
renderItem={renderItem} renderItem={renderItem}
style={s.flex1} style={s.flex1}
contentContainerStyle={{paddingBottom: 200}} contentContainerStyle={s.contentContainer}
/> />
</View> </View>
)} )}

View File

@ -207,12 +207,7 @@ const ChooseAccountForm = ({
style={[pal.borderDark, styles.group]} style={[pal.borderDark, styles.group]}
onPress={() => onSelectAccount(undefined)}> onPress={() => onSelectAccount(undefined)}>
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}> <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
<View style={s.p10}> <Text style={[styles.accountText, styles.accountTextOther]}>
<View
style={[pal.btn, {width: 30, height: 30, borderRadius: 15}]}
/>
</View>
<Text style={styles.accountText}>
<Text type="lg" style={pal.text}> <Text type="lg" style={pal.text}>
Other account Other account
</Text> </Text>
@ -556,7 +551,7 @@ const ForgotPasswordForm = ({
{!serviceDescription || isProcessing ? ( {!serviceDescription || isProcessing ? (
<ActivityIndicator /> <ActivityIndicator />
) : !email ? ( ) : !email ? (
<Text type="xl-bold" style={[pal.link, s.pr5, {opacity: 0.5}]}> <Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}>
Next Next
</Text> </Text>
) : ( ) : (
@ -691,7 +686,7 @@ const SetNewPasswordForm = ({
{isProcessing ? ( {isProcessing ? (
<ActivityIndicator /> <ActivityIndicator />
) : !resetCode || !password ? ( ) : !resetCode || !password ? (
<Text type="xl-bold" style={[pal.link, s.pr5, {opacity: 0.5}]}> <Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}>
Next Next
</Text> </Text>
) : ( ) : (
@ -810,6 +805,9 @@ const styles = StyleSheet.create({
alignItems: 'baseline', alignItems: 'baseline',
paddingVertical: 10, paddingVertical: 10,
}, },
accountTextOther: {
paddingLeft: 12,
},
error: { error: {
backgroundColor: colors.red4, backgroundColor: colors.red4,
flexDirection: 'row', flexDirection: 'row',
@ -832,4 +830,5 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
marginRight: 5, marginRight: 5,
}, },
dimmed: {opacity: 0.5},
}) })

View File

@ -121,7 +121,7 @@ export function Component({
</View> </View>
</View> </View>
{error !== '' && ( {error !== '' && (
<View style={{marginTop: 20}}> <View style={styles.errorContainer}>
<ErrorMessage message={error} /> <ErrorMessage message={error} />
</View> </View>
)} )}
@ -231,4 +231,5 @@ const styles = StyleSheet.create({
marginBottom: 36, marginBottom: 36,
marginHorizontal: -14, marginHorizontal: -14,
}, },
errorContainer: {marginTop: 20},
}) })

View File

@ -56,7 +56,7 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
</View> </View>
<View style={styles.group}> <View style={styles.group}>
<Text style={styles.label}>Other service</Text> <Text style={styles.label}>Other service</Text>
<View style={{flexDirection: 'row'}}> <View style={s.flexRow}>
<BottomSheetTextInput <BottomSheetTextInput
testID="customServerTextInput" testID="customServerTextInput"
style={styles.textInput} style={styles.textInput}

View File

@ -1,12 +1,13 @@
import React from 'react' import React from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {View, FlatList} from 'react-native' import {FlatList, StyleSheet, View} from 'react-native'
import {NotificationsViewModel} from '../../../state/models/notifications-view' import {NotificationsViewModel} from '../../../state/models/notifications-view'
import {FeedItem} from './FeedItem' import {FeedItem} from './FeedItem'
import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {ErrorMessage} from '../util/error/ErrorMessage' import {ErrorMessage} from '../util/error/ErrorMessage'
import {EmptyState} from '../util/EmptyState' import {EmptyState} from '../util/EmptyState'
import {OnScrollCb} from '../../lib/hooks/useOnMainScroll' import {OnScrollCb} from '../../lib/hooks/useOnMainScroll'
import {s} from '../../lib/styles'
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
@ -29,7 +30,7 @@ export const Feed = observer(function Feed({
<EmptyState <EmptyState
icon="bell" icon="bell"
message="No notifications yet!" message="No notifications yet!"
style={{paddingVertical: 40}} style={styles.emptyState}
/> />
) )
} }
@ -58,14 +59,10 @@ export const Feed = observer(function Feed({
} }
} }
return ( return (
<View style={{flex: 1}}> <View style={s.h100pct}>
{view.isLoading && !data && <NotificationFeedLoadingPlaceholder />} {view.isLoading && !data && <NotificationFeedLoadingPlaceholder />}
{view.hasError && ( {view.hasError && (
<ErrorMessage <ErrorMessage message={view.error} onPressTryAgain={onPressTryAgain} />
message={view.error}
style={{margin: 6}}
onPressTryAgain={onPressTryAgain}
/>
)} )}
{data && ( {data && (
<FlatList <FlatList
@ -76,9 +73,13 @@ export const Feed = observer(function Feed({
onRefresh={onRefresh} onRefresh={onRefresh}
onEndReached={onEndReached} onEndReached={onEndReached}
onScroll={onScroll} onScroll={onScroll}
contentContainerStyle={{paddingBottom: 200}} contentContainerStyle={s.contentContainer}
/> />
)} )}
</View> </View>
) )
}) })
const styles = StyleSheet.create({
emptyState: {paddingVertical: 40},
})

View File

@ -19,14 +19,13 @@ import {TABS_ENABLED} from '../../../build-flags'
const Intro = () => ( const Intro = () => (
<View style={styles.explainer}> <View style={styles.explainer}>
<Text <Text
style={[ style={[styles.explainerHeading, s.normal, styles.explainerHeadingIntro]}>
styles.explainerHeading, Welcome to{' '}
s.normal, <Text style={[s.bold, s.blue3, styles.explainerHeadingBrand]}>
{lineHeight: 60, paddingTop: 50, paddingBottom: 50}, Bluesky
]}> </Text>
Welcome to <Text style={[s.bold, s.blue3, {fontSize: 56}]}>Bluesky</Text>
</Text> </Text>
<Text style={[styles.explainerDesc, {fontSize: 24}]}> <Text style={[styles.explainerDesc, styles.explainerDescIntro]}>
This is an early beta. Your feedback is appreciated! This is an early beta. Your feedback is appreciated!
</Text> </Text>
</View> </View>
@ -161,11 +160,18 @@ const styles = StyleSheet.create({
textAlign: 'center', textAlign: 'center',
marginBottom: 16, marginBottom: 16,
}, },
explainerHeadingIntro: {
lineHeight: 60,
paddingTop: 50,
paddingBottom: 50,
},
explainerHeadingBrand: {fontSize: 56},
explainerDesc: { explainerDesc: {
fontSize: 18, fontSize: 18,
textAlign: 'center', textAlign: 'center',
marginBottom: 16, marginBottom: 16,
}, },
explainerDescIntro: {fontSize: 24},
explainerImg: { explainerImg: {
resizeMode: 'contain', resizeMode: 'contain',
maxWidth: '100%', maxWidth: '100%',

View File

@ -53,11 +53,7 @@ export const PostRepostedBy = observer(function PostRepostedBy({
if (view.hasError) { if (view.hasError) {
return ( return (
<View> <View>
<ErrorMessage <ErrorMessage message={view.error} onPressTryAgain={onRefresh} />
message={view.error}
style={{margin: 6}}
onPressTryAgain={onRefresh}
/>
</View> </View>
) )
} }

View File

@ -7,6 +7,7 @@ import {
} from '../../../state/models/post-thread-view' } from '../../../state/models/post-thread-view'
import {PostThreadItem} from './PostThreadItem' import {PostThreadItem} from './PostThreadItem'
import {ErrorMessage} from '../util/error/ErrorMessage' import {ErrorMessage} from '../util/error/ErrorMessage'
import {s} from '../../lib/styles'
export const PostThread = observer(function PostThread({ export const PostThread = observer(function PostThread({
uri, uri,
@ -60,11 +61,7 @@ export const PostThread = observer(function PostThread({
if (view.hasError) { if (view.hasError) {
return ( return (
<View> <View>
<ErrorMessage <ErrorMessage message={view.error} onPressTryAgain={onRefresh} />
message={view.error}
style={{margin: 6}}
onPressTryAgain={onRefresh}
/>
</View> </View>
) )
} }
@ -84,8 +81,8 @@ export const PostThread = observer(function PostThread({
onRefresh={onRefresh} onRefresh={onRefresh}
onLayout={onLayout} onLayout={onLayout}
onScrollToIndexFailed={onScrollToIndexFailed} onScrollToIndexFailed={onScrollToIndexFailed}
style={{flex: 1}} style={s.h100pct}
contentContainerStyle={{paddingBottom: 200}} contentContainerStyle={s.contentContainer}
/> />
) )
}) })

View File

@ -80,7 +80,7 @@ export const PostThreadItem = observer(function PostThreadItem({
.catch(e => store.log.error('Failed to toggle upvote', e)) .catch(e => store.log.error('Failed to toggle upvote', e))
} }
const onCopyPostText = () => { const onCopyPostText = () => {
Clipboard.setString(record.text) Clipboard.setString(record?.text || '')
Toast.show('Copied to clipboard') Toast.show('Copied to clipboard')
} }
const onDeletePost = () => { const onDeletePost = () => {
@ -131,8 +131,8 @@ export const PostThreadItem = observer(function PostThreadItem({
</Link> </Link>
</View> </View>
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
<View style={[styles.meta, {paddingTop: 5, paddingBottom: 0}]}> <View style={[styles.meta, styles.metaExpandedLine1]}>
<View style={{flexDirection: 'row', alignItems: 'baseline'}}> <View style={[s.flexRow, s.alignBaseline]}>
<Link <Link
style={styles.metaItem} style={styles.metaItem}
href={authorHref} href={authorHref}
@ -305,10 +305,8 @@ export const PostThreadItem = observer(function PostThreadItem({
lineHeight={1.3} lineHeight={1.3}
/> />
</View> </View>
) : ( ) : undefined}
<View style={{height: 5}} /> <PostEmbeds embed={item.post.embed} style={s.mb10} />
)}
<PostEmbeds embed={item.post.embed} style={{marginBottom: 10}} />
<PostCtrls <PostCtrls
itemHref={itemHref} itemHref={itemHref}
itemTitle={itemTitle} itemTitle={itemTitle}
@ -389,6 +387,10 @@ const styles = StyleSheet.create({
paddingTop: 2, paddingTop: 2,
paddingBottom: 2, paddingBottom: 2,
}, },
metaExpandedLine1: {
paddingTop: 5,
paddingBottom: 0,
},
metaItem: { metaItem: {
paddingRight: 5, paddingRight: 5,
maxWidth: 240, maxWidth: 240,

View File

@ -48,11 +48,7 @@ export const PostVotedBy = observer(function PostVotedBy({
if (view.hasError) { if (view.hasError) {
return ( return (
<View> <View>
<ErrorMessage <ErrorMessage message={view.error} onPressTryAgain={onRefresh} />
message={view.error}
style={{margin: 6}}
onPressTryAgain={onRefresh}
/>
</View> </View>
) )
} }

View File

@ -156,7 +156,7 @@ export const Post = observer(function Post({
timestamp={item.post.indexedAt} timestamp={item.post.indexedAt}
/> />
{replyAuthorDid !== '' && ( {replyAuthorDid !== '' && (
<View style={[s.flexRow, s.mb2, {alignItems: 'center'}]}> <View style={[s.flexRow, s.mb2, s.alignCenter]}>
<FontAwesomeIcon <FontAwesomeIcon
icon="reply" icon="reply"
size={9} size={9}
@ -187,10 +187,8 @@ export const Post = observer(function Post({
lineHeight={1.3} lineHeight={1.3}
/> />
</View> </View>
) : ( ) : undefined}
<View style={{height: 5}} /> <PostEmbeds embed={item.post.embed} style={s.mb10} />
)}
<PostEmbeds embed={item.post.embed} style={{marginBottom: 10}} />
<PostCtrls <PostCtrls
itemHref={itemHref} itemHref={itemHref}
itemTitle={itemTitle} itemTitle={itemTitle}

View File

@ -1,6 +1,6 @@
import React, {useState, useEffect} from 'react' import React, {useState, useEffect} from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {View} from 'react-native' import {StyleSheet, View} from 'react-native'
import {LoadingPlaceholder} from '../util/LoadingPlaceholder' import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
import {ErrorMessage} from '../util/error/ErrorMessage' import {ErrorMessage} from '../util/error/ErrorMessage'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
@ -31,9 +31,9 @@ export const PostText = observer(function PostText({
if (!model || model.isLoading || model.uri !== uri) { if (!model || model.isLoading || model.uri !== uri) {
return ( return (
<View> <View>
<LoadingPlaceholder width="100%" height={8} style={{marginTop: 6}} /> <LoadingPlaceholder width="100%" height={8} style={styles.mt6} />
<LoadingPlaceholder width="100%" height={8} style={{marginTop: 6}} /> <LoadingPlaceholder width="100%" height={8} style={styles.mt6} />
<LoadingPlaceholder width={100} height={8} style={{marginTop: 6}} /> <LoadingPlaceholder width={100} height={8} style={styles.mt6} />
</View> </View>
) )
} }
@ -56,3 +56,7 @@ export const PostText = observer(function PostText({
</View> </View>
) )
}) })
const styles = StyleSheet.create({
mt6: {marginTop: 6},
})

View File

@ -5,6 +5,7 @@ import {
View, View,
FlatList, FlatList,
StyleProp, StyleProp,
StyleSheet,
ViewStyle, ViewStyle,
} from 'react-native' } from 'react-native'
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
@ -14,6 +15,7 @@ import {FeedModel} from '../../../state/models/feed-view'
import {FeedItem} from './FeedItem' import {FeedItem} from './FeedItem'
import {PromptButtons} from './PromptButtons' import {PromptButtons} from './PromptButtons'
import {OnScrollCb} from '../../lib/hooks/useOnMainScroll' import {OnScrollCb} from '../../lib/hooks/useOnMainScroll'
import {s} from '../../lib/styles'
const COMPOSE_PROMPT_ITEM = {_reactKey: '__prompt__'} const COMPOSE_PROMPT_ITEM = {_reactKey: '__prompt__'}
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
@ -47,7 +49,7 @@ export const Feed = observer(function Feed({
<EmptyState <EmptyState
icon="bars" icon="bars"
message="This feed is empty!" message="This feed is empty!"
style={{paddingVertical: 40}} style={styles.emptyState}
/> />
) )
} else { } else {
@ -76,7 +78,7 @@ export const Feed = observer(function Feed({
} }
const FeedFooter = () => const FeedFooter = () =>
feed.isLoading ? ( feed.isLoading ? (
<View style={{paddingTop: 20}}> <View style={styles.feedFooter}>
<ActivityIndicator /> <ActivityIndicator />
</View> </View>
) : ( ) : (
@ -87,11 +89,7 @@ export const Feed = observer(function Feed({
{!data && <PromptButtons onPressCompose={onPressCompose} />} {!data && <PromptButtons onPressCompose={onPressCompose} />}
{feed.isLoading && !data && <PostFeedLoadingPlaceholder />} {feed.isLoading && !data && <PostFeedLoadingPlaceholder />}
{feed.hasError && ( {feed.hasError && (
<ErrorMessage <ErrorMessage message={feed.error} onPressTryAgain={onPressTryAgain} />
message={feed.error}
style={{margin: 6}}
onPressTryAgain={onPressTryAgain}
/>
)} )}
{feed.hasLoaded && data && ( {feed.hasLoaded && data && (
<FlatList <FlatList
@ -101,7 +99,7 @@ export const Feed = observer(function Feed({
renderItem={renderItem} renderItem={renderItem}
ListFooterComponent={FeedFooter} ListFooterComponent={FeedFooter}
refreshing={feed.isRefreshing} refreshing={feed.isRefreshing}
contentContainerStyle={{paddingBottom: 100}} contentContainerStyle={s.contentContainer}
onScroll={onScroll} onScroll={onScroll}
onRefresh={onRefresh} onRefresh={onRefresh}
onEndReached={onEndReached} onEndReached={onEndReached}
@ -110,3 +108,8 @@ export const Feed = observer(function Feed({
</View> </View>
) )
}) })
const styles = StyleSheet.create({
feedFooter: {paddingTop: 20},
emptyState: {paddingVertical: 40},
})

View File

@ -124,7 +124,7 @@ export const FeedItem = observer(function ({
style={[ style={[
styles.bottomReplyLine, styles.bottomReplyLine,
{borderColor: pal.colors.replyLine}, {borderColor: pal.colors.replyLine},
isNoTop ? {top: 64} : undefined, isNoTop ? styles.bottomReplyLineNoTop : undefined,
]} ]}
/> />
)} )}
@ -163,7 +163,7 @@ export const FeedItem = observer(function ({
timestamp={item.post.indexedAt} timestamp={item.post.indexedAt}
/> />
{!isChild && replyAuthorDid !== '' && ( {!isChild && replyAuthorDid !== '' && (
<View style={[s.flexRow, s.mb2, {alignItems: 'center'}]}> <View style={[s.flexRow, s.mb2, s.alignCenter]}>
<FontAwesomeIcon <FontAwesomeIcon
icon="reply" icon="reply"
size={9} size={9}
@ -195,9 +195,7 @@ export const FeedItem = observer(function ({
lineHeight={1.3} lineHeight={1.3}
/> />
</View> </View>
) : ( ) : undefined}
<View style={{height: 5}} />
)}
{item.post.embed ? ( {item.post.embed ? (
<PostEmbeds embed={item.post.embed} style={styles.embed} /> <PostEmbeds embed={item.post.embed} style={styles.embed} />
) : null} ) : null}
@ -281,6 +279,7 @@ const styles = StyleSheet.create({
bottom: 0, bottom: 0,
borderLeftWidth: 2, borderLeftWidth: 2,
}, },
bottomReplyLineNoTop: {top: 64},
includeReason: { includeReason: {
flexDirection: 'row', flexDirection: 'row',
paddingLeft: 50, paddingLeft: 50,

View File

@ -54,11 +54,7 @@ export const ProfileFollowers = observer(function ProfileFollowers({
if (view.hasError) { if (view.hasError) {
return ( return (
<View> <View>
<ErrorMessage <ErrorMessage message={view.error} onPressTryAgain={onRefresh} />
message={view.error}
style={{margin: 6}}
onPressTryAgain={onRefresh}
/>
</View> </View>
) )
} }

View File

@ -54,11 +54,7 @@ export const ProfileFollows = observer(function ProfileFollows({
if (view.hasError) { if (view.hasError) {
return ( return (
<View> <View>
<ErrorMessage <ErrorMessage message={view.error} onPressTryAgain={onRefresh} />
message={view.error}
style={{margin: 6}}
onPressTryAgain={onRefresh}
/>
</View> </View>
) )
} }

View File

@ -100,22 +100,14 @@ export const ProfileHeader = observer(function ProfileHeader({
<LoadingPlaceholder width="100%" height={120} /> <LoadingPlaceholder width="100%" height={120} />
<View <View
style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}> style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}>
<LoadingPlaceholder <LoadingPlaceholder width={80} height={80} style={styles.br40} />
width={80}
height={80}
style={{borderRadius: 40}}
/>
</View> </View>
<View style={styles.content}> <View style={styles.content}>
<View style={[styles.buttonsLine]}> <View style={[styles.buttonsLine]}>
<LoadingPlaceholder <LoadingPlaceholder width={100} height={31} style={styles.br50} />
width={100}
height={31}
style={{borderRadius: 50}}
/>
</View> </View>
<View style={styles.displayNameLine}> <View style={styles.displayNameLine}>
<Text type="title-xl" style={[pal.text, {lineHeight: 38}]}> <Text type="title-xl" style={[pal.text, styles.title]}>
{view.displayName || view.handle} {view.displayName || view.handle}
</Text> </Text>
</View> </View>
@ -208,7 +200,7 @@ export const ProfileHeader = observer(function ProfileHeader({
) : undefined} ) : undefined}
</View> </View>
<View style={styles.displayNameLine}> <View style={styles.displayNameLine}>
<Text type="title-xl" style={[pal.text, {lineHeight: 38}]}> <Text type="title-xl" style={[pal.text, styles.title]}>
{view.displayName || view.handle} {view.displayName || view.handle}
</Text> </Text>
</View> </View>
@ -349,6 +341,7 @@ const styles = StyleSheet.create({
// paddingLeft: 86, // paddingLeft: 86,
// marginBottom: 14, // marginBottom: 14,
}, },
title: {lineHeight: 38},
handleLine: { handleLine: {
flexDirection: 'row', flexDirection: 'row',
@ -369,4 +362,7 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
marginBottom: 5, marginBottom: 5,
}, },
br40: {borderRadius: 40},
br50: {borderRadius: 50},
}) })

View File

@ -57,7 +57,7 @@ export const Link = observer(function Link({
) )
}) })
export const TextLink = observer(function Link({ export const TextLink = observer(function TextLink({
type = 'md', type = 'md',
style, style,
href, href,

View File

@ -19,23 +19,15 @@ export function LoadingPlaceholder({
return ( return (
<View <View
style={[ style={[
styles.loadingPlaceholder,
{ {
width, width,
height, height,
backgroundColor: theme.palette.default.backgroundLight, backgroundColor: theme.palette.default.backgroundLight,
borderRadius: 6,
overflow: 'hidden',
}, },
style, style,
]}> ]}
<View />
style={{
width,
height,
backgroundColor: theme.palette.default.backgroundLight,
}}
/>
</View>
) )
} }
@ -137,6 +129,9 @@ export function NotificationFeedLoadingPlaceholder() {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
loadingPlaceholder: {
borderRadius: 6,
},
post: { post: {
flexDirection: 'row', flexDirection: 'row',
padding: 10, padding: 10,

View File

@ -128,10 +128,7 @@ export function PostCtrls(opts: PostCtrlsOpts) {
hitSlop={HITSLOP} hitSlop={HITSLOP}
onPress={opts.onPressReply}> onPress={opts.onPressReply}>
<CommentBottomArrow <CommentBottomArrow
style={[ style={[defaultCtrlColor, opts.big ? s.mt2 : styles.mt1]}
defaultCtrlColor,
opts.big ? {marginTop: 2} : {marginTop: 1},
]}
strokeWidth={3} strokeWidth={3}
size={opts.big ? 20 : 15} size={opts.big ? 20 : 15}
/> />
@ -181,10 +178,7 @@ export function PostCtrls(opts: PostCtrlsOpts) {
/> />
) : ( ) : (
<HeartIcon <HeartIcon
style={[ style={[defaultCtrlColor, opts.big ? styles.mt1 : undefined]}
defaultCtrlColor,
opts.big ? {marginTop: 1} : undefined,
]}
strokeWidth={3} strokeWidth={3}
size={opts.big ? 20 : 16} size={opts.big ? 20 : 16}
/> />
@ -244,4 +238,7 @@ const styles = StyleSheet.create({
ctrlIconUpvoted: { ctrlIconUpvoted: {
color: colors.red3, color: colors.red3,
}, },
mt1: {
marginTop: 1,
},
}) })

View File

@ -67,7 +67,7 @@ export function PostEmbeds({
<AutoSizedImage <AutoSizedImage
uri={embed.images[0].thumb} uri={embed.images[0].thumb}
onPress={() => openLightbox(0)} onPress={() => openLightbox(0)}
containerStyle={{borderRadius: 8}} containerStyle={styles.singleImage}
/> />
</View> </View>
) )
@ -120,6 +120,9 @@ const styles = StyleSheet.create({
imagesContainer: { imagesContainer: {
marginTop: 4, marginTop: 4,
}, },
singleImage: {
borderRadius: 8,
},
extOuter: { extOuter: {
borderWidth: 1, borderWidth: 1,
borderRadius: 8, borderRadius: 8,

View File

@ -41,7 +41,7 @@ export function Selector({
width: middle.width, width: middle.width,
} }
return [left, middle, right] return [left, middle, right]
}, [selectedIndex, items, itemLayouts]) }, [selectedIndex, itemLayouts])
const underlineStyle = { const underlineStyle = {
backgroundColor: pal.colors.text, backgroundColor: pal.colors.text,

View File

@ -62,8 +62,8 @@ export function UserAvatar({
]) ])
}, [onSelectNewAvatar]) }, [onSelectNewAvatar])
const renderSvg = (size: number, initials: string) => ( const renderSvg = (svgSize: number, svgInitials: string) => (
<Svg width={size} height={size} viewBox="0 0 100 100"> <Svg width={svgSize} height={svgSize} viewBox="0 0 100 100">
<Defs> <Defs>
<LinearGradient id="grad" x1="0" y1="0" x2="1" y2="1"> <LinearGradient id="grad" x1="0" y1="0" x2="1" y2="1">
<Stop offset="0" stopColor={gradients.blue.start} stopOpacity="1" /> <Stop offset="0" stopColor={gradients.blue.start} stopOpacity="1" />
@ -78,7 +78,7 @@ export function UserAvatar({
x="50" x="50"
y="67" y="67"
textAnchor="middle"> textAnchor="middle">
{initials} {svgInitials}
</Text> </Text>
</Svg> </Svg>
) )
@ -88,7 +88,11 @@ export function UserAvatar({
<TouchableOpacity onPress={handleEditAvatar}> <TouchableOpacity onPress={handleEditAvatar}>
{avatar ? ( {avatar ? (
<Image <Image
style={{width: size, height: size, borderRadius: (size / 2) | 0}} style={{
width: size,
height: size,
borderRadius: Math.floor(size / 2),
}}
source={{uri: avatar}} source={{uri: avatar}}
/> />
) : ( ) : (
@ -104,7 +108,7 @@ export function UserAvatar({
</TouchableOpacity> </TouchableOpacity>
) : avatar ? ( ) : avatar ? (
<Image <Image
style={{width: size, height: size, borderRadius: (size / 2) | 0}} style={{width: size, height: size, borderRadius: Math.floor(size / 2)}}
resizeMode="stretch" resizeMode="stretch"
source={{uri: avatar}} source={{uri: avatar}}
/> />

View File

@ -1,6 +1,6 @@
import React, {useState, useEffect} from 'react' import React, {useState, useEffect} from 'react'
import {AppBskyActorGetProfile as GetProfile} from '@atproto/api' import {AppBskyActorGetProfile as GetProfile} from '@atproto/api'
import {StyleProp, TextStyle} from 'react-native' import {StyleProp, StyleSheet, TextStyle} from 'react-native'
import {Link} from './Link' import {Link} from './Link'
import {Text} from './text/Text' import {Text} from './text/Text'
import {LoadingPlaceholder} from './LoadingPlaceholder' import {LoadingPlaceholder} from './LoadingPlaceholder'
@ -53,7 +53,7 @@ export function UserInfoText({
return () => { return () => {
aborted = true aborted = true
} }
}, [did, store.api.app.bsky]) }, [did, store.profiles])
let inner let inner
if (didFail) { if (didFail) {
@ -73,7 +73,7 @@ export function UserInfoText({
<LoadingPlaceholder <LoadingPlaceholder
width={80} width={80}
height={8} height={8}
style={{position: 'relative', top: 1, left: 2}} style={styles.loadingPlaceholder}
/> />
) )
} }
@ -91,3 +91,7 @@ export function UserInfoText({
return inner return inner
} }
const styles = StyleSheet.create({
loadingPlaceholder: {position: 'relative', top: 1, left: 2},
})

View File

@ -11,8 +11,8 @@ import {UserAvatar} from './UserAvatar'
import {Text} from './text/Text' import {Text} from './text/Text'
import {MagnifyingGlassIcon} from '../../lib/icons' import {MagnifyingGlassIcon} from '../../lib/icons'
import {useStores} from '../../../state' import {useStores} from '../../../state'
import {useTheme} from '../../lib/ThemeContext'
import {usePalette} from '../../lib/hooks/usePalette' import {usePalette} from '../../lib/hooks/usePalette'
import {colors} from '../../lib/styles'
const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10} const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10}
const BACK_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10} const BACK_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10}
@ -26,7 +26,6 @@ export const ViewHeader = observer(function ViewHeader({
subtitle?: string subtitle?: string
canGoBack?: boolean canGoBack?: boolean
}) { }) {
const theme = useTheme()
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores() const store = useStores()
const onPressBack = () => { const onPressBack = () => {
@ -52,12 +51,12 @@ export const ViewHeader = observer(function ViewHeader({
testID="viewHeaderBackOrMenuBtn" testID="viewHeaderBackOrMenuBtn"
onPress={canGoBack ? onPressBack : onPressMenu} onPress={canGoBack ? onPressBack : onPressMenu}
hitSlop={BACK_HITSLOP} hitSlop={BACK_HITSLOP}
style={canGoBack ? styles.backIcon : styles.backIconWide}> style={canGoBack ? styles.backBtn : styles.backBtnWide}>
{canGoBack ? ( {canGoBack ? (
<FontAwesomeIcon <FontAwesomeIcon
size={18} size={18}
icon="angle-left" icon="angle-left"
style={[{marginTop: 6}, pal.text]} style={[styles.backIcon, pal.text]}
/> />
) : ( ) : (
<UserAvatar <UserAvatar
@ -96,13 +95,10 @@ export const ViewHeader = observer(function ViewHeader({
<FontAwesomeIcon icon="signal" style={pal.text} size={16} /> <FontAwesomeIcon icon="signal" style={pal.text} size={16} />
<FontAwesomeIcon <FontAwesomeIcon
icon="x" icon="x"
style={{ style={[
backgroundColor: pal.colors.background, styles.littleXIcon,
color: theme.palette.error.background, {backgroundColor: pal.colors.background},
position: 'absolute', ]}
right: 7,
bottom: 7,
}}
size={8} size={8}
/> />
</> </>
@ -136,15 +132,18 @@ const styles = StyleSheet.create({
fontWeight: 'normal', fontWeight: 'normal',
}, },
backIcon: { backBtn: {
width: 30, width: 30,
height: 30, height: 30,
}, },
backIconWide: { backBtnWide: {
width: 40, width: 40,
height: 30, height: 30,
marginLeft: 6, marginLeft: 6,
}, },
backIcon: {
marginTop: 6,
},
btn: { btn: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
@ -154,4 +153,10 @@ const styles = StyleSheet.create({
borderRadius: 20, borderRadius: 20,
marginLeft: 4, marginLeft: 4,
}, },
littleXIcon: {
color: colors.red3,
position: 'absolute',
right: 7,
bottom: 7,
},
}) })

View File

@ -5,6 +5,7 @@ import {HorzSwipe} from './gestures/HorzSwipe'
import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue' import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue'
import {OnScrollCb} from '../../lib/hooks/useOnMainScroll' import {OnScrollCb} from '../../lib/hooks/useOnMainScroll'
import {clamp} from '../../../lib/numbers' import {clamp} from '../../../lib/numbers'
import {s} from '../../lib/styles'
const HEADER_ITEM = {_reactKey: '__header__'} const HEADER_ITEM = {_reactKey: '__header__'}
const SELECTOR_ITEM = {_reactKey: '__selector__'} const SELECTOR_ITEM = {_reactKey: '__selector__'}
@ -54,7 +55,7 @@ export function ViewSelector({
setSelectedIndex(clamp(index, 0, sections.length)) setSelectedIndex(clamp(index, 0, sections.length))
useEffect(() => { useEffect(() => {
onSelectView?.(selectedIndex) onSelectView?.(selectedIndex)
}, [selectedIndex]) }, [selectedIndex, onSelectView])
// rendering // rendering
// = // =
@ -98,7 +99,7 @@ export function ViewSelector({
onScroll={onScroll} onScroll={onScroll}
onRefresh={onRefresh} onRefresh={onRefresh}
onEndReached={onEndReached} onEndReached={onEndReached}
contentContainerStyle={{paddingBottom: 200}} contentContainerStyle={s.contentContainer}
/> />
</HorzSwipe> </HorzSwipe>
) )

View File

@ -2,6 +2,7 @@ import React, {useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {RadioButton} from './RadioButton' import {RadioButton} from './RadioButton'
import {ButtonType} from './Button' import {ButtonType} from './Button'
import {s} from '../../../lib/styles'
export interface RadioGroupItem { export interface RadioGroupItem {
label: string label: string
@ -29,7 +30,7 @@ export function RadioGroup({
{items.map((item, i) => ( {items.map((item, i) => (
<RadioButton <RadioButton
key={item.key} key={item.key}
style={i !== 0 ? {marginTop: 2} : undefined} style={i !== 0 ? s.mt2 : undefined}
type={type} type={type}
label={item.label} label={item.label}
isSelected={item.key === selection} isSelected={item.key === selection}

View File

@ -9,6 +9,7 @@ import {
View, View,
} from 'react-native' } from 'react-native'
import {clamp} from 'lodash' import {clamp} from 'lodash'
import {s} from '../../../lib/styles'
interface Props { interface Props {
panX: Animated.Value panX: Animated.Value
@ -111,7 +112,9 @@ export function HorzSwipe({
(Math.abs(gestureState.dx) > swipeDistanceThreshold / 4 || (Math.abs(gestureState.dx) > swipeDistanceThreshold / 4 ||
Math.abs(gestureState.vx) > swipeVelocityThreshold) Math.abs(gestureState.vx) > swipeVelocityThreshold)
) { ) {
const final = ((gestureState.dx / Math.abs(gestureState.dx)) * -1) | 0 const final = Math.floor(
(gestureState.dx / Math.abs(gestureState.dx)) * -1,
)
Animated.timing(panX, { Animated.timing(panX, {
toValue: final, toValue: final,
duration: 100, duration: 100,
@ -144,7 +147,7 @@ export function HorzSwipe({
}) })
return ( return (
<View {...panResponder.panHandlers} style={{flex: 1}}> <View {...panResponder.panHandlers} style={s.h100pct}>
{children} {children}
</View> </View>
) )

View File

@ -9,6 +9,7 @@ import {
View, View,
} from 'react-native' } from 'react-native'
import {clamp} from 'lodash' import {clamp} from 'lodash'
import {s} from '../../../lib/styles'
export enum Dir { export enum Dir {
None, None,
@ -294,7 +295,7 @@ export function SwipeAndZoom({
}) })
return ( return (
<View {...panResponder.panHandlers} style={{flex: 1}}> <View {...panResponder.panHandlers} style={s.h100pct}>
{children} {children}
</View> </View>
) )

View File

@ -47,9 +47,9 @@ export function AutoSizedImage({
setImgInfo({width, height}) setImgInfo({width, height})
} }
}, },
(error: any) => { (err: any) => {
if (!aborted) { if (!aborted) {
setError(String(error)) setError(String(err))
} }
}, },
) )

View File

@ -105,7 +105,7 @@ function ImageLayoutGridInner({
<TouchableWithoutFeedback onPress={() => onPress?.(1)}> <TouchableWithoutFeedback onPress={() => onPress?.(1)}>
<Image source={{uri: uris[1]}} style={size1} /> <Image source={{uri: uris[1]}} style={size1} />
</TouchableWithoutFeedback> </TouchableWithoutFeedback>
<View style={{height: 5}} /> <View style={styles.hSpace} />
<TouchableWithoutFeedback onPress={() => onPress?.(2)}> <TouchableWithoutFeedback onPress={() => onPress?.(2)}>
<Image source={{uri: uris[2]}} style={size1} /> <Image source={{uri: uris[2]}} style={size1} />
</TouchableWithoutFeedback> </TouchableWithoutFeedback>

View File

@ -58,6 +58,8 @@ export const gradients = {
export const s = StyleSheet.create({ export const s = StyleSheet.create({
// helpers // helpers
footerSpacer: {height: 100}, footerSpacer: {height: 100},
contentContainer: {paddingBottom: 200},
border1: {borderWidth: 1},
// font weights // font weights
fw600: {fontWeight: '600'}, fw600: {fontWeight: '600'},
@ -140,6 +142,7 @@ export const s = StyleSheet.create({
flexCol: {flexDirection: 'column'}, flexCol: {flexDirection: 'column'},
flex1: {flex: 1}, flex1: {flex: 1},
alignCenter: {alignItems: 'center'}, alignCenter: {alignItems: 'center'},
alignBaseline: {alignItems: 'baseline'},
// position // position
absolute: {position: 'absolute'}, absolute: {position: 'absolute'},

View File

@ -18,7 +18,7 @@ import {Debug} from './screens/Debug'
import {Log} from './screens/Log' import {Log} from './screens/Log'
export type ScreenParams = { export type ScreenParams = {
navIdx: [number, number] navIdx: string
params: Record<string, any> params: Record<string, any>
visible: boolean visible: boolean
scrollElRef?: MutableRefObject<FlatList<any> | undefined> scrollElRef?: MutableRefObject<FlatList<any> | undefined>

View File

@ -17,7 +17,7 @@ export const Contacts = ({navIdx, visible}: ScreenParams) => {
if (visible) { if (visible) {
store.nav.setTitle(navIdx, 'Contacts') store.nav.setTitle(navIdx, 'Contacts')
} }
}, [store, visible]) }, [store, visible, navIdx])
const [searchText, onChangeSearchText] = useState('') const [searchText, onChangeSearchText] = useState('')
const inputRef = useRef<TextInput | null>(null) const inputRef = useRef<TextInput | null>(null)

View File

@ -4,6 +4,7 @@ import {ViewHeader} from '../com/util/ViewHeader'
import {ThemeProvider} from '../lib/ThemeContext' import {ThemeProvider} from '../lib/ThemeContext'
import {PaletteColorName} from '../lib/ThemeContext' import {PaletteColorName} from '../lib/ThemeContext'
import {usePalette} from '../lib/hooks/usePalette' import {usePalette} from '../lib/hooks/usePalette'
import {s} from '../lib/styles'
import {Text} from '../com/util/text/Text' import {Text} from '../com/util/text/Text'
import {ViewSelector} from '../com/util/ViewSelector' import {ViewSelector} from '../com/util/ViewSelector'
@ -48,7 +49,7 @@ function DebugInner({
const renderItem = item => { const renderItem = item => {
return ( return (
<View> <View>
<View style={{paddingTop: 10, paddingHorizontal: 10}}> <View style={[s.pt10, s.pl10, s.pr10]}>
<ToggleButton <ToggleButton
type="default-light" type="default-light"
onPress={onToggleColorScheme} onPress={onToggleColorScheme}
@ -70,7 +71,7 @@ function DebugInner({
const items = [{currentView}] const items = [{currentView}]
return ( return (
<View style={[{flex: 1}, pal.view]}> <View style={[s.h100pct, pal.view]}>
<ViewHeader title="Debug panel" /> <ViewHeader title="Debug panel" />
<ViewSelector <ViewSelector
swipeEnabled swipeEnabled
@ -86,7 +87,7 @@ function DebugInner({
function Heading({label}: {label: string}) { function Heading({label}: {label: string}) {
const pal = usePalette('default') const pal = usePalette('default')
return ( return (
<View style={{paddingTop: 10, paddingBottom: 5}}> <View style={[s.pt10, s.pb5]}>
<Text type="title-lg" style={pal.text}> <Text type="title-lg" style={pal.text}>
{label} {label}
</Text> </Text>
@ -96,7 +97,7 @@ function Heading({label}: {label: string}) {
function BaseView() { function BaseView() {
return ( return (
<View style={{paddingHorizontal: 10}}> <View style={[s.pl10, s.pr10]}>
<Heading label="Typography" /> <Heading label="Typography" />
<TypographyView /> <TypographyView />
<Heading label="Palettes" /> <Heading label="Palettes" />
@ -109,14 +110,14 @@ function BaseView() {
<EmptyStateView /> <EmptyStateView />
<Heading label="Loading placeholders" /> <Heading label="Loading placeholders" />
<LoadingPlaceholderView /> <LoadingPlaceholderView />
<View style={{height: 200}} /> <View style={s.footerSpacer} />
</View> </View>
) )
} }
function ControlsView() { function ControlsView() {
return ( return (
<ScrollView style={{paddingHorizontal: 10}}> <ScrollView style={[s.pl10, s.pr10]}>
<Heading label="Buttons" /> <Heading label="Buttons" />
<ButtonsView /> <ButtonsView />
<Heading label="Dropdown Buttons" /> <Heading label="Dropdown Buttons" />
@ -125,15 +126,15 @@ function ControlsView() {
<ToggleButtonsView /> <ToggleButtonsView />
<Heading label="Radio Buttons" /> <Heading label="Radio Buttons" />
<RadioButtonsView /> <RadioButtonsView />
<View style={{height: 200}} /> <View style={s.footerSpacer} />
</ScrollView> </ScrollView>
) )
} }
function ErrorView() { function ErrorView() {
return ( return (
<View style={{padding: 10}}> <View style={s.p10}>
<View style={{marginBottom: 5}}> <View style={s.mb5}>
<ErrorScreen <ErrorScreen
title="Error screen" title="Error screen"
message="A major error occurred that led the entire screen to fail" message="A major error occurred that led the entire screen to fail"
@ -141,22 +142,22 @@ function ErrorView() {
onPressTryAgain={() => {}} onPressTryAgain={() => {}}
/> />
</View> </View>
<View style={{marginBottom: 5}}> <View style={s.mb5}>
<ErrorMessage message="This is an error that occurred while things were being done" /> <ErrorMessage message="This is an error that occurred while things were being done" />
</View> </View>
<View style={{marginBottom: 5}}> <View style={s.mb5}>
<ErrorMessage <ErrorMessage
message="This is an error that occurred while things were being done" message="This is an error that occurred while things were being done"
numberOfLines={1} numberOfLines={1}
/> />
</View> </View>
<View style={{marginBottom: 5}}> <View style={s.mb5}>
<ErrorMessage <ErrorMessage
message="This is an error that occurred while things were being done" message="This is an error that occurred while things were being done"
onPressTryAgain={() => {}} onPressTryAgain={() => {}}
/> />
</View> </View>
<View style={{marginBottom: 5}}> <View style={s.mb5}>
<ErrorMessage <ErrorMessage
message="This is an error that occurred while things were being done" message="This is an error that occurred while things were being done"
onPressTryAgain={() => {}} onPressTryAgain={() => {}}
@ -171,16 +172,7 @@ function PaletteView({palette}: {palette: PaletteColorName}) {
const defaultPal = usePalette('default') const defaultPal = usePalette('default')
const pal = usePalette(palette) const pal = usePalette(palette)
return ( return (
<View <View style={[pal.view, pal.border, s.p10, s.mb5, s.border1]}>
style={[
pal.view,
pal.border,
{
borderWidth: 1,
padding: 10,
marginBottom: 5,
},
]}>
<Text style={[pal.text]}>{palette} colors</Text> <Text style={[pal.text]}>{palette} colors</Text>
<Text style={[pal.textLight]}>Light text</Text> <Text style={[pal.textLight]}>Light text</Text>
<Text style={[pal.link]}>Link text</Text> <Text style={[pal.link]}>Link text</Text>
@ -197,21 +189,6 @@ function TypographyView() {
const pal = usePalette('default') const pal = usePalette('default')
return ( return (
<View style={[pal.view]}> <View style={[pal.view]}>
<Text type="xxl-thin" style={[pal.text]}>
'xxl-thin' lorem ipsum dolor
</Text>
<Text type="xxl" style={[pal.text]}>
'xxl' lorem ipsum dolor
</Text>
<Text type="xxl-medium" style={[pal.text]}>
'xxl-medium' lorem ipsum dolor
</Text>
<Text type="xxl-bold" style={[pal.text]}>
'xxl-bold' lorem ipsum dolor
</Text>
<Text type="xxl-heavy" style={[pal.text]}>
'xxl-heavy' lorem ipsum dolor
</Text>
<Text type="xl-thin" style={[pal.text]}> <Text type="xl-thin" style={[pal.text]}>
'xl-thin' lorem ipsum dolor 'xl-thin' lorem ipsum dolor
</Text> </Text>
@ -300,9 +277,6 @@ function TypographyView() {
<Text type="button" style={[pal.text]}> <Text type="button" style={[pal.text]}>
Button Button
</Text> </Text>
<Text type="overline" style={[pal.text]}>
Overline
</Text>
</View> </View>
) )
} }
@ -325,16 +299,12 @@ function ButtonsView() {
const buttonStyles = {marginRight: 5} const buttonStyles = {marginRight: 5}
return ( return (
<View style={[defaultPal.view]}> <View style={[defaultPal.view]}>
<View <View style={[s.flexRow, s.mb5]}>
style={{
flexDirection: 'row',
marginBottom: 5,
}}>
<Button type="primary" label="Primary solid" style={buttonStyles} /> <Button type="primary" label="Primary solid" style={buttonStyles} />
<Button type="secondary" label="Secondary solid" style={buttonStyles} /> <Button type="secondary" label="Secondary solid" style={buttonStyles} />
<Button type="inverted" label="Inverted solid" style={buttonStyles} /> <Button type="inverted" label="Inverted solid" style={buttonStyles} />
</View> </View>
<View style={{flexDirection: 'row'}}> <View style={s.flexRow}>
<Button <Button
type="primary-outline" type="primary-outline"
label="Primary outline" label="Primary outline"
@ -346,7 +316,7 @@ function ButtonsView() {
style={buttonStyles} style={buttonStyles}
/> />
</View> </View>
<View style={{flexDirection: 'row'}}> <View style={s.flexRow}>
<Button <Button
type="primary-light" type="primary-light"
label="Primary light" label="Primary light"
@ -358,7 +328,7 @@ function ButtonsView() {
style={buttonStyles} style={buttonStyles}
/> />
</View> </View>
<View style={{flexDirection: 'row'}}> <View style={s.flexRow}>
<Button <Button
type="default-light" type="default-light"
label="Default light" label="Default light"
@ -390,10 +360,7 @@ function DropdownButtonsView() {
const defaultPal = usePalette('default') const defaultPal = usePalette('default')
return ( return (
<View style={[defaultPal.view]}> <View style={[defaultPal.view]}>
<View <View style={s.mb5}>
style={{
marginBottom: 5,
}}>
<DropdownButton <DropdownButton
type="primary" type="primary"
items={DROPDOWN_ITEMS} items={DROPDOWN_ITEMS}
@ -401,10 +368,7 @@ function DropdownButtonsView() {
label="Primary button" label="Primary button"
/> />
</View> </View>
<View <View style={s.mb5}>
style={{
marginBottom: 5,
}}>
<DropdownButton type="bare" items={DROPDOWN_ITEMS} menuWidth={200}> <DropdownButton type="bare" items={DROPDOWN_ITEMS} menuWidth={200}>
<Text>Bare</Text> <Text>Bare</Text>
</DropdownButton> </DropdownButton>
@ -415,7 +379,7 @@ function DropdownButtonsView() {
function ToggleButtonsView() { function ToggleButtonsView() {
const defaultPal = usePalette('default') const defaultPal = usePalette('default')
const buttonStyles = {marginBottom: 5} const buttonStyles = s.mb5
const [isSelected, setIsSelected] = React.useState(false) const [isSelected, setIsSelected] = React.useState(false)
const onToggle = () => setIsSelected(!isSelected) const onToggle = () => setIsSelected(!isSelected)
return ( return (

View File

@ -83,14 +83,14 @@ export const Home = observer(function Home({
} }
return ( return (
<View style={s.flex1}> <View style={s.h100pct}>
<ViewHeader title="Bluesky" subtitle="Private Beta" canGoBack={false} /> <ViewHeader title="Bluesky" subtitle="Private Beta" canGoBack={false} />
<Feed <Feed
testID="homeFeed" testID="homeFeed"
key="default" key="default"
feed={store.me.mainFeed} feed={store.me.mainFeed}
scrollElRef={scrollElRef} scrollElRef={scrollElRef}
style={{flex: 1}} style={s.h100pct}
onPressCompose={onPressCompose} onPressCompose={onPressCompose}
onPressTryAgain={onPressTryAgain} onPressTryAgain={onPressTryAgain}
onScroll={onMainScroll} onScroll={onMainScroll}
@ -99,9 +99,9 @@ export const Home = observer(function Home({
<TouchableOpacity <TouchableOpacity
style={[ style={[
styles.loadLatest, styles.loadLatest,
store.shell.minimalShellMode !store.shell.minimalShellMode && {
? {bottom: 35} bottom: 60 + clamp(safeAreaInsets.bottom, 15, 30),
: {bottom: 60 + clamp(safeAreaInsets.bottom, 15, 30)}, },
]} ]}
onPress={onPressLoadLatest} onPress={onPressLoadLatest}
hitSlop={HITSLOP}> hitSlop={HITSLOP}>
@ -125,6 +125,7 @@ const styles = StyleSheet.create({
loadLatest: { loadLatest: {
position: 'absolute', position: 'absolute',
left: 20, left: 20,
bottom: 35,
shadowColor: '#000', shadowColor: '#000',
shadowOpacity: 0.3, shadowOpacity: 0.3,
shadowOffset: {width: 0, height: 1}, shadowOffset: {width: 0, height: 1},

View File

@ -21,7 +21,7 @@ export const Log = observer(function Log({navIdx, visible}: ScreenParams) {
} }
store.shell.setMinimalShellMode(false) store.shell.setMinimalShellMode(false)
store.nav.setTitle(navIdx, 'Log') store.nav.setTitle(navIdx, 'Log')
}, [visible, store]) }, [visible, store, navIdx])
const toggler = (id: string) => () => { const toggler = (id: string) => () => {
if (expanded.includes(id)) { if (expanded.includes(id)) {
@ -52,7 +52,7 @@ export const Log = observer(function Log({navIdx, visible}: ScreenParams) {
<Text type="sm" style={[styles.summary, pal.text]}> <Text type="sm" style={[styles.summary, pal.text]}>
{entry.summary} {entry.summary}
</Text> </Text>
{!!entry.details ? ( {entry.details ? (
<FontAwesomeIcon <FontAwesomeIcon
icon={ icon={
expanded.includes(entry.id) ? 'angle-up' : 'angle-down' expanded.includes(entry.id) ? 'angle-up' : 'angle-down'

View File

@ -18,9 +18,9 @@ import {s, colors} from '../lib/styles'
import {usePalette} from '../lib/hooks/usePalette' import {usePalette} from '../lib/hooks/usePalette'
enum ScreenState { enum ScreenState {
SigninOrCreateAccount, S_SigninOrCreateAccount,
Signin, S_Signin,
CreateAccount, S_CreateAccount,
} }
const SigninOrCreateAccount = ({ const SigninOrCreateAccount = ({
@ -78,58 +78,56 @@ const SigninOrCreateAccount = ({
) )
} }
export const Login = observer( export const Login = observer(() => {
(/*{navigation}: RootTabsScreenProps<'Login'>*/) => { const pal = usePalette('default')
const pal = usePalette('default') const [screenState, setScreenState] = useState<ScreenState>(
const [screenState, setScreenState] = useState<ScreenState>( ScreenState.S_SigninOrCreateAccount,
ScreenState.SigninOrCreateAccount, )
)
if (screenState === ScreenState.SigninOrCreateAccount) {
return (
<LinearGradient
colors={['#007CFF', '#00BCFF']}
start={{x: 0, y: 0.8}}
end={{x: 0, y: 1}}
style={styles.container}>
<SafeAreaView testID="noSessionView" style={styles.container}>
<ErrorBoundary>
<SigninOrCreateAccount
onPressSignin={() => setScreenState(ScreenState.Signin)}
onPressCreateAccount={() =>
setScreenState(ScreenState.CreateAccount)
}
/>
</ErrorBoundary>
</SafeAreaView>
</LinearGradient>
)
}
if (screenState === ScreenState.S_SigninOrCreateAccount) {
return ( return (
<View style={[styles.container, pal.view]}> <LinearGradient
colors={['#007CFF', '#00BCFF']}
start={{x: 0, y: 0.8}}
end={{x: 0, y: 1}}
style={styles.container}>
<SafeAreaView testID="noSessionView" style={styles.container}> <SafeAreaView testID="noSessionView" style={styles.container}>
<ErrorBoundary> <ErrorBoundary>
{screenState === ScreenState.Signin ? ( <SigninOrCreateAccount
<Signin onPressSignin={() => setScreenState(ScreenState.S_Signin)}
onPressBack={() => onPressCreateAccount={() =>
setScreenState(ScreenState.SigninOrCreateAccount) setScreenState(ScreenState.S_CreateAccount)
} }
/> />
) : undefined}
{screenState === ScreenState.CreateAccount ? (
<CreateAccount
onPressBack={() =>
setScreenState(ScreenState.SigninOrCreateAccount)
}
/>
) : undefined}
</ErrorBoundary> </ErrorBoundary>
</SafeAreaView> </SafeAreaView>
</View> </LinearGradient>
) )
}, }
)
return (
<View style={[styles.container, pal.view]}>
<SafeAreaView testID="noSessionView" style={styles.container}>
<ErrorBoundary>
{screenState === ScreenState.S_Signin ? (
<Signin
onPressBack={() =>
setScreenState(ScreenState.S_SigninOrCreateAccount)
}
/>
) : undefined}
{screenState === ScreenState.S_CreateAccount ? (
<CreateAccount
onPressBack={() =>
setScreenState(ScreenState.S_SigninOrCreateAccount)
}
/>
) : undefined}
</ErrorBoundary>
</SafeAreaView>
</View>
)
})
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {

View File

@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import {Button, View} from 'react-native' import {Button, StyleSheet, View} from 'react-native'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {Text} from '../com/util/text/Text' import {Text} from '../com/util/text/Text'
import {useStores} from '../../state' import {useStores} from '../../state'
@ -9,13 +9,8 @@ export const NotFound = () => {
return ( return (
<View testID="notFoundView"> <View testID="notFoundView">
<ViewHeader title="Page not found" /> <ViewHeader title="Page not found" />
<View <View style={styles.container}>
style={{ <Text style={styles.title}>Page not found</Text>
justifyContent: 'center',
alignItems: 'center',
paddingTop: 100,
}}>
<Text style={{fontSize: 40, fontWeight: 'bold'}}>Page not found</Text>
<Button <Button
testID="navigateHomeButton" testID="navigateHomeButton"
title="Home" title="Home"
@ -25,3 +20,15 @@ export const NotFound = () => {
</View> </View>
) )
} }
const styles = StyleSheet.create({
container: {
justifyContent: 'center',
alignItems: 'center',
paddingTop: 100,
},
title: {
fontSize: 40,
fontWeight: 'bold',
},
})

View File

@ -5,6 +5,7 @@ import {Feed} from '../com/notifications/Feed'
import {useStores} from '../../state' import {useStores} from '../../state'
import {ScreenParams} from '../routes' import {ScreenParams} from '../routes'
import {useOnMainScroll} from '../lib/hooks/useOnMainScroll' import {useOnMainScroll} from '../lib/hooks/useOnMainScroll'
import {s} from '../lib/styles'
export const Notifications = ({navIdx, visible}: ScreenParams) => { export const Notifications = ({navIdx, visible}: ScreenParams) => {
const store = useStores() const store = useStores()
@ -24,14 +25,14 @@ export const Notifications = ({navIdx, visible}: ScreenParams) => {
store.me.notifications.updateReadState() store.me.notifications.updateReadState()
}) })
store.nav.setTitle(navIdx, 'Notifications') store.nav.setTitle(navIdx, 'Notifications')
}, [visible, store]) }, [visible, store, navIdx])
const onPressTryAgain = () => { const onPressTryAgain = () => {
store.me.notifications.refresh() store.me.notifications.refresh()
} }
return ( return (
<View style={{flex: 1}}> <View style={s.h100pct}>
<ViewHeader title="Notifications" canGoBack={false} /> <ViewHeader title="Notifications" canGoBack={false} />
<Feed <Feed
view={store.me.notifications} view={store.me.notifications}

View File

@ -1,5 +1,5 @@
import React, {useEffect} from 'react' import React, {useEffect} from 'react'
import {View} from 'react-native' import {StyleSheet, View} from 'react-native'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {FeatureExplainer} from '../com/onboard/FeatureExplainer' import {FeatureExplainer} from '../com/onboard/FeatureExplainer'
import {Follows} from '../com/onboard/Follows' import {Follows} from '../com/onboard/Follows'
@ -14,7 +14,7 @@ export const Onboard = observer(() => {
if (!OnboardStageOrder.includes(store.onboard.stage)) { if (!OnboardStageOrder.includes(store.onboard.stage)) {
store.onboard.stop() store.onboard.stop()
} }
}, [store.onboard.stage]) }, [store.onboard])
let Com let Com
if (store.onboard.stage === OnboardStage.Explainers) { if (store.onboard.stage === OnboardStage.Explainers) {
@ -26,8 +26,15 @@ export const Onboard = observer(() => {
} }
return ( return (
<View style={{flex: 1, backgroundColor: '#fff'}}> <View style={styles.container}>
<Com /> <Com />
</View> </View>
) )
}) })
const styles = StyleSheet.create({
container: {
height: '100%',
backgroundColor: '#fff',
},
})

View File

@ -16,7 +16,7 @@ export const PostDownvotedBy = ({navIdx, visible, params}: ScreenParams) => {
store.nav.setTitle(navIdx, 'Downvoted by') store.nav.setTitle(navIdx, 'Downvoted by')
store.shell.setMinimalShellMode(false) store.shell.setMinimalShellMode(false)
} }
}, [store, visible]) }, [store, visible, navIdx])
return ( return (
<View> <View>

View File

@ -16,7 +16,7 @@ export const PostRepostedBy = ({navIdx, visible, params}: ScreenParams) => {
store.nav.setTitle(navIdx, 'Reposted by') store.nav.setTitle(navIdx, 'Reposted by')
store.shell.setMinimalShellMode(false) store.shell.setMinimalShellMode(false)
} }
}, [store, visible]) }, [store, visible, navIdx])
return ( return (
<View> <View>

View File

@ -6,6 +6,7 @@ import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread'
import {PostThreadViewModel} from '../../state/models/post-thread-view' import {PostThreadViewModel} from '../../state/models/post-thread-view'
import {ScreenParams} from '../routes' import {ScreenParams} from '../routes'
import {useStores} from '../../state' import {useStores} from '../../state'
import {s} from '../lib/styles'
export const PostThread = ({navIdx, visible, params}: ScreenParams) => { export const PostThread = ({navIdx, visible, params}: ScreenParams) => {
const store = useStores() const store = useStores()
@ -14,18 +15,18 @@ export const PostThread = ({navIdx, visible, params}: ScreenParams) => {
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
const view = useMemo<PostThreadViewModel>( const view = useMemo<PostThreadViewModel>(
() => new PostThreadViewModel(store, {uri}), () => new PostThreadViewModel(store, {uri}),
[uri], [store, uri],
) )
const setTitle = () => {
const author = view.thread?.author
const niceName = author?.handle || name
setViewSubtitle(`by ${niceName}`)
store.nav.setTitle(navIdx, `Post by ${niceName}`)
}
useEffect(() => { useEffect(() => {
let aborted = false let aborted = false
const threadCleanup = view.registerListeners() const threadCleanup = view.registerListeners()
const setTitle = () => {
const author = view.thread?.post.author
const niceName = author?.handle || name
setViewSubtitle(`by ${niceName}`)
store.nav.setTitle(navIdx, `Post by ${niceName}`)
}
if (!visible) { if (!visible) {
return threadCleanup return threadCleanup
} }
@ -47,12 +48,12 @@ export const PostThread = ({navIdx, visible, params}: ScreenParams) => {
aborted = true aborted = true
threadCleanup() threadCleanup()
} }
}, [visible, store.nav, store.log, name]) }, [visible, store.nav, store.log, store.shell, name, navIdx, view])
return ( return (
<View style={{flex: 1}}> <View style={s.h100pct}>
<ViewHeader title="Post" subtitle={viewSubtitle} /> <ViewHeader title="Post" subtitle={viewSubtitle} />
<View style={{flex: 1}}> <View style={s.h100pct}>
<PostThreadComponent uri={uri} view={view} /> <PostThreadComponent uri={uri} view={view} />
</View> </View>
</View> </View>

View File

@ -15,7 +15,7 @@ export const PostUpvotedBy = ({navIdx, visible, params}: ScreenParams) => {
if (visible) { if (visible) {
store.nav.setTitle(navIdx, 'Liked by') store.nav.setTitle(navIdx, 'Liked by')
} }
}, [store, visible]) }, [store, visible, navIdx])
return ( return (
<View> <View>

View File

@ -26,9 +26,13 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
const [hasSetup, setHasSetup] = useState<boolean>(false) const [hasSetup, setHasSetup] = useState<boolean>(false)
const uiState = React.useMemo( const uiState = React.useMemo(
() => new ProfileUiModel(store, {user: params.name}), () => new ProfileUiModel(store, {user: params.name}),
[params.user], [params.name, store],
) )
useEffect(() => {
store.nav.setTitle(navIdx, params.name)
}, [store, navIdx, params.name])
useEffect(() => { useEffect(() => {
let aborted = false let aborted = false
const feedCleanup = uiState.feed.registerListeners() const feedCleanup = uiState.feed.registerListeners()
@ -38,7 +42,6 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
if (hasSetup) { if (hasSetup) {
uiState.update() uiState.update()
} else { } else {
store.nav.setTitle(navIdx, params.name)
uiState.setup().then(() => { uiState.setup().then(() => {
if (aborted) { if (aborted) {
return return
@ -50,7 +53,7 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
aborted = true aborted = true
feedCleanup() feedCleanup()
} }
}, [visible, params.name, store]) }, [visible, store, hasSetup, uiState])
// events // events
// = // =
@ -139,7 +142,7 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
<EmptyState <EmptyState
icon={['far', 'message']} icon={['far', 'message']}
message="No posts yet!" message="No posts yet!"
style={{paddingVertical: 40}} style={styles.emptyState}
/> />
) )
} }
@ -187,7 +190,7 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
function LoadingMoreFooter() { function LoadingMoreFooter() {
return ( return (
<View style={{paddingVertical: 20}}> <View style={styles.loadingMoreFooter}>
<ActivityIndicator /> <ActivityIndicator />
</View> </View>
) )
@ -202,6 +205,12 @@ const styles = StyleSheet.create({
paddingVertical: 10, paddingVertical: 10,
paddingHorizontal: 14, paddingHorizontal: 14,
}, },
emptyState: {
paddingVertical: 40,
},
loadingMoreFooter: {
paddingVertical: 20,
},
endItem: { endItem: {
paddingTop: 20, paddingTop: 20,
paddingBottom: 30, paddingBottom: 30,

View File

@ -14,7 +14,7 @@ export const ProfileFollowers = ({navIdx, visible, params}: ScreenParams) => {
store.nav.setTitle(navIdx, `Followers of ${name}`) store.nav.setTitle(navIdx, `Followers of ${name}`)
store.shell.setMinimalShellMode(false) store.shell.setMinimalShellMode(false)
} }
}, [store, visible, name]) }, [store, visible, name, navIdx])
return ( return (
<View> <View>

View File

@ -14,7 +14,7 @@ export const ProfileFollows = ({navIdx, visible, params}: ScreenParams) => {
store.nav.setTitle(navIdx, `Followed by ${name}`) store.nav.setTitle(navIdx, `Followed by ${name}`)
store.shell.setMinimalShellMode(false) store.shell.setMinimalShellMode(false)
} }
}, [store, visible, name]) }, [store, visible, name, navIdx])
return ( return (
<View> <View>

View File

@ -25,7 +25,7 @@ export const Search = ({navIdx, visible, params}: ScreenParams) => {
const [query, setQuery] = useState<string>('') const [query, setQuery] = useState<string>('')
const autocompleteView = useMemo<UserAutocompleteViewModel>( const autocompleteView = useMemo<UserAutocompleteViewModel>(
() => new UserAutocompleteViewModel(store), () => new UserAutocompleteViewModel(store),
[], [store],
) )
const {name} = params const {name} = params
@ -35,7 +35,7 @@ export const Search = ({navIdx, visible, params}: ScreenParams) => {
autocompleteView.setup() autocompleteView.setup()
store.nav.setTitle(navIdx, 'Search') store.nav.setTitle(navIdx, 'Search')
} }
}, [store, visible, name]) }, [store, visible, name, navIdx, autocompleteView])
const onChangeQuery = (text: string) => { const onChangeQuery = (text: string) => {
setQuery(text) setQuery(text)

View File

@ -33,7 +33,7 @@ export const Settings = observer(function Settings({
} }
store.shell.setMinimalShellMode(false) store.shell.setMinimalShellMode(false)
store.nav.setTitle(navIdx, 'Settings') store.nav.setTitle(navIdx, 'Settings')
}, [visible, store]) }, [visible, store, navIdx])
const onPressSwitchAccount = async (acct: AccountData) => { const onPressSwitchAccount = async (acct: AccountData) => {
setIsSwitching(true) setIsSwitching(true)
@ -130,8 +130,8 @@ export const Settings = observer(function Settings({
style={[ style={[
pal.view, pal.view,
styles.profile, styles.profile,
styles.alignCenter,
s.mb2, s.mb2,
{alignItems: 'center'},
isSwitching && styles.dimmed, isSwitching && styles.dimmed,
]} ]}
onPress={isSwitching ? undefined : onPressAddAccount}> onPress={isSwitching ? undefined : onPressAddAccount}>
@ -142,7 +142,7 @@ export const Settings = observer(function Settings({
</Text> </Text>
</View> </View>
</TouchableOpacity> </TouchableOpacity>
<View style={{height: 50}} /> <View style={styles.spacer} />
<Text type="sm-medium" style={[s.mb5]}> <Text type="sm-medium" style={[s.mb5]}>
Developer tools Developer tools
</Text> </Text>
@ -168,6 +168,12 @@ const styles = StyleSheet.create({
dimmed: { dimmed: {
opacity: 0.5, opacity: 0.5,
}, },
spacer: {
height: 50,
},
alignCenter: {
alignItems: 'center',
},
title: { title: {
fontSize: 32, fontSize: 32,
fontWeight: 'bold', fontWeight: 'bold',

View File

@ -23,163 +23,157 @@ import {Text} from '../../com/util/text/Text'
import {ToggleButton} from '../../com/util/forms/ToggleButton' import {ToggleButton} from '../../com/util/forms/ToggleButton'
import {usePalette} from '../../lib/hooks/usePalette' import {usePalette} from '../../lib/hooks/usePalette'
export const Menu = observer( export const Menu = observer(({onClose}: {onClose: () => void}) => {
({visible, onClose}: {visible: boolean; onClose: () => void}) => { const pal = usePalette('default')
const pal = usePalette('default') const store = useStores()
const store = useStores()
// events // events
// = // =
const onNavigate = (url: string) => { const onNavigate = (url: string) => {
onClose() onClose()
if (url === '/notifications') { if (url === '/notifications') {
store.nav.switchTo(1, true) store.nav.switchTo(1, true)
} else { } else {
store.nav.switchTo(0, true) store.nav.switchTo(0, true)
if (url !== '/') { if (url !== '/') {
store.nav.navigate(url) store.nav.navigate(url)
}
} }
} }
}
// rendering // rendering
// = // =
const MenuItem = ({ const MenuItem = ({
icon, icon,
label, label,
count, count,
url, url,
bold, bold,
onPress, onPress,
}: { }: {
icon: JSX.Element icon: JSX.Element
label: string label: string
count?: number count?: number
url?: string url?: string
bold?: boolean bold?: boolean
onPress?: () => void onPress?: () => void
}) => ( }) => (
<TouchableOpacity
testID={`menuItemButton-${label}`}
style={styles.menuItem}
onPress={onPress ? onPress : () => onNavigate(url || '/')}>
<View style={[styles.menuItemIconWrapper]}>
{icon}
{count ? (
<View style={styles.menuItemCount}>
<Text style={styles.menuItemCountLabel}>{count}</Text>
</View>
) : undefined}
</View>
<Text
type="title"
style={[
pal.text,
bold ? styles.menuItemLabelBold : styles.menuItemLabel,
]}
numberOfLines={1}>
{label}
</Text>
</TouchableOpacity>
)
return (
<ScrollView testID="menuView" style={[styles.view, pal.view]}>
<TouchableOpacity <TouchableOpacity
testID={`menuItemButton-${label}`} testID="profileCardButton"
style={styles.menuItem} onPress={() => onNavigate(`/profile/${store.me.handle}`)}
onPress={onPress ? onPress : () => onNavigate(url || '/')}> style={styles.profileCard}>
<View style={[styles.menuItemIconWrapper]}> <UserAvatar
{icon} size={60}
{count ? ( displayName={store.me.displayName}
<View style={styles.menuItemCount}> handle={store.me.handle}
<Text style={styles.menuItemCountLabel}>{count}</Text> avatar={store.me.avatar}
</View> />
) : undefined} <View style={s.flex1}>
<Text
type="title-lg"
style={[pal.text, styles.profileCardDisplayName]}
numberOfLines={1}>
{store.me.displayName || store.me.handle}
</Text>
<Text
style={[pal.textLight, styles.profileCardHandle]}
numberOfLines={1}>
@{store.me.handle}
</Text>
</View> </View>
<Text </TouchableOpacity>
type="title" <TouchableOpacity
style={[ testID="searchBtn"
pal.text, style={[styles.searchBtn, pal.btn]}
bold ? styles.menuItemLabelBold : styles.menuItemLabel, onPress={() => onNavigate('/search')}>
]} <MagnifyingGlassIcon
numberOfLines={1}> style={pal.text as StyleProp<ViewStyle>}
{label} size={25}
/>
<Text type="title" style={[pal.text, styles.searchBtnLabel]}>
Search
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
) <View style={[styles.section, pal.border, s.pt5]}>
<MenuItem
return ( icon={<HomeIcon style={pal.text as StyleProp<ViewStyle>} size="26" />}
<ScrollView testID="menuView" style={[styles.view, pal.view]}> label="Home"
<TouchableOpacity url="/"
testID="profileCardButton" />
onPress={() => onNavigate(`/profile/${store.me.handle}`)} <MenuItem
style={styles.profileCard}> icon={<BellIcon style={pal.text as StyleProp<ViewStyle>} size="28" />}
<UserAvatar label="Notifications"
size={60} url="/notifications"
displayName={store.me.displayName} count={store.me.notificationCount}
handle={store.me.handle} />
avatar={store.me.avatar} <MenuItem
/> icon={
<View style={s.flex1}> <UserIcon
<Text style={pal.text as StyleProp<ViewStyle>}
type="title-lg" size="30"
style={[pal.text, styles.profileCardDisplayName]} strokeWidth={2}
numberOfLines={1}> />
{store.me.displayName || store.me.handle} }
</Text> label="Profile"
<Text url={`/profile/${store.me.handle}`}
style={[pal.textLight, styles.profileCardHandle]} />
numberOfLines={1}> <MenuItem
@{store.me.handle} icon={
</Text> <CogIcon
</View> style={pal.text as StyleProp<ViewStyle>}
</TouchableOpacity> size="30"
<TouchableOpacity strokeWidth={2}
testID="searchBtn" />
style={[styles.searchBtn, pal.btn]} }
onPress={() => onNavigate('/search')}> label="Settings"
<MagnifyingGlassIcon url="/settings"
style={pal.text as StyleProp<ViewStyle>} />
size={25} </View>
/> <View style={[styles.section, pal.border]}>
<Text type="title" style={[pal.text, styles.searchBtnLabel]}> <ToggleButton
Search label="Dark mode"
</Text> isSelected={store.shell.darkMode}
</TouchableOpacity> onPress={() => store.shell.setDarkMode(!store.shell.darkMode)}
<View style={[styles.section, pal.border, {paddingTop: 5}]}> />
<MenuItem </View>
icon={ <View style={styles.footer}>
<HomeIcon style={pal.text as StyleProp<ViewStyle>} size="26" /> <Text style={[pal.textLight]}>
} Build version {VersionNumber.appVersion} ({VersionNumber.buildVersion}
label="Home" )
url="/" </Text>
/> </View>
<MenuItem <View style={s.footerSpacer} />
icon={ </ScrollView>
<BellIcon style={pal.text as StyleProp<ViewStyle>} size="28" /> )
} })
label="Notifications"
url="/notifications"
count={store.me.notificationCount}
/>
<MenuItem
icon={
<UserIcon
style={pal.text as StyleProp<ViewStyle>}
size="30"
strokeWidth={2}
/>
}
label="Profile"
url={`/profile/${store.me.handle}`}
/>
<MenuItem
icon={
<CogIcon
style={pal.text as StyleProp<ViewStyle>}
size="30"
strokeWidth={2}
/>
}
label="Settings"
url="/settings"
/>
</View>
<View style={[styles.section, pal.border]}>
<ToggleButton
label="Dark mode"
isSelected={store.shell.darkMode}
onPress={() => store.shell.setDarkMode(!store.shell.darkMode)}
/>
</View>
<View style={styles.footer}>
<Text style={[pal.textLight]}>
Build version {VersionNumber.appVersion} (
{VersionNumber.buildVersion})
</Text>
</View>
<View style={s.footerSpacer} />
</ScrollView>
)
},
)
const styles = StyleSheet.create({ const styles = StyleSheet.create({
view: { view: {

View File

@ -32,7 +32,7 @@ import {Text} from '../../com/util/text/Text'
import {ErrorBoundary} from '../../com/util/ErrorBoundary' import {ErrorBoundary} from '../../com/util/ErrorBoundary'
import {TabsSelector} from './TabsSelector' import {TabsSelector} from './TabsSelector'
import {Composer} from './Composer' import {Composer} from './Composer'
import {colors} from '../../lib/styles' import {s, colors} from '../../lib/styles'
import {clamp} from '../../../lib/numbers' import {clamp} from '../../../lib/numbers'
import { import {
GridIcon, GridIcon,
@ -385,7 +385,7 @@ export const MobileShell: React.FC = observer(() => {
/> />
<Animated.View <Animated.View
style={[ style={[
{height: '100%'}, s.h100pct,
screenBg, screenBg,
current current
? [ ? [
@ -486,7 +486,7 @@ export const MobileShell: React.FC = observer(() => {
*/ */
type ScreenRenderDesc = MatchResult & { type ScreenRenderDesc = MatchResult & {
key: string key: string
navIdx: [number, number] navIdx: string
current: boolean current: boolean
previous: boolean previous: boolean
isNewTab: boolean isNewTab: boolean
@ -514,7 +514,7 @@ function constructScreenRenderDesc(nav: NavigationModel): {
hasNewTab = hasNewTab || tab.isNewTab hasNewTab = hasNewTab || tab.isNewTab
return Object.assign(matchRes, { return Object.assign(matchRes, {
key: `t${tab.id}-s${screen.index}`, key: `t${tab.id}-s${screen.index}`,
navIdx: [tab.id, screen.id], navIdx: `${tab.id}-${screen.id}`,
current: isCurrent, current: isCurrent,
previous: isPrevious, previous: isPrevious,
isNewTab: tab.isNewTab, isNewTab: tab.isNewTab,