An ATproto social media client -- with an independent Appview.

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

authored by

Paul Frazee and committed by
GitHub
f36c9565 3a90114f

+478 -482
+4
.github/workflows/lint.yml
··· 25 25 name: Run tests 26 26 runs-on: ubuntu-latest 27 27 steps: 28 + - name: Install node 18 29 + uses: actions/setup-node@v3 30 + with: 31 + node-version: 18 28 32 - name: Check out Git repository 29 33 uses: actions/checkout@v2 30 34 - name: Yarn install
+2 -2
__tests__/state/models/navigation.test.ts
··· 6 6 7 7 beforeEach(() => { 8 8 model = new NavigationModel() 9 - model.setTitle([0, 0], 'title') 9 + model.setTitle('0-0', 'title') 10 10 }) 11 11 12 12 afterAll(() => { ··· 44 44 }) 45 45 46 46 it('should call the isCurrentScreen method', () => { 47 - expect(model.isCurrentScreen(11, 0)).toEqual(false) 47 + expect(model.isCurrentScreen('11', 0)).toEqual(false) 48 48 }) 49 49 50 50 it('should call the tab getter', () => {
-1
jest/test-pds.ts
··· 41 41 yield new Date(start).toISOString() 42 42 start += 1e3 43 43 } 44 - return '' 45 44 } 46 45 47 46 export async function createServer(): Promise<TestPDS> {
-2
metro.config.js
··· 4 4 * 5 5 * @format 6 6 */ 7 - const metroResolver = require('metro-resolver') 8 - const path = require('path') 9 7 10 8 module.exports = { 11 9 transformer: {
+2 -1
src/App.native.tsx
··· 10 10 import * as view from './view/index' 11 11 import {RootStoreModel, setupState, RootStoreProvider} from './state' 12 12 import {MobileShell} from './view/shell/mobile' 13 + import {s} from './view/lib/styles' 13 14 14 15 const App = observer(() => { 15 16 const [rootStore, setRootStore] = useState<RootStoreModel | undefined>( ··· 39 40 } 40 41 41 42 return ( 42 - <GestureHandlerRootView style={{flex: 1}}> 43 + <GestureHandlerRootView style={s.h100pct}> 43 44 <RootSiblingParent> 44 45 <RootStoreProvider value={rootStore}> 45 46 <ThemeProvider theme={rootStore.shell.darkMode ? 'dark' : 'light'}>
+10 -12
src/lib/strings.ts
··· 78 78 let ents: Entity[] = [] 79 79 { 80 80 // mentions 81 - const re = /(^|\s|\()(@)([a-zA-Z0-9\.-]+)(\b)/g 81 + const re = /(^|\s|\()(@)([a-zA-Z0-9.-]+)(\b)/g 82 82 while ((match = re.exec(text))) { 83 83 if (knownHandles && !knownHandles.has(match[3])) { 84 84 continue // not a known handle ··· 133 133 type DetectedLinkable = string | DetectedLink 134 134 export function detectLinkables(text: string): DetectedLinkable[] { 135 135 const re = 136 - /((^|\s|\()@[a-z0-9\.-]*)|((^|\s|\()https?:\/\/[\S]+)|((^|\s|\()(?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*)/gi 136 + /((^|\s|\()@[a-z0-9.-]*)|((^|\s|\()https?:\/\/[\S]+)|((^|\s|\()(?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*)/gi 137 137 const segments = [] 138 138 let match 139 139 let start = 0 ··· 154 154 matchValue = matchValue.slice(1) 155 155 } 156 156 157 - { 158 - // strip ending puncuation 159 - if (/[.,;!?]$/.test(matchValue)) { 160 - matchValue = matchValue.slice(0, -1) 161 - } 162 - if (/[)]$/.test(matchValue) && !matchValue.includes('(')) { 163 - matchValue = matchValue.slice(0, -1) 164 - } 157 + // strip ending puncuation 158 + if (/[.,;!?]$/.test(matchValue)) { 159 + matchValue = matchValue.slice(0, -1) 160 + } 161 + if (/[)]$/.test(matchValue) && !matchValue.includes('(')) { 162 + matchValue = matchValue.slice(0, -1) 165 163 } 166 164 167 165 if (start !== matchIndex) { ··· 185 183 } 186 184 187 185 export function createFullHandle(name: string, domain: string): string { 188 - name = (name || '').replace(/[\.]+$/, '') 189 - domain = (domain || '').replace(/^[\.]+/, '') 186 + name = (name || '').replace(/[.]+$/, '') 187 + domain = (domain || '').replace(/^[.]+/, '') 190 188 return `${name}.${domain}` 191 189 } 192 190
+9 -8
src/state/models/navigation.ts
··· 3 3 4 4 let __id = 0 5 5 function genId() { 6 - return ++__id 6 + return String(++__id) 7 7 } 8 8 9 9 // NOTE ··· 24 24 url: string 25 25 ts: number 26 26 title?: string 27 - id: number 27 + id: string 28 28 } 29 29 30 - export type HistoryPtr = [number, number] 30 + export type HistoryPtr = string // `{tabId}-{historyId}` 31 31 32 32 export class NavigationTabModel { 33 33 id = genId() ··· 151 151 } 152 152 } 153 153 154 - setTitle(id: number, title: string) { 154 + setTitle(id: string, title: string) { 155 155 this.history = this.history.map(h => { 156 156 if (h.id === id) { 157 157 return {...h, title} ··· 174 174 } 175 175 } 176 176 177 - hydrate(v: unknown) { 177 + hydrate(_v: unknown) { 178 178 // TODO fixme 179 179 // if (isObj(v)) { 180 180 // if (hasProp(v, 'history') && Array.isArray(v.history)) { ··· 241 241 return this.tabs.length 242 242 } 243 243 244 - isCurrentScreen(tabId: number, index: number) { 244 + isCurrentScreen(tabId: string, index: number) { 245 245 return this.tab.id === tabId && this.tab.index === index 246 246 } 247 247 ··· 257 257 } 258 258 259 259 setTitle(ptr: HistoryPtr, title: string) { 260 - this.tabs.find(t => t.id === ptr[0])?.setTitle(ptr[1], title) 260 + const [tid, hid] = ptr.split('-') 261 + this.tabs.find(t => t.id === tid)?.setTitle(hid, title) 261 262 } 262 263 263 264 handleLink(url: string) { ··· 338 339 } 339 340 } 340 341 341 - hydrate(v: unknown) { 342 + hydrate(_v: unknown) { 342 343 // TODO fixme 343 344 this.clear() 344 345 /*if (isObj(v)) {
+3 -3
src/view/com/composer/ComposePost.tsx
··· 297 297 ) 298 298 } 299 299 }) 300 - }, [text, pal.link]) 300 + }, [text, pal.link, pal.text]) 301 301 302 302 return ( 303 303 <KeyboardAvoidingView ··· 393 393 ref={textInput} 394 394 multiline 395 395 scrollEnabled 396 - onChangeText={(text: string) => onChangeText(text)} 396 + onChangeText={(str: string) => onChangeText(str)} 397 397 onPaste={onPaste} 398 398 placeholder={selectTextInputPlaceholder} 399 399 placeholderTextColor={pal.colors.textLight} ··· 475 475 ) 476 476 }) 477 477 478 - const atPrefixRegex = /@([a-z0-9\.]*)$/i 478 + const atPrefixRegex = /@([a-z0-9.]*)$/i 479 479 function extractTextAutocompletePrefix(text: string) { 480 480 const match = atPrefixRegex.exec(text) 481 481 if (match) {
+3 -3
src/view/com/discover/SuggestedFollows.tsx
··· 39 39 // Using default import (React.use...) instead of named import (use...) to be able to mock store's data in jest environment 40 40 const view = React.useMemo<SuggestedActorsViewModel>( 41 41 () => new SuggestedActorsViewModel(store), 42 - [], 42 + [store], 43 43 ) 44 44 45 45 useEffect(() => { ··· 54 54 if (!view.isLoading && !view.hasError && !view.hasContent) { 55 55 onNoSuggestions?.() 56 56 } 57 - }, [view, view.isLoading, view.hasError, view.hasContent]) 57 + }, [view, view.isLoading, view.hasError, view.hasContent, onNoSuggestions]) 58 58 59 59 const onPressTryAgain = () => 60 60 view ··· 128 128 keyExtractor={item => item._reactKey} 129 129 renderItem={renderItem} 130 130 style={s.flex1} 131 - contentContainerStyle={{paddingBottom: 200}} 131 + contentContainerStyle={s.contentContainer} 132 132 /> 133 133 </View> 134 134 )}
+7 -8
src/view/com/login/Signin.tsx
··· 207 207 style={[pal.borderDark, styles.group]} 208 208 onPress={() => onSelectAccount(undefined)}> 209 209 <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}> 210 - <View style={s.p10}> 211 - <View 212 - style={[pal.btn, {width: 30, height: 30, borderRadius: 15}]} 213 - /> 214 - </View> 215 - <Text style={styles.accountText}> 210 + <Text style={[styles.accountText, styles.accountTextOther]}> 216 211 <Text type="lg" style={pal.text}> 217 212 Other account 218 213 </Text> ··· 556 551 {!serviceDescription || isProcessing ? ( 557 552 <ActivityIndicator /> 558 553 ) : !email ? ( 559 - <Text type="xl-bold" style={[pal.link, s.pr5, {opacity: 0.5}]}> 554 + <Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}> 560 555 Next 561 556 </Text> 562 557 ) : ( ··· 691 686 {isProcessing ? ( 692 687 <ActivityIndicator /> 693 688 ) : !resetCode || !password ? ( 694 - <Text type="xl-bold" style={[pal.link, s.pr5, {opacity: 0.5}]}> 689 + <Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}> 695 690 Next 696 691 </Text> 697 692 ) : ( ··· 810 805 alignItems: 'baseline', 811 806 paddingVertical: 10, 812 807 }, 808 + accountTextOther: { 809 + paddingLeft: 12, 810 + }, 813 811 error: { 814 812 backgroundColor: colors.red4, 815 813 flexDirection: 'row', ··· 832 830 justifyContent: 'center', 833 831 marginRight: 5, 834 832 }, 833 + dimmed: {opacity: 0.5}, 835 834 })
+2 -1
src/view/com/modals/EditProfile.tsx
··· 121 121 </View> 122 122 </View> 123 123 {error !== '' && ( 124 - <View style={{marginTop: 20}}> 124 + <View style={styles.errorContainer}> 125 125 <ErrorMessage message={error} /> 126 126 </View> 127 127 )} ··· 231 231 marginBottom: 36, 232 232 marginHorizontal: -14, 233 233 }, 234 + errorContainer: {marginTop: 20}, 234 235 })
+1 -1
src/view/com/modals/ServerInput.tsx
··· 56 56 </View> 57 57 <View style={styles.group}> 58 58 <Text style={styles.label}>Other service</Text> 59 - <View style={{flexDirection: 'row'}}> 59 + <View style={s.flexRow}> 60 60 <BottomSheetTextInput 61 61 testID="customServerTextInput" 62 62 style={styles.textInput}
+10 -9
src/view/com/notifications/Feed.tsx
··· 1 1 import React from 'react' 2 2 import {observer} from 'mobx-react-lite' 3 - import {View, FlatList} from 'react-native' 3 + import {FlatList, StyleSheet, View} from 'react-native' 4 4 import {NotificationsViewModel} from '../../../state/models/notifications-view' 5 5 import {FeedItem} from './FeedItem' 6 6 import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' 7 7 import {ErrorMessage} from '../util/error/ErrorMessage' 8 8 import {EmptyState} from '../util/EmptyState' 9 9 import {OnScrollCb} from '../../lib/hooks/useOnMainScroll' 10 + import {s} from '../../lib/styles' 10 11 11 12 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} 12 13 ··· 29 30 <EmptyState 30 31 icon="bell" 31 32 message="No notifications yet!" 32 - style={{paddingVertical: 40}} 33 + style={styles.emptyState} 33 34 /> 34 35 ) 35 36 } ··· 58 59 } 59 60 } 60 61 return ( 61 - <View style={{flex: 1}}> 62 + <View style={s.h100pct}> 62 63 {view.isLoading && !data && <NotificationFeedLoadingPlaceholder />} 63 64 {view.hasError && ( 64 - <ErrorMessage 65 - message={view.error} 66 - style={{margin: 6}} 67 - onPressTryAgain={onPressTryAgain} 68 - /> 65 + <ErrorMessage message={view.error} onPressTryAgain={onPressTryAgain} /> 69 66 )} 70 67 {data && ( 71 68 <FlatList ··· 76 73 onRefresh={onRefresh} 77 74 onEndReached={onEndReached} 78 75 onScroll={onScroll} 79 - contentContainerStyle={{paddingBottom: 200}} 76 + contentContainerStyle={s.contentContainer} 80 77 /> 81 78 )} 82 79 </View> 83 80 ) 84 81 }) 82 + 83 + const styles = StyleSheet.create({ 84 + emptyState: {paddingVertical: 40}, 85 + })
+13 -7
src/view/com/onboard/FeatureExplainer.tsx
··· 19 19 const Intro = () => ( 20 20 <View style={styles.explainer}> 21 21 <Text 22 - style={[ 23 - styles.explainerHeading, 24 - s.normal, 25 - {lineHeight: 60, paddingTop: 50, paddingBottom: 50}, 26 - ]}> 27 - Welcome to <Text style={[s.bold, s.blue3, {fontSize: 56}]}>Bluesky</Text> 22 + style={[styles.explainerHeading, s.normal, styles.explainerHeadingIntro]}> 23 + Welcome to{' '} 24 + <Text style={[s.bold, s.blue3, styles.explainerHeadingBrand]}> 25 + Bluesky 26 + </Text> 28 27 </Text> 29 - <Text style={[styles.explainerDesc, {fontSize: 24}]}> 28 + <Text style={[styles.explainerDesc, styles.explainerDescIntro]}> 30 29 This is an early beta. Your feedback is appreciated! 31 30 </Text> 32 31 </View> ··· 161 160 textAlign: 'center', 162 161 marginBottom: 16, 163 162 }, 163 + explainerHeadingIntro: { 164 + lineHeight: 60, 165 + paddingTop: 50, 166 + paddingBottom: 50, 167 + }, 168 + explainerHeadingBrand: {fontSize: 56}, 164 169 explainerDesc: { 165 170 fontSize: 18, 166 171 textAlign: 'center', 167 172 marginBottom: 16, 168 173 }, 174 + explainerDescIntro: {fontSize: 24}, 169 175 explainerImg: { 170 176 resizeMode: 'contain', 171 177 maxWidth: '100%',
+1 -5
src/view/com/post-thread/PostRepostedBy.tsx
··· 53 53 if (view.hasError) { 54 54 return ( 55 55 <View> 56 - <ErrorMessage 57 - message={view.error} 58 - style={{margin: 6}} 59 - onPressTryAgain={onRefresh} 60 - /> 56 + <ErrorMessage message={view.error} onPressTryAgain={onRefresh} /> 61 57 </View> 62 58 ) 63 59 }
+4 -7
src/view/com/post-thread/PostThread.tsx
··· 7 7 } from '../../../state/models/post-thread-view' 8 8 import {PostThreadItem} from './PostThreadItem' 9 9 import {ErrorMessage} from '../util/error/ErrorMessage' 10 + import {s} from '../../lib/styles' 10 11 11 12 export const PostThread = observer(function PostThread({ 12 13 uri, ··· 60 61 if (view.hasError) { 61 62 return ( 62 63 <View> 63 - <ErrorMessage 64 - message={view.error} 65 - style={{margin: 6}} 66 - onPressTryAgain={onRefresh} 67 - /> 64 + <ErrorMessage message={view.error} onPressTryAgain={onRefresh} /> 68 65 </View> 69 66 ) 70 67 } ··· 84 81 onRefresh={onRefresh} 85 82 onLayout={onLayout} 86 83 onScrollToIndexFailed={onScrollToIndexFailed} 87 - style={{flex: 1}} 88 - contentContainerStyle={{paddingBottom: 200}} 84 + style={s.h100pct} 85 + contentContainerStyle={s.contentContainer} 89 86 /> 90 87 ) 91 88 })
+9 -7
src/view/com/post-thread/PostThreadItem.tsx
··· 80 80 .catch(e => store.log.error('Failed to toggle upvote', e)) 81 81 } 82 82 const onCopyPostText = () => { 83 - Clipboard.setString(record.text) 83 + Clipboard.setString(record?.text || '') 84 84 Toast.show('Copied to clipboard') 85 85 } 86 86 const onDeletePost = () => { ··· 131 131 </Link> 132 132 </View> 133 133 <View style={styles.layoutContent}> 134 - <View style={[styles.meta, {paddingTop: 5, paddingBottom: 0}]}> 135 - <View style={{flexDirection: 'row', alignItems: 'baseline'}}> 134 + <View style={[styles.meta, styles.metaExpandedLine1]}> 135 + <View style={[s.flexRow, s.alignBaseline]}> 136 136 <Link 137 137 style={styles.metaItem} 138 138 href={authorHref} ··· 305 305 lineHeight={1.3} 306 306 /> 307 307 </View> 308 - ) : ( 309 - <View style={{height: 5}} /> 310 - )} 311 - <PostEmbeds embed={item.post.embed} style={{marginBottom: 10}} /> 308 + ) : undefined} 309 + <PostEmbeds embed={item.post.embed} style={s.mb10} /> 312 310 <PostCtrls 313 311 itemHref={itemHref} 314 312 itemTitle={itemTitle} ··· 388 386 flexDirection: 'row', 389 387 paddingTop: 2, 390 388 paddingBottom: 2, 389 + }, 390 + metaExpandedLine1: { 391 + paddingTop: 5, 392 + paddingBottom: 0, 391 393 }, 392 394 metaItem: { 393 395 paddingRight: 5,
+1 -5
src/view/com/post-thread/PostVotedBy.tsx
··· 48 48 if (view.hasError) { 49 49 return ( 50 50 <View> 51 - <ErrorMessage 52 - message={view.error} 53 - style={{margin: 6}} 54 - onPressTryAgain={onRefresh} 55 - /> 51 + <ErrorMessage message={view.error} onPressTryAgain={onRefresh} /> 56 52 </View> 57 53 ) 58 54 }
+3 -5
src/view/com/post/Post.tsx
··· 156 156 timestamp={item.post.indexedAt} 157 157 /> 158 158 {replyAuthorDid !== '' && ( 159 - <View style={[s.flexRow, s.mb2, {alignItems: 'center'}]}> 159 + <View style={[s.flexRow, s.mb2, s.alignCenter]}> 160 160 <FontAwesomeIcon 161 161 icon="reply" 162 162 size={9} ··· 187 187 lineHeight={1.3} 188 188 /> 189 189 </View> 190 - ) : ( 191 - <View style={{height: 5}} /> 192 - )} 193 - <PostEmbeds embed={item.post.embed} style={{marginBottom: 10}} /> 190 + ) : undefined} 191 + <PostEmbeds embed={item.post.embed} style={s.mb10} /> 194 192 <PostCtrls 195 193 itemHref={itemHref} 196 194 itemTitle={itemTitle}
+8 -4
src/view/com/post/PostText.tsx
··· 1 1 import React, {useState, useEffect} from 'react' 2 2 import {observer} from 'mobx-react-lite' 3 - import {View} from 'react-native' 3 + import {StyleSheet, View} from 'react-native' 4 4 import {LoadingPlaceholder} from '../util/LoadingPlaceholder' 5 5 import {ErrorMessage} from '../util/error/ErrorMessage' 6 6 import {Text} from '../util/text/Text' ··· 31 31 if (!model || model.isLoading || model.uri !== uri) { 32 32 return ( 33 33 <View> 34 - <LoadingPlaceholder width="100%" height={8} style={{marginTop: 6}} /> 35 - <LoadingPlaceholder width="100%" height={8} style={{marginTop: 6}} /> 36 - <LoadingPlaceholder width={100} height={8} style={{marginTop: 6}} /> 34 + <LoadingPlaceholder width="100%" height={8} style={styles.mt6} /> 35 + <LoadingPlaceholder width="100%" height={8} style={styles.mt6} /> 36 + <LoadingPlaceholder width={100} height={8} style={styles.mt6} /> 37 37 </View> 38 38 ) 39 39 } ··· 56 56 </View> 57 57 ) 58 58 }) 59 + 60 + const styles = StyleSheet.create({ 61 + mt6: {marginTop: 6}, 62 + })
+11 -8
src/view/com/posts/Feed.tsx
··· 5 5 View, 6 6 FlatList, 7 7 StyleProp, 8 + StyleSheet, 8 9 ViewStyle, 9 10 } from 'react-native' 10 11 import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' ··· 14 15 import {FeedItem} from './FeedItem' 15 16 import {PromptButtons} from './PromptButtons' 16 17 import {OnScrollCb} from '../../lib/hooks/useOnMainScroll' 18 + import {s} from '../../lib/styles' 17 19 18 20 const COMPOSE_PROMPT_ITEM = {_reactKey: '__prompt__'} 19 21 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} ··· 47 49 <EmptyState 48 50 icon="bars" 49 51 message="This feed is empty!" 50 - style={{paddingVertical: 40}} 52 + style={styles.emptyState} 51 53 /> 52 54 ) 53 55 } else { ··· 76 78 } 77 79 const FeedFooter = () => 78 80 feed.isLoading ? ( 79 - <View style={{paddingTop: 20}}> 81 + <View style={styles.feedFooter}> 80 82 <ActivityIndicator /> 81 83 </View> 82 84 ) : ( ··· 87 89 {!data && <PromptButtons onPressCompose={onPressCompose} />} 88 90 {feed.isLoading && !data && <PostFeedLoadingPlaceholder />} 89 91 {feed.hasError && ( 90 - <ErrorMessage 91 - message={feed.error} 92 - style={{margin: 6}} 93 - onPressTryAgain={onPressTryAgain} 94 - /> 92 + <ErrorMessage message={feed.error} onPressTryAgain={onPressTryAgain} /> 95 93 )} 96 94 {feed.hasLoaded && data && ( 97 95 <FlatList ··· 101 99 renderItem={renderItem} 102 100 ListFooterComponent={FeedFooter} 103 101 refreshing={feed.isRefreshing} 104 - contentContainerStyle={{paddingBottom: 100}} 102 + contentContainerStyle={s.contentContainer} 105 103 onScroll={onScroll} 106 104 onRefresh={onRefresh} 107 105 onEndReached={onEndReached} ··· 110 108 </View> 111 109 ) 112 110 }) 111 + 112 + const styles = StyleSheet.create({ 113 + feedFooter: {paddingTop: 20}, 114 + emptyState: {paddingVertical: 40}, 115 + })
+4 -5
src/view/com/posts/FeedItem.tsx
··· 124 124 style={[ 125 125 styles.bottomReplyLine, 126 126 {borderColor: pal.colors.replyLine}, 127 - isNoTop ? {top: 64} : undefined, 127 + isNoTop ? styles.bottomReplyLineNoTop : undefined, 128 128 ]} 129 129 /> 130 130 )} ··· 163 163 timestamp={item.post.indexedAt} 164 164 /> 165 165 {!isChild && replyAuthorDid !== '' && ( 166 - <View style={[s.flexRow, s.mb2, {alignItems: 'center'}]}> 166 + <View style={[s.flexRow, s.mb2, s.alignCenter]}> 167 167 <FontAwesomeIcon 168 168 icon="reply" 169 169 size={9} ··· 195 195 lineHeight={1.3} 196 196 /> 197 197 </View> 198 - ) : ( 199 - <View style={{height: 5}} /> 200 - )} 198 + ) : undefined} 201 199 {item.post.embed ? ( 202 200 <PostEmbeds embed={item.post.embed} style={styles.embed} /> 203 201 ) : null} ··· 281 279 bottom: 0, 282 280 borderLeftWidth: 2, 283 281 }, 282 + bottomReplyLineNoTop: {top: 64}, 284 283 includeReason: { 285 284 flexDirection: 'row', 286 285 paddingLeft: 50,
+1 -5
src/view/com/profile/ProfileFollowers.tsx
··· 54 54 if (view.hasError) { 55 55 return ( 56 56 <View> 57 - <ErrorMessage 58 - message={view.error} 59 - style={{margin: 6}} 60 - onPressTryAgain={onRefresh} 61 - /> 57 + <ErrorMessage message={view.error} onPressTryAgain={onRefresh} /> 62 58 </View> 63 59 ) 64 60 }
+1 -5
src/view/com/profile/ProfileFollows.tsx
··· 54 54 if (view.hasError) { 55 55 return ( 56 56 <View> 57 - <ErrorMessage 58 - message={view.error} 59 - style={{margin: 6}} 60 - onPressTryAgain={onRefresh} 61 - /> 57 + <ErrorMessage message={view.error} onPressTryAgain={onRefresh} /> 62 58 </View> 63 59 ) 64 60 }
+8 -12
src/view/com/profile/ProfileHeader.tsx
··· 100 100 <LoadingPlaceholder width="100%" height={120} /> 101 101 <View 102 102 style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}> 103 - <LoadingPlaceholder 104 - width={80} 105 - height={80} 106 - style={{borderRadius: 40}} 107 - /> 103 + <LoadingPlaceholder width={80} height={80} style={styles.br40} /> 108 104 </View> 109 105 <View style={styles.content}> 110 106 <View style={[styles.buttonsLine]}> 111 - <LoadingPlaceholder 112 - width={100} 113 - height={31} 114 - style={{borderRadius: 50}} 115 - /> 107 + <LoadingPlaceholder width={100} height={31} style={styles.br50} /> 116 108 </View> 117 109 <View style={styles.displayNameLine}> 118 - <Text type="title-xl" style={[pal.text, {lineHeight: 38}]}> 110 + <Text type="title-xl" style={[pal.text, styles.title]}> 119 111 {view.displayName || view.handle} 120 112 </Text> 121 113 </View> ··· 208 200 ) : undefined} 209 201 </View> 210 202 <View style={styles.displayNameLine}> 211 - <Text type="title-xl" style={[pal.text, {lineHeight: 38}]}> 203 + <Text type="title-xl" style={[pal.text, styles.title]}> 212 204 {view.displayName || view.handle} 213 205 </Text> 214 206 </View> ··· 349 341 // paddingLeft: 86, 350 342 // marginBottom: 14, 351 343 }, 344 + title: {lineHeight: 38}, 352 345 353 346 handleLine: { 354 347 flexDirection: 'row', ··· 369 362 alignItems: 'center', 370 363 marginBottom: 5, 371 364 }, 365 + 366 + br40: {borderRadius: 40}, 367 + br50: {borderRadius: 50}, 372 368 })
+1 -1
src/view/com/util/Link.tsx
··· 57 57 ) 58 58 }) 59 59 60 - export const TextLink = observer(function Link({ 60 + export const TextLink = observer(function TextLink({ 61 61 type = 'md', 62 62 style, 63 63 href,
+6 -11
src/view/com/util/LoadingPlaceholder.tsx
··· 19 19 return ( 20 20 <View 21 21 style={[ 22 + styles.loadingPlaceholder, 22 23 { 23 24 width, 24 25 height, 25 26 backgroundColor: theme.palette.default.backgroundLight, 26 - borderRadius: 6, 27 - overflow: 'hidden', 28 27 }, 29 28 style, 30 - ]}> 31 - <View 32 - style={{ 33 - width, 34 - height, 35 - backgroundColor: theme.palette.default.backgroundLight, 36 - }} 37 - /> 38 - </View> 29 + ]} 30 + /> 39 31 ) 40 32 } 41 33 ··· 137 129 } 138 130 139 131 const styles = StyleSheet.create({ 132 + loadingPlaceholder: { 133 + borderRadius: 6, 134 + }, 140 135 post: { 141 136 flexDirection: 'row', 142 137 padding: 10,
+5 -8
src/view/com/util/PostCtrls.tsx
··· 128 128 hitSlop={HITSLOP} 129 129 onPress={opts.onPressReply}> 130 130 <CommentBottomArrow 131 - style={[ 132 - defaultCtrlColor, 133 - opts.big ? {marginTop: 2} : {marginTop: 1}, 134 - ]} 131 + style={[defaultCtrlColor, opts.big ? s.mt2 : styles.mt1]} 135 132 strokeWidth={3} 136 133 size={opts.big ? 20 : 15} 137 134 /> ··· 181 178 /> 182 179 ) : ( 183 180 <HeartIcon 184 - style={[ 185 - defaultCtrlColor, 186 - opts.big ? {marginTop: 1} : undefined, 187 - ]} 181 + style={[defaultCtrlColor, opts.big ? styles.mt1 : undefined]} 188 182 strokeWidth={3} 189 183 size={opts.big ? 20 : 16} 190 184 /> ··· 243 237 }, 244 238 ctrlIconUpvoted: { 245 239 color: colors.red3, 240 + }, 241 + mt1: { 242 + marginTop: 1, 246 243 }, 247 244 })
+4 -1
src/view/com/util/PostEmbeds.tsx
··· 67 67 <AutoSizedImage 68 68 uri={embed.images[0].thumb} 69 69 onPress={() => openLightbox(0)} 70 - containerStyle={{borderRadius: 8}} 70 + containerStyle={styles.singleImage} 71 71 /> 72 72 </View> 73 73 ) ··· 119 119 const styles = StyleSheet.create({ 120 120 imagesContainer: { 121 121 marginTop: 4, 122 + }, 123 + singleImage: { 124 + borderRadius: 8, 122 125 }, 123 126 extOuter: { 124 127 borderWidth: 1,
+1 -1
src/view/com/util/Selector.tsx
··· 41 41 width: middle.width, 42 42 } 43 43 return [left, middle, right] 44 - }, [selectedIndex, items, itemLayouts]) 44 + }, [selectedIndex, itemLayouts]) 45 45 46 46 const underlineStyle = { 47 47 backgroundColor: pal.colors.text,
+9 -5
src/view/com/util/UserAvatar.tsx
··· 62 62 ]) 63 63 }, [onSelectNewAvatar]) 64 64 65 - const renderSvg = (size: number, initials: string) => ( 66 - <Svg width={size} height={size} viewBox="0 0 100 100"> 65 + const renderSvg = (svgSize: number, svgInitials: string) => ( 66 + <Svg width={svgSize} height={svgSize} viewBox="0 0 100 100"> 67 67 <Defs> 68 68 <LinearGradient id="grad" x1="0" y1="0" x2="1" y2="1"> 69 69 <Stop offset="0" stopColor={gradients.blue.start} stopOpacity="1" /> ··· 78 78 x="50" 79 79 y="67" 80 80 textAnchor="middle"> 81 - {initials} 81 + {svgInitials} 82 82 </Text> 83 83 </Svg> 84 84 ) ··· 88 88 <TouchableOpacity onPress={handleEditAvatar}> 89 89 {avatar ? ( 90 90 <Image 91 - style={{width: size, height: size, borderRadius: (size / 2) | 0}} 91 + style={{ 92 + width: size, 93 + height: size, 94 + borderRadius: Math.floor(size / 2), 95 + }} 92 96 source={{uri: avatar}} 93 97 /> 94 98 ) : ( ··· 104 108 </TouchableOpacity> 105 109 ) : avatar ? ( 106 110 <Image 107 - style={{width: size, height: size, borderRadius: (size / 2) | 0}} 111 + style={{width: size, height: size, borderRadius: Math.floor(size / 2)}} 108 112 resizeMode="stretch" 109 113 source={{uri: avatar}} 110 114 />
+7 -3
src/view/com/util/UserInfoText.tsx
··· 1 1 import React, {useState, useEffect} from 'react' 2 2 import {AppBskyActorGetProfile as GetProfile} from '@atproto/api' 3 - import {StyleProp, TextStyle} from 'react-native' 3 + import {StyleProp, StyleSheet, TextStyle} from 'react-native' 4 4 import {Link} from './Link' 5 5 import {Text} from './text/Text' 6 6 import {LoadingPlaceholder} from './LoadingPlaceholder' ··· 53 53 return () => { 54 54 aborted = true 55 55 } 56 - }, [did, store.api.app.bsky]) 56 + }, [did, store.profiles]) 57 57 58 58 let inner 59 59 if (didFail) { ··· 73 73 <LoadingPlaceholder 74 74 width={80} 75 75 height={8} 76 - style={{position: 'relative', top: 1, left: 2}} 76 + style={styles.loadingPlaceholder} 77 77 /> 78 78 ) 79 79 } ··· 91 91 92 92 return inner 93 93 } 94 + 95 + const styles = StyleSheet.create({ 96 + loadingPlaceholder: {position: 'relative', top: 1, left: 2}, 97 + })
+18 -13
src/view/com/util/ViewHeader.tsx
··· 11 11 import {Text} from './text/Text' 12 12 import {MagnifyingGlassIcon} from '../../lib/icons' 13 13 import {useStores} from '../../../state' 14 - import {useTheme} from '../../lib/ThemeContext' 15 14 import {usePalette} from '../../lib/hooks/usePalette' 15 + import {colors} from '../../lib/styles' 16 16 17 17 const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10} 18 18 const BACK_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10} ··· 26 26 subtitle?: string 27 27 canGoBack?: boolean 28 28 }) { 29 - const theme = useTheme() 30 29 const pal = usePalette('default') 31 30 const store = useStores() 32 31 const onPressBack = () => { ··· 52 51 testID="viewHeaderBackOrMenuBtn" 53 52 onPress={canGoBack ? onPressBack : onPressMenu} 54 53 hitSlop={BACK_HITSLOP} 55 - style={canGoBack ? styles.backIcon : styles.backIconWide}> 54 + style={canGoBack ? styles.backBtn : styles.backBtnWide}> 56 55 {canGoBack ? ( 57 56 <FontAwesomeIcon 58 57 size={18} 59 58 icon="angle-left" 60 - style={[{marginTop: 6}, pal.text]} 59 + style={[styles.backIcon, pal.text]} 61 60 /> 62 61 ) : ( 63 62 <UserAvatar ··· 96 95 <FontAwesomeIcon icon="signal" style={pal.text} size={16} /> 97 96 <FontAwesomeIcon 98 97 icon="x" 99 - style={{ 100 - backgroundColor: pal.colors.background, 101 - color: theme.palette.error.background, 102 - position: 'absolute', 103 - right: 7, 104 - bottom: 7, 105 - }} 98 + style={[ 99 + styles.littleXIcon, 100 + {backgroundColor: pal.colors.background}, 101 + ]} 106 102 size={8} 107 103 /> 108 104 </> ··· 136 132 fontWeight: 'normal', 137 133 }, 138 134 139 - backIcon: { 135 + backBtn: { 140 136 width: 30, 141 137 height: 30, 142 138 }, 143 - backIconWide: { 139 + backBtnWide: { 144 140 width: 40, 145 141 height: 30, 146 142 marginLeft: 6, 143 + }, 144 + backIcon: { 145 + marginTop: 6, 147 146 }, 148 147 btn: { 149 148 flexDirection: 'row', ··· 153 152 height: 36, 154 153 borderRadius: 20, 155 154 marginLeft: 4, 155 + }, 156 + littleXIcon: { 157 + color: colors.red3, 158 + position: 'absolute', 159 + right: 7, 160 + bottom: 7, 156 161 }, 157 162 })
+3 -2
src/view/com/util/ViewSelector.tsx
··· 5 5 import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue' 6 6 import {OnScrollCb} from '../../lib/hooks/useOnMainScroll' 7 7 import {clamp} from '../../../lib/numbers' 8 + import {s} from '../../lib/styles' 8 9 9 10 const HEADER_ITEM = {_reactKey: '__header__'} 10 11 const SELECTOR_ITEM = {_reactKey: '__selector__'} ··· 54 55 setSelectedIndex(clamp(index, 0, sections.length)) 55 56 useEffect(() => { 56 57 onSelectView?.(selectedIndex) 57 - }, [selectedIndex]) 58 + }, [selectedIndex, onSelectView]) 58 59 59 60 // rendering 60 61 // = ··· 98 99 onScroll={onScroll} 99 100 onRefresh={onRefresh} 100 101 onEndReached={onEndReached} 101 - contentContainerStyle={{paddingBottom: 200}} 102 + contentContainerStyle={s.contentContainer} 102 103 /> 103 104 </HorzSwipe> 104 105 )
+2 -1
src/view/com/util/forms/RadioGroup.tsx
··· 2 2 import {View} from 'react-native' 3 3 import {RadioButton} from './RadioButton' 4 4 import {ButtonType} from './Button' 5 + import {s} from '../../../lib/styles' 5 6 6 7 export interface RadioGroupItem { 7 8 label: string ··· 29 30 {items.map((item, i) => ( 30 31 <RadioButton 31 32 key={item.key} 32 - style={i !== 0 ? {marginTop: 2} : undefined} 33 + style={i !== 0 ? s.mt2 : undefined} 33 34 type={type} 34 35 label={item.label} 35 36 isSelected={item.key === selection}
+5 -2
src/view/com/util/gestures/HorzSwipe.tsx
··· 9 9 View, 10 10 } from 'react-native' 11 11 import {clamp} from 'lodash' 12 + import {s} from '../../../lib/styles' 12 13 13 14 interface Props { 14 15 panX: Animated.Value ··· 111 112 (Math.abs(gestureState.dx) > swipeDistanceThreshold / 4 || 112 113 Math.abs(gestureState.vx) > swipeVelocityThreshold) 113 114 ) { 114 - const final = ((gestureState.dx / Math.abs(gestureState.dx)) * -1) | 0 115 + const final = Math.floor( 116 + (gestureState.dx / Math.abs(gestureState.dx)) * -1, 117 + ) 115 118 Animated.timing(panX, { 116 119 toValue: final, 117 120 duration: 100, ··· 144 147 }) 145 148 146 149 return ( 147 - <View {...panResponder.panHandlers} style={{flex: 1}}> 150 + <View {...panResponder.panHandlers} style={s.h100pct}> 148 151 {children} 149 152 </View> 150 153 )
+2 -1
src/view/com/util/gestures/SwipeAndZoom.tsx
··· 9 9 View, 10 10 } from 'react-native' 11 11 import {clamp} from 'lodash' 12 + import {s} from '../../../lib/styles' 12 13 13 14 export enum Dir { 14 15 None, ··· 294 295 }) 295 296 296 297 return ( 297 - <View {...panResponder.panHandlers} style={{flex: 1}}> 298 + <View {...panResponder.panHandlers} style={s.h100pct}> 298 299 {children} 299 300 </View> 300 301 )
+2 -2
src/view/com/util/images/AutoSizedImage.tsx
··· 47 47 setImgInfo({width, height}) 48 48 } 49 49 }, 50 - (error: any) => { 50 + (err: any) => { 51 51 if (!aborted) { 52 - setError(String(error)) 52 + setError(String(err)) 53 53 } 54 54 }, 55 55 )
+1 -1
src/view/com/util/images/ImageLayoutGrid.tsx
··· 105 105 <TouchableWithoutFeedback onPress={() => onPress?.(1)}> 106 106 <Image source={{uri: uris[1]}} style={size1} /> 107 107 </TouchableWithoutFeedback> 108 - <View style={{height: 5}} /> 108 + <View style={styles.hSpace} /> 109 109 <TouchableWithoutFeedback onPress={() => onPress?.(2)}> 110 110 <Image source={{uri: uris[2]}} style={size1} /> 111 111 </TouchableWithoutFeedback>
+3
src/view/lib/styles.ts
··· 58 58 export const s = StyleSheet.create({ 59 59 // helpers 60 60 footerSpacer: {height: 100}, 61 + contentContainer: {paddingBottom: 200}, 62 + border1: {borderWidth: 1}, 61 63 62 64 // font weights 63 65 fw600: {fontWeight: '600'}, ··· 140 142 flexCol: {flexDirection: 'column'}, 141 143 flex1: {flex: 1}, 142 144 alignCenter: {alignItems: 'center'}, 145 + alignBaseline: {alignItems: 'baseline'}, 143 146 144 147 // position 145 148 absolute: {position: 'absolute'},
+1 -1
src/view/routes.ts
··· 18 18 import {Log} from './screens/Log' 19 19 20 20 export type ScreenParams = { 21 - navIdx: [number, number] 21 + navIdx: string 22 22 params: Record<string, any> 23 23 visible: boolean 24 24 scrollElRef?: MutableRefObject<FlatList<any> | undefined>
+1 -1
src/view/screens/Contacts.tsx
··· 17 17 if (visible) { 18 18 store.nav.setTitle(navIdx, 'Contacts') 19 19 } 20 - }, [store, visible]) 20 + }, [store, visible, navIdx]) 21 21 22 22 const [searchText, onChangeSearchText] = useState('') 23 23 const inputRef = useRef<TextInput | null>(null)
+22 -58
src/view/screens/Debug.tsx
··· 4 4 import {ThemeProvider} from '../lib/ThemeContext' 5 5 import {PaletteColorName} from '../lib/ThemeContext' 6 6 import {usePalette} from '../lib/hooks/usePalette' 7 + import {s} from '../lib/styles' 7 8 8 9 import {Text} from '../com/util/text/Text' 9 10 import {ViewSelector} from '../com/util/ViewSelector' ··· 48 49 const renderItem = item => { 49 50 return ( 50 51 <View> 51 - <View style={{paddingTop: 10, paddingHorizontal: 10}}> 52 + <View style={[s.pt10, s.pl10, s.pr10]}> 52 53 <ToggleButton 53 54 type="default-light" 54 55 onPress={onToggleColorScheme} ··· 70 71 const items = [{currentView}] 71 72 72 73 return ( 73 - <View style={[{flex: 1}, pal.view]}> 74 + <View style={[s.h100pct, pal.view]}> 74 75 <ViewHeader title="Debug panel" /> 75 76 <ViewSelector 76 77 swipeEnabled ··· 86 87 function Heading({label}: {label: string}) { 87 88 const pal = usePalette('default') 88 89 return ( 89 - <View style={{paddingTop: 10, paddingBottom: 5}}> 90 + <View style={[s.pt10, s.pb5]}> 90 91 <Text type="title-lg" style={pal.text}> 91 92 {label} 92 93 </Text> ··· 96 97 97 98 function BaseView() { 98 99 return ( 99 - <View style={{paddingHorizontal: 10}}> 100 + <View style={[s.pl10, s.pr10]}> 100 101 <Heading label="Typography" /> 101 102 <TypographyView /> 102 103 <Heading label="Palettes" /> ··· 109 110 <EmptyStateView /> 110 111 <Heading label="Loading placeholders" /> 111 112 <LoadingPlaceholderView /> 112 - <View style={{height: 200}} /> 113 + <View style={s.footerSpacer} /> 113 114 </View> 114 115 ) 115 116 } 116 117 117 118 function ControlsView() { 118 119 return ( 119 - <ScrollView style={{paddingHorizontal: 10}}> 120 + <ScrollView style={[s.pl10, s.pr10]}> 120 121 <Heading label="Buttons" /> 121 122 <ButtonsView /> 122 123 <Heading label="Dropdown Buttons" /> ··· 125 126 <ToggleButtonsView /> 126 127 <Heading label="Radio Buttons" /> 127 128 <RadioButtonsView /> 128 - <View style={{height: 200}} /> 129 + <View style={s.footerSpacer} /> 129 130 </ScrollView> 130 131 ) 131 132 } 132 133 133 134 function ErrorView() { 134 135 return ( 135 - <View style={{padding: 10}}> 136 - <View style={{marginBottom: 5}}> 136 + <View style={s.p10}> 137 + <View style={s.mb5}> 137 138 <ErrorScreen 138 139 title="Error screen" 139 140 message="A major error occurred that led the entire screen to fail" ··· 141 142 onPressTryAgain={() => {}} 142 143 /> 143 144 </View> 144 - <View style={{marginBottom: 5}}> 145 + <View style={s.mb5}> 145 146 <ErrorMessage message="This is an error that occurred while things were being done" /> 146 147 </View> 147 - <View style={{marginBottom: 5}}> 148 + <View style={s.mb5}> 148 149 <ErrorMessage 149 150 message="This is an error that occurred while things were being done" 150 151 numberOfLines={1} 151 152 /> 152 153 </View> 153 - <View style={{marginBottom: 5}}> 154 + <View style={s.mb5}> 154 155 <ErrorMessage 155 156 message="This is an error that occurred while things were being done" 156 157 onPressTryAgain={() => {}} 157 158 /> 158 159 </View> 159 - <View style={{marginBottom: 5}}> 160 + <View style={s.mb5}> 160 161 <ErrorMessage 161 162 message="This is an error that occurred while things were being done" 162 163 onPressTryAgain={() => {}} ··· 171 172 const defaultPal = usePalette('default') 172 173 const pal = usePalette(palette) 173 174 return ( 174 - <View 175 - style={[ 176 - pal.view, 177 - pal.border, 178 - { 179 - borderWidth: 1, 180 - padding: 10, 181 - marginBottom: 5, 182 - }, 183 - ]}> 175 + <View style={[pal.view, pal.border, s.p10, s.mb5, s.border1]}> 184 176 <Text style={[pal.text]}>{palette} colors</Text> 185 177 <Text style={[pal.textLight]}>Light text</Text> 186 178 <Text style={[pal.link]}>Link text</Text> ··· 197 189 const pal = usePalette('default') 198 190 return ( 199 191 <View style={[pal.view]}> 200 - <Text type="xxl-thin" style={[pal.text]}> 201 - 'xxl-thin' lorem ipsum dolor 202 - </Text> 203 - <Text type="xxl" style={[pal.text]}> 204 - 'xxl' lorem ipsum dolor 205 - </Text> 206 - <Text type="xxl-medium" style={[pal.text]}> 207 - 'xxl-medium' lorem ipsum dolor 208 - </Text> 209 - <Text type="xxl-bold" style={[pal.text]}> 210 - 'xxl-bold' lorem ipsum dolor 211 - </Text> 212 - <Text type="xxl-heavy" style={[pal.text]}> 213 - 'xxl-heavy' lorem ipsum dolor 214 - </Text> 215 192 <Text type="xl-thin" style={[pal.text]}> 216 193 'xl-thin' lorem ipsum dolor 217 194 </Text> ··· 300 277 <Text type="button" style={[pal.text]}> 301 278 Button 302 279 </Text> 303 - <Text type="overline" style={[pal.text]}> 304 - Overline 305 - </Text> 306 280 </View> 307 281 ) 308 282 } ··· 325 299 const buttonStyles = {marginRight: 5} 326 300 return ( 327 301 <View style={[defaultPal.view]}> 328 - <View 329 - style={{ 330 - flexDirection: 'row', 331 - marginBottom: 5, 332 - }}> 302 + <View style={[s.flexRow, s.mb5]}> 333 303 <Button type="primary" label="Primary solid" style={buttonStyles} /> 334 304 <Button type="secondary" label="Secondary solid" style={buttonStyles} /> 335 305 <Button type="inverted" label="Inverted solid" style={buttonStyles} /> 336 306 </View> 337 - <View style={{flexDirection: 'row'}}> 307 + <View style={s.flexRow}> 338 308 <Button 339 309 type="primary-outline" 340 310 label="Primary outline" ··· 346 316 style={buttonStyles} 347 317 /> 348 318 </View> 349 - <View style={{flexDirection: 'row'}}> 319 + <View style={s.flexRow}> 350 320 <Button 351 321 type="primary-light" 352 322 label="Primary light" ··· 358 328 style={buttonStyles} 359 329 /> 360 330 </View> 361 - <View style={{flexDirection: 'row'}}> 331 + <View style={s.flexRow}> 362 332 <Button 363 333 type="default-light" 364 334 label="Default light" ··· 390 360 const defaultPal = usePalette('default') 391 361 return ( 392 362 <View style={[defaultPal.view]}> 393 - <View 394 - style={{ 395 - marginBottom: 5, 396 - }}> 363 + <View style={s.mb5}> 397 364 <DropdownButton 398 365 type="primary" 399 366 items={DROPDOWN_ITEMS} ··· 401 368 label="Primary button" 402 369 /> 403 370 </View> 404 - <View 405 - style={{ 406 - marginBottom: 5, 407 - }}> 371 + <View style={s.mb5}> 408 372 <DropdownButton type="bare" items={DROPDOWN_ITEMS} menuWidth={200}> 409 373 <Text>Bare</Text> 410 374 </DropdownButton> ··· 415 379 416 380 function ToggleButtonsView() { 417 381 const defaultPal = usePalette('default') 418 - const buttonStyles = {marginBottom: 5} 382 + const buttonStyles = s.mb5 419 383 const [isSelected, setIsSelected] = React.useState(false) 420 384 const onToggle = () => setIsSelected(!isSelected) 421 385 return (
+6 -5
src/view/screens/Home.tsx
··· 83 83 } 84 84 85 85 return ( 86 - <View style={s.flex1}> 86 + <View style={s.h100pct}> 87 87 <ViewHeader title="Bluesky" subtitle="Private Beta" canGoBack={false} /> 88 88 <Feed 89 89 testID="homeFeed" 90 90 key="default" 91 91 feed={store.me.mainFeed} 92 92 scrollElRef={scrollElRef} 93 - style={{flex: 1}} 93 + style={s.h100pct} 94 94 onPressCompose={onPressCompose} 95 95 onPressTryAgain={onPressTryAgain} 96 96 onScroll={onMainScroll} ··· 99 99 <TouchableOpacity 100 100 style={[ 101 101 styles.loadLatest, 102 - store.shell.minimalShellMode 103 - ? {bottom: 35} 104 - : {bottom: 60 + clamp(safeAreaInsets.bottom, 15, 30)}, 102 + !store.shell.minimalShellMode && { 103 + bottom: 60 + clamp(safeAreaInsets.bottom, 15, 30), 104 + }, 105 105 ]} 106 106 onPress={onPressLoadLatest} 107 107 hitSlop={HITSLOP}> ··· 125 125 loadLatest: { 126 126 position: 'absolute', 127 127 left: 20, 128 + bottom: 35, 128 129 shadowColor: '#000', 129 130 shadowOpacity: 0.3, 130 131 shadowOffset: {width: 0, height: 1},
+2 -2
src/view/screens/Log.tsx
··· 21 21 } 22 22 store.shell.setMinimalShellMode(false) 23 23 store.nav.setTitle(navIdx, 'Log') 24 - }, [visible, store]) 24 + }, [visible, store, navIdx]) 25 25 26 26 const toggler = (id: string) => () => { 27 27 if (expanded.includes(id)) { ··· 52 52 <Text type="sm" style={[styles.summary, pal.text]}> 53 53 {entry.summary} 54 54 </Text> 55 - {!!entry.details ? ( 55 + {entry.details ? ( 56 56 <FontAwesomeIcon 57 57 icon={ 58 58 expanded.includes(entry.id) ? 'angle-up' : 'angle-down'
+46 -48
src/view/screens/Login.tsx
··· 18 18 import {usePalette} from '../lib/hooks/usePalette' 19 19 20 20 enum ScreenState { 21 - SigninOrCreateAccount, 22 - Signin, 23 - CreateAccount, 21 + S_SigninOrCreateAccount, 22 + S_Signin, 23 + S_CreateAccount, 24 24 } 25 25 26 26 const SigninOrCreateAccount = ({ ··· 78 78 ) 79 79 } 80 80 81 - export const Login = observer( 82 - (/*{navigation}: RootTabsScreenProps<'Login'>*/) => { 83 - const pal = usePalette('default') 84 - const [screenState, setScreenState] = useState<ScreenState>( 85 - ScreenState.SigninOrCreateAccount, 86 - ) 87 - 88 - if (screenState === ScreenState.SigninOrCreateAccount) { 89 - return ( 90 - <LinearGradient 91 - colors={['#007CFF', '#00BCFF']} 92 - start={{x: 0, y: 0.8}} 93 - end={{x: 0, y: 1}} 94 - style={styles.container}> 95 - <SafeAreaView testID="noSessionView" style={styles.container}> 96 - <ErrorBoundary> 97 - <SigninOrCreateAccount 98 - onPressSignin={() => setScreenState(ScreenState.Signin)} 99 - onPressCreateAccount={() => 100 - setScreenState(ScreenState.CreateAccount) 101 - } 102 - /> 103 - </ErrorBoundary> 104 - </SafeAreaView> 105 - </LinearGradient> 106 - ) 107 - } 81 + export const Login = observer(() => { 82 + const pal = usePalette('default') 83 + const [screenState, setScreenState] = useState<ScreenState>( 84 + ScreenState.S_SigninOrCreateAccount, 85 + ) 108 86 87 + if (screenState === ScreenState.S_SigninOrCreateAccount) { 109 88 return ( 110 - <View style={[styles.container, pal.view]}> 89 + <LinearGradient 90 + colors={['#007CFF', '#00BCFF']} 91 + start={{x: 0, y: 0.8}} 92 + end={{x: 0, y: 1}} 93 + style={styles.container}> 111 94 <SafeAreaView testID="noSessionView" style={styles.container}> 112 95 <ErrorBoundary> 113 - {screenState === ScreenState.Signin ? ( 114 - <Signin 115 - onPressBack={() => 116 - setScreenState(ScreenState.SigninOrCreateAccount) 117 - } 118 - /> 119 - ) : undefined} 120 - {screenState === ScreenState.CreateAccount ? ( 121 - <CreateAccount 122 - onPressBack={() => 123 - setScreenState(ScreenState.SigninOrCreateAccount) 124 - } 125 - /> 126 - ) : undefined} 96 + <SigninOrCreateAccount 97 + onPressSignin={() => setScreenState(ScreenState.S_Signin)} 98 + onPressCreateAccount={() => 99 + setScreenState(ScreenState.S_CreateAccount) 100 + } 101 + /> 127 102 </ErrorBoundary> 128 103 </SafeAreaView> 129 - </View> 104 + </LinearGradient> 130 105 ) 131 - }, 132 - ) 106 + } 107 + 108 + return ( 109 + <View style={[styles.container, pal.view]}> 110 + <SafeAreaView testID="noSessionView" style={styles.container}> 111 + <ErrorBoundary> 112 + {screenState === ScreenState.S_Signin ? ( 113 + <Signin 114 + onPressBack={() => 115 + setScreenState(ScreenState.S_SigninOrCreateAccount) 116 + } 117 + /> 118 + ) : undefined} 119 + {screenState === ScreenState.S_CreateAccount ? ( 120 + <CreateAccount 121 + onPressBack={() => 122 + setScreenState(ScreenState.S_SigninOrCreateAccount) 123 + } 124 + /> 125 + ) : undefined} 126 + </ErrorBoundary> 127 + </SafeAreaView> 128 + </View> 129 + ) 130 + }) 133 131 134 132 const styles = StyleSheet.create({ 135 133 container: {
+15 -8
src/view/screens/NotFound.tsx
··· 1 1 import React from 'react' 2 - import {Button, View} from 'react-native' 2 + import {Button, StyleSheet, View} from 'react-native' 3 3 import {ViewHeader} from '../com/util/ViewHeader' 4 4 import {Text} from '../com/util/text/Text' 5 5 import {useStores} from '../../state' ··· 9 9 return ( 10 10 <View testID="notFoundView"> 11 11 <ViewHeader title="Page not found" /> 12 - <View 13 - style={{ 14 - justifyContent: 'center', 15 - alignItems: 'center', 16 - paddingTop: 100, 17 - }}> 18 - <Text style={{fontSize: 40, fontWeight: 'bold'}}>Page not found</Text> 12 + <View style={styles.container}> 13 + <Text style={styles.title}>Page not found</Text> 19 14 <Button 20 15 testID="navigateHomeButton" 21 16 title="Home" ··· 25 20 </View> 26 21 ) 27 22 } 23 + 24 + const styles = StyleSheet.create({ 25 + container: { 26 + justifyContent: 'center', 27 + alignItems: 'center', 28 + paddingTop: 100, 29 + }, 30 + title: { 31 + fontSize: 40, 32 + fontWeight: 'bold', 33 + }, 34 + })
+3 -2
src/view/screens/Notifications.tsx
··· 5 5 import {useStores} from '../../state' 6 6 import {ScreenParams} from '../routes' 7 7 import {useOnMainScroll} from '../lib/hooks/useOnMainScroll' 8 + import {s} from '../lib/styles' 8 9 9 10 export const Notifications = ({navIdx, visible}: ScreenParams) => { 10 11 const store = useStores() ··· 24 25 store.me.notifications.updateReadState() 25 26 }) 26 27 store.nav.setTitle(navIdx, 'Notifications') 27 - }, [visible, store]) 28 + }, [visible, store, navIdx]) 28 29 29 30 const onPressTryAgain = () => { 30 31 store.me.notifications.refresh() 31 32 } 32 33 33 34 return ( 34 - <View style={{flex: 1}}> 35 + <View style={s.h100pct}> 35 36 <ViewHeader title="Notifications" canGoBack={false} /> 36 37 <Feed 37 38 view={store.me.notifications}
+10 -3
src/view/screens/Onboard.tsx
··· 1 1 import React, {useEffect} from 'react' 2 - import {View} from 'react-native' 2 + import {StyleSheet, View} from 'react-native' 3 3 import {observer} from 'mobx-react-lite' 4 4 import {FeatureExplainer} from '../com/onboard/FeatureExplainer' 5 5 import {Follows} from '../com/onboard/Follows' ··· 14 14 if (!OnboardStageOrder.includes(store.onboard.stage)) { 15 15 store.onboard.stop() 16 16 } 17 - }, [store.onboard.stage]) 17 + }, [store.onboard]) 18 18 19 19 let Com 20 20 if (store.onboard.stage === OnboardStage.Explainers) { ··· 26 26 } 27 27 28 28 return ( 29 - <View style={{flex: 1, backgroundColor: '#fff'}}> 29 + <View style={styles.container}> 30 30 <Com /> 31 31 </View> 32 32 ) 33 33 }) 34 + 35 + const styles = StyleSheet.create({ 36 + container: { 37 + height: '100%', 38 + backgroundColor: '#fff', 39 + }, 40 + })
+1 -1
src/view/screens/PostDownvotedBy.tsx
··· 16 16 store.nav.setTitle(navIdx, 'Downvoted by') 17 17 store.shell.setMinimalShellMode(false) 18 18 } 19 - }, [store, visible]) 19 + }, [store, visible, navIdx]) 20 20 21 21 return ( 22 22 <View>
+1 -1
src/view/screens/PostRepostedBy.tsx
··· 16 16 store.nav.setTitle(navIdx, 'Reposted by') 17 17 store.shell.setMinimalShellMode(false) 18 18 } 19 - }, [store, visible]) 19 + }, [store, visible, navIdx]) 20 20 21 21 return ( 22 22 <View>
+11 -10
src/view/screens/PostThread.tsx
··· 6 6 import {PostThreadViewModel} from '../../state/models/post-thread-view' 7 7 import {ScreenParams} from '../routes' 8 8 import {useStores} from '../../state' 9 + import {s} from '../lib/styles' 9 10 10 11 export const PostThread = ({navIdx, visible, params}: ScreenParams) => { 11 12 const store = useStores() ··· 14 15 const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) 15 16 const view = useMemo<PostThreadViewModel>( 16 17 () => new PostThreadViewModel(store, {uri}), 17 - [uri], 18 + [store, uri], 18 19 ) 19 20 20 - const setTitle = () => { 21 - const author = view.thread?.author 22 - const niceName = author?.handle || name 23 - setViewSubtitle(`by ${niceName}`) 24 - store.nav.setTitle(navIdx, `Post by ${niceName}`) 25 - } 26 21 useEffect(() => { 27 22 let aborted = false 28 23 const threadCleanup = view.registerListeners() 24 + const setTitle = () => { 25 + const author = view.thread?.post.author 26 + const niceName = author?.handle || name 27 + setViewSubtitle(`by ${niceName}`) 28 + store.nav.setTitle(navIdx, `Post by ${niceName}`) 29 + } 29 30 if (!visible) { 30 31 return threadCleanup 31 32 } ··· 47 48 aborted = true 48 49 threadCleanup() 49 50 } 50 - }, [visible, store.nav, store.log, name]) 51 + }, [visible, store.nav, store.log, store.shell, name, navIdx, view]) 51 52 52 53 return ( 53 - <View style={{flex: 1}}> 54 + <View style={s.h100pct}> 54 55 <ViewHeader title="Post" subtitle={viewSubtitle} /> 55 - <View style={{flex: 1}}> 56 + <View style={s.h100pct}> 56 57 <PostThreadComponent uri={uri} view={view} /> 57 58 </View> 58 59 </View>
+1 -1
src/view/screens/PostUpvotedBy.tsx
··· 15 15 if (visible) { 16 16 store.nav.setTitle(navIdx, 'Liked by') 17 17 } 18 - }, [store, visible]) 18 + }, [store, visible, navIdx]) 19 19 20 20 return ( 21 21 <View>
+14 -5
src/view/screens/Profile.tsx
··· 26 26 const [hasSetup, setHasSetup] = useState<boolean>(false) 27 27 const uiState = React.useMemo( 28 28 () => new ProfileUiModel(store, {user: params.name}), 29 - [params.user], 29 + [params.name, store], 30 30 ) 31 31 32 32 useEffect(() => { 33 + store.nav.setTitle(navIdx, params.name) 34 + }, [store, navIdx, params.name]) 35 + 36 + useEffect(() => { 33 37 let aborted = false 34 38 const feedCleanup = uiState.feed.registerListeners() 35 39 if (!visible) { ··· 38 42 if (hasSetup) { 39 43 uiState.update() 40 44 } else { 41 - store.nav.setTitle(navIdx, params.name) 42 45 uiState.setup().then(() => { 43 46 if (aborted) { 44 47 return ··· 50 53 aborted = true 51 54 feedCleanup() 52 55 } 53 - }, [visible, params.name, store]) 56 + }, [visible, store, hasSetup, uiState]) 54 57 55 58 // events 56 59 // = ··· 139 142 <EmptyState 140 143 icon={['far', 'message']} 141 144 message="No posts yet!" 142 - style={{paddingVertical: 40}} 145 + style={styles.emptyState} 143 146 /> 144 147 ) 145 148 } ··· 187 190 188 191 function LoadingMoreFooter() { 189 192 return ( 190 - <View style={{paddingVertical: 20}}> 193 + <View style={styles.loadingMoreFooter}> 191 194 <ActivityIndicator /> 192 195 </View> 193 196 ) ··· 201 204 loading: { 202 205 paddingVertical: 10, 203 206 paddingHorizontal: 14, 207 + }, 208 + emptyState: { 209 + paddingVertical: 40, 210 + }, 211 + loadingMoreFooter: { 212 + paddingVertical: 20, 204 213 }, 205 214 endItem: { 206 215 paddingTop: 20,
+1 -1
src/view/screens/ProfileFollowers.tsx
··· 14 14 store.nav.setTitle(navIdx, `Followers of ${name}`) 15 15 store.shell.setMinimalShellMode(false) 16 16 } 17 - }, [store, visible, name]) 17 + }, [store, visible, name, navIdx]) 18 18 19 19 return ( 20 20 <View>
+1 -1
src/view/screens/ProfileFollows.tsx
··· 14 14 store.nav.setTitle(navIdx, `Followed by ${name}`) 15 15 store.shell.setMinimalShellMode(false) 16 16 } 17 - }, [store, visible, name]) 17 + }, [store, visible, name, navIdx]) 18 18 19 19 return ( 20 20 <View>
+2 -2
src/view/screens/Search.tsx
··· 25 25 const [query, setQuery] = useState<string>('') 26 26 const autocompleteView = useMemo<UserAutocompleteViewModel>( 27 27 () => new UserAutocompleteViewModel(store), 28 - [], 28 + [store], 29 29 ) 30 30 const {name} = params 31 31 ··· 35 35 autocompleteView.setup() 36 36 store.nav.setTitle(navIdx, 'Search') 37 37 } 38 - }, [store, visible, name]) 38 + }, [store, visible, name, navIdx, autocompleteView]) 39 39 40 40 const onChangeQuery = (text: string) => { 41 41 setQuery(text)
+9 -3
src/view/screens/Settings.tsx
··· 33 33 } 34 34 store.shell.setMinimalShellMode(false) 35 35 store.nav.setTitle(navIdx, 'Settings') 36 - }, [visible, store]) 36 + }, [visible, store, navIdx]) 37 37 38 38 const onPressSwitchAccount = async (acct: AccountData) => { 39 39 setIsSwitching(true) ··· 130 130 style={[ 131 131 pal.view, 132 132 styles.profile, 133 + styles.alignCenter, 133 134 s.mb2, 134 - {alignItems: 'center'}, 135 135 isSwitching && styles.dimmed, 136 136 ]} 137 137 onPress={isSwitching ? undefined : onPressAddAccount}> ··· 142 142 </Text> 143 143 </View> 144 144 </TouchableOpacity> 145 - <View style={{height: 50}} /> 145 + <View style={styles.spacer} /> 146 146 <Text type="sm-medium" style={[s.mb5]}> 147 147 Developer tools 148 148 </Text> ··· 167 167 const styles = StyleSheet.create({ 168 168 dimmed: { 169 169 opacity: 0.5, 170 + }, 171 + spacer: { 172 + height: 50, 173 + }, 174 + alignCenter: { 175 + alignItems: 'center', 170 176 }, 171 177 title: { 172 178 fontSize: 32,
+141 -147
src/view/shell/mobile/Menu.tsx
··· 23 23 import {ToggleButton} from '../../com/util/forms/ToggleButton' 24 24 import {usePalette} from '../../lib/hooks/usePalette' 25 25 26 - export const Menu = observer( 27 - ({visible, onClose}: {visible: boolean; onClose: () => void}) => { 28 - const pal = usePalette('default') 29 - const store = useStores() 26 + export const Menu = observer(({onClose}: {onClose: () => void}) => { 27 + const pal = usePalette('default') 28 + const store = useStores() 30 29 31 - // events 32 - // = 30 + // events 31 + // = 33 32 34 - const onNavigate = (url: string) => { 35 - onClose() 36 - if (url === '/notifications') { 37 - store.nav.switchTo(1, true) 38 - } else { 39 - store.nav.switchTo(0, true) 40 - if (url !== '/') { 41 - store.nav.navigate(url) 42 - } 33 + const onNavigate = (url: string) => { 34 + onClose() 35 + if (url === '/notifications') { 36 + store.nav.switchTo(1, true) 37 + } else { 38 + store.nav.switchTo(0, true) 39 + if (url !== '/') { 40 + store.nav.navigate(url) 43 41 } 44 42 } 45 - 46 - // rendering 47 - // = 43 + } 48 44 49 - const MenuItem = ({ 50 - icon, 51 - label, 52 - count, 53 - url, 54 - bold, 55 - onPress, 56 - }: { 57 - icon: JSX.Element 58 - label: string 59 - count?: number 60 - url?: string 61 - bold?: boolean 62 - onPress?: () => void 63 - }) => ( 64 - <TouchableOpacity 65 - testID={`menuItemButton-${label}`} 66 - style={styles.menuItem} 67 - onPress={onPress ? onPress : () => onNavigate(url || '/')}> 68 - <View style={[styles.menuItemIconWrapper]}> 69 - {icon} 70 - {count ? ( 71 - <View style={styles.menuItemCount}> 72 - <Text style={styles.menuItemCountLabel}>{count}</Text> 73 - </View> 74 - ) : undefined} 75 - </View> 76 - <Text 77 - type="title" 78 - style={[ 79 - pal.text, 80 - bold ? styles.menuItemLabelBold : styles.menuItemLabel, 81 - ]} 82 - numberOfLines={1}> 83 - {label} 84 - </Text> 85 - </TouchableOpacity> 86 - ) 45 + // rendering 46 + // = 87 47 88 - return ( 89 - <ScrollView testID="menuView" style={[styles.view, pal.view]}> 90 - <TouchableOpacity 91 - testID="profileCardButton" 92 - onPress={() => onNavigate(`/profile/${store.me.handle}`)} 93 - style={styles.profileCard}> 94 - <UserAvatar 95 - size={60} 96 - displayName={store.me.displayName} 97 - handle={store.me.handle} 98 - avatar={store.me.avatar} 99 - /> 100 - <View style={s.flex1}> 101 - <Text 102 - type="title-lg" 103 - style={[pal.text, styles.profileCardDisplayName]} 104 - numberOfLines={1}> 105 - {store.me.displayName || store.me.handle} 106 - </Text> 107 - <Text 108 - style={[pal.textLight, styles.profileCardHandle]} 109 - numberOfLines={1}> 110 - @{store.me.handle} 111 - </Text> 48 + const MenuItem = ({ 49 + icon, 50 + label, 51 + count, 52 + url, 53 + bold, 54 + onPress, 55 + }: { 56 + icon: JSX.Element 57 + label: string 58 + count?: number 59 + url?: string 60 + bold?: boolean 61 + onPress?: () => void 62 + }) => ( 63 + <TouchableOpacity 64 + testID={`menuItemButton-${label}`} 65 + style={styles.menuItem} 66 + onPress={onPress ? onPress : () => onNavigate(url || '/')}> 67 + <View style={[styles.menuItemIconWrapper]}> 68 + {icon} 69 + {count ? ( 70 + <View style={styles.menuItemCount}> 71 + <Text style={styles.menuItemCountLabel}>{count}</Text> 112 72 </View> 113 - </TouchableOpacity> 114 - <TouchableOpacity 115 - testID="searchBtn" 116 - style={[styles.searchBtn, pal.btn]} 117 - onPress={() => onNavigate('/search')}> 118 - <MagnifyingGlassIcon 119 - style={pal.text as StyleProp<ViewStyle>} 120 - size={25} 121 - /> 122 - <Text type="title" style={[pal.text, styles.searchBtnLabel]}> 123 - Search 73 + ) : undefined} 74 + </View> 75 + <Text 76 + type="title" 77 + style={[ 78 + pal.text, 79 + bold ? styles.menuItemLabelBold : styles.menuItemLabel, 80 + ]} 81 + numberOfLines={1}> 82 + {label} 83 + </Text> 84 + </TouchableOpacity> 85 + ) 86 + 87 + return ( 88 + <ScrollView testID="menuView" style={[styles.view, pal.view]}> 89 + <TouchableOpacity 90 + testID="profileCardButton" 91 + onPress={() => onNavigate(`/profile/${store.me.handle}`)} 92 + style={styles.profileCard}> 93 + <UserAvatar 94 + size={60} 95 + displayName={store.me.displayName} 96 + handle={store.me.handle} 97 + avatar={store.me.avatar} 98 + /> 99 + <View style={s.flex1}> 100 + <Text 101 + type="title-lg" 102 + style={[pal.text, styles.profileCardDisplayName]} 103 + numberOfLines={1}> 104 + {store.me.displayName || store.me.handle} 124 105 </Text> 125 - </TouchableOpacity> 126 - <View style={[styles.section, pal.border, {paddingTop: 5}]}> 127 - <MenuItem 128 - icon={ 129 - <HomeIcon style={pal.text as StyleProp<ViewStyle>} size="26" /> 130 - } 131 - label="Home" 132 - url="/" 133 - /> 134 - <MenuItem 135 - icon={ 136 - <BellIcon style={pal.text as StyleProp<ViewStyle>} size="28" /> 137 - } 138 - label="Notifications" 139 - url="/notifications" 140 - count={store.me.notificationCount} 141 - /> 142 - <MenuItem 143 - icon={ 144 - <UserIcon 145 - style={pal.text as StyleProp<ViewStyle>} 146 - size="30" 147 - strokeWidth={2} 148 - /> 149 - } 150 - label="Profile" 151 - url={`/profile/${store.me.handle}`} 152 - /> 153 - <MenuItem 154 - icon={ 155 - <CogIcon 156 - style={pal.text as StyleProp<ViewStyle>} 157 - size="30" 158 - strokeWidth={2} 159 - /> 160 - } 161 - label="Settings" 162 - url="/settings" 163 - /> 164 - </View> 165 - <View style={[styles.section, pal.border]}> 166 - <ToggleButton 167 - label="Dark mode" 168 - isSelected={store.shell.darkMode} 169 - onPress={() => store.shell.setDarkMode(!store.shell.darkMode)} 170 - /> 171 - </View> 172 - <View style={styles.footer}> 173 - <Text style={[pal.textLight]}> 174 - Build version {VersionNumber.appVersion} ( 175 - {VersionNumber.buildVersion}) 106 + <Text 107 + style={[pal.textLight, styles.profileCardHandle]} 108 + numberOfLines={1}> 109 + @{store.me.handle} 176 110 </Text> 177 111 </View> 178 - <View style={s.footerSpacer} /> 179 - </ScrollView> 180 - ) 181 - }, 182 - ) 112 + </TouchableOpacity> 113 + <TouchableOpacity 114 + testID="searchBtn" 115 + style={[styles.searchBtn, pal.btn]} 116 + onPress={() => onNavigate('/search')}> 117 + <MagnifyingGlassIcon 118 + style={pal.text as StyleProp<ViewStyle>} 119 + size={25} 120 + /> 121 + <Text type="title" style={[pal.text, styles.searchBtnLabel]}> 122 + Search 123 + </Text> 124 + </TouchableOpacity> 125 + <View style={[styles.section, pal.border, s.pt5]}> 126 + <MenuItem 127 + icon={<HomeIcon style={pal.text as StyleProp<ViewStyle>} size="26" />} 128 + label="Home" 129 + url="/" 130 + /> 131 + <MenuItem 132 + icon={<BellIcon style={pal.text as StyleProp<ViewStyle>} size="28" />} 133 + label="Notifications" 134 + url="/notifications" 135 + count={store.me.notificationCount} 136 + /> 137 + <MenuItem 138 + icon={ 139 + <UserIcon 140 + style={pal.text as StyleProp<ViewStyle>} 141 + size="30" 142 + strokeWidth={2} 143 + /> 144 + } 145 + label="Profile" 146 + url={`/profile/${store.me.handle}`} 147 + /> 148 + <MenuItem 149 + icon={ 150 + <CogIcon 151 + style={pal.text as StyleProp<ViewStyle>} 152 + size="30" 153 + strokeWidth={2} 154 + /> 155 + } 156 + label="Settings" 157 + url="/settings" 158 + /> 159 + </View> 160 + <View style={[styles.section, pal.border]}> 161 + <ToggleButton 162 + label="Dark mode" 163 + isSelected={store.shell.darkMode} 164 + onPress={() => store.shell.setDarkMode(!store.shell.darkMode)} 165 + /> 166 + </View> 167 + <View style={styles.footer}> 168 + <Text style={[pal.textLight]}> 169 + Build version {VersionNumber.appVersion} ({VersionNumber.buildVersion} 170 + ) 171 + </Text> 172 + </View> 173 + <View style={s.footerSpacer} /> 174 + </ScrollView> 175 + ) 176 + }) 183 177 184 178 const styles = StyleSheet.create({ 185 179 view: {
+4 -4
src/view/shell/mobile/index.tsx
··· 32 32 import {ErrorBoundary} from '../../com/util/ErrorBoundary' 33 33 import {TabsSelector} from './TabsSelector' 34 34 import {Composer} from './Composer' 35 - import {colors} from '../../lib/styles' 35 + import {s, colors} from '../../lib/styles' 36 36 import {clamp} from '../../../lib/numbers' 37 37 import { 38 38 GridIcon, ··· 385 385 /> 386 386 <Animated.View 387 387 style={[ 388 - {height: '100%'}, 388 + s.h100pct, 389 389 screenBg, 390 390 current 391 391 ? [ ··· 486 486 */ 487 487 type ScreenRenderDesc = MatchResult & { 488 488 key: string 489 - navIdx: [number, number] 489 + navIdx: string 490 490 current: boolean 491 491 previous: boolean 492 492 isNewTab: boolean ··· 514 514 hasNewTab = hasNewTab || tab.isNewTab 515 515 return Object.assign(matchRes, { 516 516 key: `t${tab.id}-s${screen.index}`, 517 - navIdx: [tab.id, screen.id], 517 + navIdx: `${tab.id}-${screen.id}`, 518 518 current: isCurrent, 519 519 previous: isPrevious, 520 520 isNewTab: tab.isNewTab,