about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/lint.yml4
-rw-r--r--__tests__/state/models/navigation.test.ts4
-rw-r--r--jest/test-pds.ts1
-rw-r--r--metro.config.js2
-rw-r--r--src/App.native.tsx3
-rw-r--r--src/lib/strings.ts22
-rw-r--r--src/state/models/navigation.ts17
-rw-r--r--src/view/com/composer/ComposePost.tsx6
-rw-r--r--src/view/com/discover/SuggestedFollows.tsx6
-rw-r--r--src/view/com/login/Signin.tsx15
-rw-r--r--src/view/com/modals/EditProfile.tsx3
-rw-r--r--src/view/com/modals/ServerInput.tsx2
-rw-r--r--src/view/com/notifications/Feed.tsx19
-rw-r--r--src/view/com/onboard/FeatureExplainer.tsx20
-rw-r--r--src/view/com/post-thread/PostRepostedBy.tsx6
-rw-r--r--src/view/com/post-thread/PostThread.tsx11
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx16
-rw-r--r--src/view/com/post-thread/PostVotedBy.tsx6
-rw-r--r--src/view/com/post/Post.tsx8
-rw-r--r--src/view/com/post/PostText.tsx12
-rw-r--r--src/view/com/posts/Feed.tsx19
-rw-r--r--src/view/com/posts/FeedItem.tsx9
-rw-r--r--src/view/com/profile/ProfileFollowers.tsx6
-rw-r--r--src/view/com/profile/ProfileFollows.tsx6
-rw-r--r--src/view/com/profile/ProfileHeader.tsx20
-rw-r--r--src/view/com/util/Link.tsx2
-rw-r--r--src/view/com/util/LoadingPlaceholder.tsx17
-rw-r--r--src/view/com/util/PostCtrls.tsx13
-rw-r--r--src/view/com/util/PostEmbeds.tsx5
-rw-r--r--src/view/com/util/Selector.tsx2
-rw-r--r--src/view/com/util/UserAvatar.tsx14
-rw-r--r--src/view/com/util/UserInfoText.tsx10
-rw-r--r--src/view/com/util/ViewHeader.tsx31
-rw-r--r--src/view/com/util/ViewSelector.tsx5
-rw-r--r--src/view/com/util/forms/RadioGroup.tsx3
-rw-r--r--src/view/com/util/gestures/HorzSwipe.tsx7
-rw-r--r--src/view/com/util/gestures/SwipeAndZoom.tsx3
-rw-r--r--src/view/com/util/images/AutoSizedImage.tsx4
-rw-r--r--src/view/com/util/images/ImageLayoutGrid.tsx2
-rw-r--r--src/view/lib/styles.ts3
-rw-r--r--src/view/routes.ts2
-rw-r--r--src/view/screens/Contacts.tsx2
-rw-r--r--src/view/screens/Debug.tsx80
-rw-r--r--src/view/screens/Home.tsx11
-rw-r--r--src/view/screens/Log.tsx4
-rw-r--r--src/view/screens/Login.tsx94
-rw-r--r--src/view/screens/NotFound.tsx23
-rw-r--r--src/view/screens/Notifications.tsx5
-rw-r--r--src/view/screens/Onboard.tsx13
-rw-r--r--src/view/screens/PostDownvotedBy.tsx2
-rw-r--r--src/view/screens/PostRepostedBy.tsx2
-rw-r--r--src/view/screens/PostThread.tsx21
-rw-r--r--src/view/screens/PostUpvotedBy.tsx2
-rw-r--r--src/view/screens/Profile.tsx19
-rw-r--r--src/view/screens/ProfileFollowers.tsx2
-rw-r--r--src/view/screens/ProfileFollows.tsx2
-rw-r--r--src/view/screens/Search.tsx4
-rw-r--r--src/view/screens/Settings.tsx12
-rw-r--r--src/view/shell/mobile/Menu.tsx288
-rw-r--r--src/view/shell/mobile/index.tsx8
60 files changed, 478 insertions, 482 deletions
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 54c0ae839..1d9859884 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -25,6 +25,10 @@ jobs:
     name: Run tests
     runs-on: ubuntu-latest
     steps:
+      - name: Install node 18
+        uses: actions/setup-node@v3
+        with:
+          node-version: 18
       - name: Check out Git repository
         uses: actions/checkout@v2
       - name: Yarn install
diff --git a/__tests__/state/models/navigation.test.ts b/__tests__/state/models/navigation.test.ts
index 284f1f36e..80f66d494 100644
--- a/__tests__/state/models/navigation.test.ts
+++ b/__tests__/state/models/navigation.test.ts
@@ -6,7 +6,7 @@ describe('NavigationModel', () => {
 
   beforeEach(() => {
     model = new NavigationModel()
-    model.setTitle([0, 0], 'title')
+    model.setTitle('0-0', 'title')
   })
 
   afterAll(() => {
@@ -44,7 +44,7 @@ describe('NavigationModel', () => {
   })
 
   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', () => {
diff --git a/jest/test-pds.ts b/jest/test-pds.ts
index 4915b58e3..01b37efb0 100644
--- a/jest/test-pds.ts
+++ b/jest/test-pds.ts
@@ -41,7 +41,6 @@ function* dateGen() {
     yield new Date(start).toISOString()
     start += 1e3
   }
-  return ''
 }
 
 export async function createServer(): Promise<TestPDS> {
diff --git a/metro.config.js b/metro.config.js
index 24de9fbdc..c81b3ca13 100644
--- a/metro.config.js
+++ b/metro.config.js
@@ -4,8 +4,6 @@
  *
  * @format
  */
-const metroResolver = require('metro-resolver')
-const path = require('path')
 
 module.exports = {
   transformer: {
diff --git a/src/App.native.tsx b/src/App.native.tsx
index 3cce25548..e9d7542c0 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -10,6 +10,7 @@ import {ThemeProvider} from './view/lib/ThemeContext'
 import * as view from './view/index'
 import {RootStoreModel, setupState, RootStoreProvider} from './state'
 import {MobileShell} from './view/shell/mobile'
+import {s} from './view/lib/styles'
 
 const App = observer(() => {
   const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
@@ -39,7 +40,7 @@ const App = observer(() => {
   }
 
   return (
-    <GestureHandlerRootView style={{flex: 1}}>
+    <GestureHandlerRootView style={s.h100pct}>
       <RootSiblingParent>
         <RootStoreProvider value={rootStore}>
           <ThemeProvider theme={rootStore.shell.darkMode ? 'dark' : 'light'}>
diff --git a/src/lib/strings.ts b/src/lib/strings.ts
index 24b8bcafa..cb79e8824 100644
--- a/src/lib/strings.ts
+++ b/src/lib/strings.ts
@@ -78,7 +78,7 @@ export function extractEntities(
   let ents: Entity[] = []
   {
     // mentions
-    const re = /(^|\s|\()(@)([a-zA-Z0-9\.-]+)(\b)/g
+    const re = /(^|\s|\()(@)([a-zA-Z0-9.-]+)(\b)/g
     while ((match = re.exec(text))) {
       if (knownHandles && !knownHandles.has(match[3])) {
         continue // not a known handle
@@ -133,7 +133,7 @@ interface DetectedLink {
 type DetectedLinkable = string | DetectedLink
 export function detectLinkables(text: string): DetectedLinkable[] {
   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 = []
   let match
   let start = 0
@@ -154,14 +154,12 @@ export function detectLinkables(text: string): DetectedLinkable[] {
       matchValue = matchValue.slice(1)
     }
 
-    {
-      // strip ending puncuation
-      if (/[.,;!?]$/.test(matchValue)) {
-        matchValue = matchValue.slice(0, -1)
-      }
-      if (/[)]$/.test(matchValue) && !matchValue.includes('(')) {
-        matchValue = matchValue.slice(0, -1)
-      }
+    // strip ending puncuation
+    if (/[.,;!?]$/.test(matchValue)) {
+      matchValue = matchValue.slice(0, -1)
+    }
+    if (/[)]$/.test(matchValue) && !matchValue.includes('(')) {
+      matchValue = matchValue.slice(0, -1)
     }
 
     if (start !== matchIndex) {
@@ -185,8 +183,8 @@ export function makeValidHandle(str: string): string {
 }
 
 export function createFullHandle(name: string, domain: string): string {
-  name = (name || '').replace(/[\.]+$/, '')
-  domain = (domain || '').replace(/^[\.]+/, '')
+  name = (name || '').replace(/[.]+$/, '')
+  domain = (domain || '').replace(/^[.]+/, '')
   return `${name}.${domain}`
 }
 
diff --git a/src/state/models/navigation.ts b/src/state/models/navigation.ts
index 8d69e5c04..224ffef0d 100644
--- a/src/state/models/navigation.ts
+++ b/src/state/models/navigation.ts
@@ -3,7 +3,7 @@ import {TABS_ENABLED} from '../../build-flags'
 
 let __id = 0
 function genId() {
-  return ++__id
+  return String(++__id)
 }
 
 // NOTE
@@ -24,10 +24,10 @@ interface HistoryItem {
   url: string
   ts: number
   title?: string
-  id: number
+  id: string
 }
 
-export type HistoryPtr = [number, number]
+export type HistoryPtr = string // `{tabId}-{historyId}`
 
 export class NavigationTabModel {
   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 => {
       if (h.id === id) {
         return {...h, title}
@@ -174,7 +174,7 @@ export class NavigationTabModel {
     }
   }
 
-  hydrate(v: unknown) {
+  hydrate(_v: unknown) {
     // TODO fixme
     // if (isObj(v)) {
     //   if (hasProp(v, 'history') && Array.isArray(v.history)) {
@@ -241,7 +241,7 @@ export class NavigationModel {
     return this.tabs.length
   }
 
-  isCurrentScreen(tabId: number, index: number) {
+  isCurrentScreen(tabId: string, index: number) {
     return this.tab.id === tabId && this.tab.index === index
   }
 
@@ -257,7 +257,8 @@ export class NavigationModel {
   }
 
   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) {
@@ -338,7 +339,7 @@ export class NavigationModel {
     }
   }
 
-  hydrate(v: unknown) {
+  hydrate(_v: unknown) {
     // TODO fixme
     this.clear()
     /*if (isObj(v)) {
diff --git a/src/view/com/composer/ComposePost.tsx b/src/view/com/composer/ComposePost.tsx
index f0dec4b0a..02b7cae5c 100644
--- a/src/view/com/composer/ComposePost.tsx
+++ b/src/view/com/composer/ComposePost.tsx
@@ -297,7 +297,7 @@ export const ComposePost = observer(function ComposePost({
         )
       }
     })
-  }, [text, pal.link])
+  }, [text, pal.link, pal.text])
 
   return (
     <KeyboardAvoidingView
@@ -393,7 +393,7 @@ export const ComposePost = observer(function ComposePost({
                 ref={textInput}
                 multiline
                 scrollEnabled
-                onChangeText={(text: string) => onChangeText(text)}
+                onChangeText={(str: string) => onChangeText(str)}
                 onPaste={onPaste}
                 placeholder={selectTextInputPlaceholder}
                 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) {
   const match = atPrefixRegex.exec(text)
   if (match) {
diff --git a/src/view/com/discover/SuggestedFollows.tsx b/src/view/com/discover/SuggestedFollows.tsx
index 87886c652..d2afc4b33 100644
--- a/src/view/com/discover/SuggestedFollows.tsx
+++ b/src/view/com/discover/SuggestedFollows.tsx
@@ -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
     const view = React.useMemo<SuggestedActorsViewModel>(
       () => new SuggestedActorsViewModel(store),
-      [],
+      [store],
     )
 
     useEffect(() => {
@@ -54,7 +54,7 @@ export const SuggestedFollows = observer(
       if (!view.isLoading && !view.hasError && !view.hasContent) {
         onNoSuggestions?.()
       }
-    }, [view, view.isLoading, view.hasError, view.hasContent])
+    }, [view, view.isLoading, view.hasError, view.hasContent, onNoSuggestions])
 
     const onPressTryAgain = () =>
       view
@@ -128,7 +128,7 @@ export const SuggestedFollows = observer(
               keyExtractor={item => item._reactKey}
               renderItem={renderItem}
               style={s.flex1}
-              contentContainerStyle={{paddingBottom: 200}}
+              contentContainerStyle={s.contentContainer}
             />
           </View>
         )}
diff --git a/src/view/com/login/Signin.tsx b/src/view/com/login/Signin.tsx
index a39ea5e74..f0637db8d 100644
--- a/src/view/com/login/Signin.tsx
+++ b/src/view/com/login/Signin.tsx
@@ -207,12 +207,7 @@ const ChooseAccountForm = ({
         style={[pal.borderDark, styles.group]}
         onPress={() => onSelectAccount(undefined)}>
         <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
-          <View style={s.p10}>
-            <View
-              style={[pal.btn, {width: 30, height: 30, borderRadius: 15}]}
-            />
-          </View>
-          <Text style={styles.accountText}>
+          <Text style={[styles.accountText, styles.accountTextOther]}>
             <Text type="lg" style={pal.text}>
               Other account
             </Text>
@@ -556,7 +551,7 @@ const ForgotPasswordForm = ({
           {!serviceDescription || isProcessing ? (
             <ActivityIndicator />
           ) : !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
             </Text>
           ) : (
@@ -691,7 +686,7 @@ const SetNewPasswordForm = ({
           {isProcessing ? (
             <ActivityIndicator />
           ) : !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
             </Text>
           ) : (
@@ -810,6 +805,9 @@ const styles = StyleSheet.create({
     alignItems: 'baseline',
     paddingVertical: 10,
   },
+  accountTextOther: {
+    paddingLeft: 12,
+  },
   error: {
     backgroundColor: colors.red4,
     flexDirection: 'row',
@@ -832,4 +830,5 @@ const styles = StyleSheet.create({
     justifyContent: 'center',
     marginRight: 5,
   },
+  dimmed: {opacity: 0.5},
 })
diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx
index f830f39e8..ba99feb32 100644
--- a/src/view/com/modals/EditProfile.tsx
+++ b/src/view/com/modals/EditProfile.tsx
@@ -121,7 +121,7 @@ export function Component({
           </View>
         </View>
         {error !== '' && (
-          <View style={{marginTop: 20}}>
+          <View style={styles.errorContainer}>
             <ErrorMessage message={error} />
           </View>
         )}
@@ -231,4 +231,5 @@ const styles = StyleSheet.create({
     marginBottom: 36,
     marginHorizontal: -14,
   },
+  errorContainer: {marginTop: 20},
 })
diff --git a/src/view/com/modals/ServerInput.tsx b/src/view/com/modals/ServerInput.tsx
index 8792d70f1..31ef4b12d 100644
--- a/src/view/com/modals/ServerInput.tsx
+++ b/src/view/com/modals/ServerInput.tsx
@@ -56,7 +56,7 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
         </View>
         <View style={styles.group}>
           <Text style={styles.label}>Other service</Text>
-          <View style={{flexDirection: 'row'}}>
+          <View style={s.flexRow}>
             <BottomSheetTextInput
               testID="customServerTextInput"
               style={styles.textInput}
diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx
index ea7695d93..5f9cb129d 100644
--- a/src/view/com/notifications/Feed.tsx
+++ b/src/view/com/notifications/Feed.tsx
@@ -1,12 +1,13 @@
 import React from 'react'
 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 {FeedItem} from './FeedItem'
 import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import {EmptyState} from '../util/EmptyState'
 import {OnScrollCb} from '../../lib/hooks/useOnMainScroll'
+import {s} from '../../lib/styles'
 
 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
 
@@ -29,7 +30,7 @@ export const Feed = observer(function Feed({
         <EmptyState
           icon="bell"
           message="No notifications yet!"
-          style={{paddingVertical: 40}}
+          style={styles.emptyState}
         />
       )
     }
@@ -58,14 +59,10 @@ export const Feed = observer(function Feed({
     }
   }
   return (
-    <View style={{flex: 1}}>
+    <View style={s.h100pct}>
       {view.isLoading && !data && <NotificationFeedLoadingPlaceholder />}
       {view.hasError && (
-        <ErrorMessage
-          message={view.error}
-          style={{margin: 6}}
-          onPressTryAgain={onPressTryAgain}
-        />
+        <ErrorMessage message={view.error} onPressTryAgain={onPressTryAgain} />
       )}
       {data && (
         <FlatList
@@ -76,9 +73,13 @@ export const Feed = observer(function Feed({
           onRefresh={onRefresh}
           onEndReached={onEndReached}
           onScroll={onScroll}
-          contentContainerStyle={{paddingBottom: 200}}
+          contentContainerStyle={s.contentContainer}
         />
       )}
     </View>
   )
 })
+
+const styles = StyleSheet.create({
+  emptyState: {paddingVertical: 40},
+})
diff --git a/src/view/com/onboard/FeatureExplainer.tsx b/src/view/com/onboard/FeatureExplainer.tsx
index 78ace5189..03e050883 100644
--- a/src/view/com/onboard/FeatureExplainer.tsx
+++ b/src/view/com/onboard/FeatureExplainer.tsx
@@ -19,14 +19,13 @@ import {TABS_ENABLED} from '../../../build-flags'
 const Intro = () => (
   <View style={styles.explainer}>
     <Text
-      style={[
-        styles.explainerHeading,
-        s.normal,
-        {lineHeight: 60, paddingTop: 50, paddingBottom: 50},
-      ]}>
-      Welcome to <Text style={[s.bold, s.blue3, {fontSize: 56}]}>Bluesky</Text>
+      style={[styles.explainerHeading, s.normal, styles.explainerHeadingIntro]}>
+      Welcome to{' '}
+      <Text style={[s.bold, s.blue3, styles.explainerHeadingBrand]}>
+        Bluesky
+      </Text>
     </Text>
-    <Text style={[styles.explainerDesc, {fontSize: 24}]}>
+    <Text style={[styles.explainerDesc, styles.explainerDescIntro]}>
       This is an early beta. Your feedback is appreciated!
     </Text>
   </View>
@@ -161,11 +160,18 @@ const styles = StyleSheet.create({
     textAlign: 'center',
     marginBottom: 16,
   },
+  explainerHeadingIntro: {
+    lineHeight: 60,
+    paddingTop: 50,
+    paddingBottom: 50,
+  },
+  explainerHeadingBrand: {fontSize: 56},
   explainerDesc: {
     fontSize: 18,
     textAlign: 'center',
     marginBottom: 16,
   },
+  explainerDescIntro: {fontSize: 24},
   explainerImg: {
     resizeMode: 'contain',
     maxWidth: '100%',
diff --git a/src/view/com/post-thread/PostRepostedBy.tsx b/src/view/com/post-thread/PostRepostedBy.tsx
index c68ceee0b..02d61b47b 100644
--- a/src/view/com/post-thread/PostRepostedBy.tsx
+++ b/src/view/com/post-thread/PostRepostedBy.tsx
@@ -53,11 +53,7 @@ export const PostRepostedBy = observer(function PostRepostedBy({
   if (view.hasError) {
     return (
       <View>
-        <ErrorMessage
-          message={view.error}
-          style={{margin: 6}}
-          onPressTryAgain={onRefresh}
-        />
+        <ErrorMessage message={view.error} onPressTryAgain={onRefresh} />
       </View>
     )
   }
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index dcdc1eb49..a52bc643c 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -7,6 +7,7 @@ import {
 } from '../../../state/models/post-thread-view'
 import {PostThreadItem} from './PostThreadItem'
 import {ErrorMessage} from '../util/error/ErrorMessage'
+import {s} from '../../lib/styles'
 
 export const PostThread = observer(function PostThread({
   uri,
@@ -60,11 +61,7 @@ export const PostThread = observer(function PostThread({
   if (view.hasError) {
     return (
       <View>
-        <ErrorMessage
-          message={view.error}
-          style={{margin: 6}}
-          onPressTryAgain={onRefresh}
-        />
+        <ErrorMessage message={view.error} onPressTryAgain={onRefresh} />
       </View>
     )
   }
@@ -84,8 +81,8 @@ export const PostThread = observer(function PostThread({
       onRefresh={onRefresh}
       onLayout={onLayout}
       onScrollToIndexFailed={onScrollToIndexFailed}
-      style={{flex: 1}}
-      contentContainerStyle={{paddingBottom: 200}}
+      style={s.h100pct}
+      contentContainerStyle={s.contentContainer}
     />
   )
 })
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 2c7ab716c..92f7acc03 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -80,7 +80,7 @@ export const PostThreadItem = observer(function PostThreadItem({
       .catch(e => store.log.error('Failed to toggle upvote', e))
   }
   const onCopyPostText = () => {
-    Clipboard.setString(record.text)
+    Clipboard.setString(record?.text || '')
     Toast.show('Copied to clipboard')
   }
   const onDeletePost = () => {
@@ -131,8 +131,8 @@ export const PostThreadItem = observer(function PostThreadItem({
               </Link>
             </View>
             <View style={styles.layoutContent}>
-              <View style={[styles.meta, {paddingTop: 5, paddingBottom: 0}]}>
-                <View style={{flexDirection: 'row', alignItems: 'baseline'}}>
+              <View style={[styles.meta, styles.metaExpandedLine1]}>
+                <View style={[s.flexRow, s.alignBaseline]}>
                   <Link
                     style={styles.metaItem}
                     href={authorHref}
@@ -305,10 +305,8 @@ export const PostThreadItem = observer(function PostThreadItem({
                     lineHeight={1.3}
                   />
                 </View>
-              ) : (
-                <View style={{height: 5}} />
-              )}
-              <PostEmbeds embed={item.post.embed} style={{marginBottom: 10}} />
+              ) : undefined}
+              <PostEmbeds embed={item.post.embed} style={s.mb10} />
               <PostCtrls
                 itemHref={itemHref}
                 itemTitle={itemTitle}
@@ -389,6 +387,10 @@ const styles = StyleSheet.create({
     paddingTop: 2,
     paddingBottom: 2,
   },
+  metaExpandedLine1: {
+    paddingTop: 5,
+    paddingBottom: 0,
+  },
   metaItem: {
     paddingRight: 5,
     maxWidth: 240,
diff --git a/src/view/com/post-thread/PostVotedBy.tsx b/src/view/com/post-thread/PostVotedBy.tsx
index 06fe53888..011df4aa1 100644
--- a/src/view/com/post-thread/PostVotedBy.tsx
+++ b/src/view/com/post-thread/PostVotedBy.tsx
@@ -48,11 +48,7 @@ export const PostVotedBy = observer(function PostVotedBy({
   if (view.hasError) {
     return (
       <View>
-        <ErrorMessage
-          message={view.error}
-          style={{margin: 6}}
-          onPressTryAgain={onRefresh}
-        />
+        <ErrorMessage message={view.error} onPressTryAgain={onRefresh} />
       </View>
     )
   }
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index 08e560bda..d00cc83c2 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -156,7 +156,7 @@ export const Post = observer(function Post({
             timestamp={item.post.indexedAt}
           />
           {replyAuthorDid !== '' && (
-            <View style={[s.flexRow, s.mb2, {alignItems: 'center'}]}>
+            <View style={[s.flexRow, s.mb2, s.alignCenter]}>
               <FontAwesomeIcon
                 icon="reply"
                 size={9}
@@ -187,10 +187,8 @@ export const Post = observer(function Post({
                 lineHeight={1.3}
               />
             </View>
-          ) : (
-            <View style={{height: 5}} />
-          )}
-          <PostEmbeds embed={item.post.embed} style={{marginBottom: 10}} />
+          ) : undefined}
+          <PostEmbeds embed={item.post.embed} style={s.mb10} />
           <PostCtrls
             itemHref={itemHref}
             itemTitle={itemTitle}
diff --git a/src/view/com/post/PostText.tsx b/src/view/com/post/PostText.tsx
index 44f9e4d20..0cdc875a9 100644
--- a/src/view/com/post/PostText.tsx
+++ b/src/view/com/post/PostText.tsx
@@ -1,6 +1,6 @@
 import React, {useState, useEffect} from 'react'
 import {observer} from 'mobx-react-lite'
-import {View} from 'react-native'
+import {StyleSheet, View} from 'react-native'
 import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import {Text} from '../util/text/Text'
@@ -31,9 +31,9 @@ export const PostText = observer(function PostText({
   if (!model || model.isLoading || model.uri !== uri) {
     return (
       <View>
-        <LoadingPlaceholder width="100%" height={8} style={{marginTop: 6}} />
-        <LoadingPlaceholder width="100%" height={8} style={{marginTop: 6}} />
-        <LoadingPlaceholder width={100} height={8} style={{marginTop: 6}} />
+        <LoadingPlaceholder width="100%" height={8} style={styles.mt6} />
+        <LoadingPlaceholder width="100%" height={8} style={styles.mt6} />
+        <LoadingPlaceholder width={100} height={8} style={styles.mt6} />
       </View>
     )
   }
@@ -56,3 +56,7 @@ export const PostText = observer(function PostText({
     </View>
   )
 })
+
+const styles = StyleSheet.create({
+  mt6: {marginTop: 6},
+})
diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx
index 726338f81..db6877660 100644
--- a/src/view/com/posts/Feed.tsx
+++ b/src/view/com/posts/Feed.tsx
@@ -5,6 +5,7 @@ import {
   View,
   FlatList,
   StyleProp,
+  StyleSheet,
   ViewStyle,
 } from 'react-native'
 import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
@@ -14,6 +15,7 @@ import {FeedModel} from '../../../state/models/feed-view'
 import {FeedItem} from './FeedItem'
 import {PromptButtons} from './PromptButtons'
 import {OnScrollCb} from '../../lib/hooks/useOnMainScroll'
+import {s} from '../../lib/styles'
 
 const COMPOSE_PROMPT_ITEM = {_reactKey: '__prompt__'}
 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
@@ -47,7 +49,7 @@ export const Feed = observer(function Feed({
         <EmptyState
           icon="bars"
           message="This feed is empty!"
-          style={{paddingVertical: 40}}
+          style={styles.emptyState}
         />
       )
     } else {
@@ -76,7 +78,7 @@ export const Feed = observer(function Feed({
   }
   const FeedFooter = () =>
     feed.isLoading ? (
-      <View style={{paddingTop: 20}}>
+      <View style={styles.feedFooter}>
         <ActivityIndicator />
       </View>
     ) : (
@@ -87,11 +89,7 @@ export const Feed = observer(function Feed({
       {!data && <PromptButtons onPressCompose={onPressCompose} />}
       {feed.isLoading && !data && <PostFeedLoadingPlaceholder />}
       {feed.hasError && (
-        <ErrorMessage
-          message={feed.error}
-          style={{margin: 6}}
-          onPressTryAgain={onPressTryAgain}
-        />
+        <ErrorMessage message={feed.error} onPressTryAgain={onPressTryAgain} />
       )}
       {feed.hasLoaded && data && (
         <FlatList
@@ -101,7 +99,7 @@ export const Feed = observer(function Feed({
           renderItem={renderItem}
           ListFooterComponent={FeedFooter}
           refreshing={feed.isRefreshing}
-          contentContainerStyle={{paddingBottom: 100}}
+          contentContainerStyle={s.contentContainer}
           onScroll={onScroll}
           onRefresh={onRefresh}
           onEndReached={onEndReached}
@@ -110,3 +108,8 @@ export const Feed = observer(function Feed({
     </View>
   )
 })
+
+const styles = StyleSheet.create({
+  feedFooter: {paddingTop: 20},
+  emptyState: {paddingVertical: 40},
+})
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index 4133c17d4..584fa0973 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -124,7 +124,7 @@ export const FeedItem = observer(function ({
             style={[
               styles.bottomReplyLine,
               {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}
             />
             {!isChild && replyAuthorDid !== '' && (
-              <View style={[s.flexRow, s.mb2, {alignItems: 'center'}]}>
+              <View style={[s.flexRow, s.mb2, s.alignCenter]}>
                 <FontAwesomeIcon
                   icon="reply"
                   size={9}
@@ -195,9 +195,7 @@ export const FeedItem = observer(function ({
                   lineHeight={1.3}
                 />
               </View>
-            ) : (
-              <View style={{height: 5}} />
-            )}
+            ) : undefined}
             {item.post.embed ? (
               <PostEmbeds embed={item.post.embed} style={styles.embed} />
             ) : null}
@@ -281,6 +279,7 @@ const styles = StyleSheet.create({
     bottom: 0,
     borderLeftWidth: 2,
   },
+  bottomReplyLineNoTop: {top: 64},
   includeReason: {
     flexDirection: 'row',
     paddingLeft: 50,
diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx
index b1dfbe996..00207c4d2 100644
--- a/src/view/com/profile/ProfileFollowers.tsx
+++ b/src/view/com/profile/ProfileFollowers.tsx
@@ -54,11 +54,7 @@ export const ProfileFollowers = observer(function ProfileFollowers({
   if (view.hasError) {
     return (
       <View>
-        <ErrorMessage
-          message={view.error}
-          style={{margin: 6}}
-          onPressTryAgain={onRefresh}
-        />
+        <ErrorMessage message={view.error} onPressTryAgain={onRefresh} />
       </View>
     )
   }
diff --git a/src/view/com/profile/ProfileFollows.tsx b/src/view/com/profile/ProfileFollows.tsx
index fca12d11b..2e67873c8 100644
--- a/src/view/com/profile/ProfileFollows.tsx
+++ b/src/view/com/profile/ProfileFollows.tsx
@@ -54,11 +54,7 @@ export const ProfileFollows = observer(function ProfileFollows({
   if (view.hasError) {
     return (
       <View>
-        <ErrorMessage
-          message={view.error}
-          style={{margin: 6}}
-          onPressTryAgain={onRefresh}
-        />
+        <ErrorMessage message={view.error} onPressTryAgain={onRefresh} />
       </View>
     )
   }
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index c14a5c827..2f98fce2d 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -100,22 +100,14 @@ export const ProfileHeader = observer(function ProfileHeader({
         <LoadingPlaceholder width="100%" height={120} />
         <View
           style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}>
-          <LoadingPlaceholder
-            width={80}
-            height={80}
-            style={{borderRadius: 40}}
-          />
+          <LoadingPlaceholder width={80} height={80} style={styles.br40} />
         </View>
         <View style={styles.content}>
           <View style={[styles.buttonsLine]}>
-            <LoadingPlaceholder
-              width={100}
-              height={31}
-              style={{borderRadius: 50}}
-            />
+            <LoadingPlaceholder width={100} height={31} style={styles.br50} />
           </View>
           <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}
             </Text>
           </View>
@@ -208,7 +200,7 @@ export const ProfileHeader = observer(function ProfileHeader({
           ) : undefined}
         </View>
         <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}
           </Text>
         </View>
@@ -349,6 +341,7 @@ const styles = StyleSheet.create({
     // paddingLeft: 86,
     // marginBottom: 14,
   },
+  title: {lineHeight: 38},
 
   handleLine: {
     flexDirection: 'row',
@@ -369,4 +362,7 @@ const styles = StyleSheet.create({
     alignItems: 'center',
     marginBottom: 5,
   },
+
+  br40: {borderRadius: 40},
+  br50: {borderRadius: 50},
 })
diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx
index aacdc3272..1cbb1af83 100644
--- a/src/view/com/util/Link.tsx
+++ b/src/view/com/util/Link.tsx
@@ -57,7 +57,7 @@ export const Link = observer(function Link({
   )
 })
 
-export const TextLink = observer(function Link({
+export const TextLink = observer(function TextLink({
   type = 'md',
   style,
   href,
diff --git a/src/view/com/util/LoadingPlaceholder.tsx b/src/view/com/util/LoadingPlaceholder.tsx
index 207a3f5d2..9828058e8 100644
--- a/src/view/com/util/LoadingPlaceholder.tsx
+++ b/src/view/com/util/LoadingPlaceholder.tsx
@@ -19,23 +19,15 @@ export function LoadingPlaceholder({
   return (
     <View
       style={[
+        styles.loadingPlaceholder,
         {
           width,
           height,
           backgroundColor: theme.palette.default.backgroundLight,
-          borderRadius: 6,
-          overflow: 'hidden',
         },
         style,
-      ]}>
-      <View
-        style={{
-          width,
-          height,
-          backgroundColor: theme.palette.default.backgroundLight,
-        }}
-      />
-    </View>
+      ]}
+    />
   )
 }
 
@@ -137,6 +129,9 @@ export function NotificationFeedLoadingPlaceholder() {
 }
 
 const styles = StyleSheet.create({
+  loadingPlaceholder: {
+    borderRadius: 6,
+  },
   post: {
     flexDirection: 'row',
     padding: 10,
diff --git a/src/view/com/util/PostCtrls.tsx b/src/view/com/util/PostCtrls.tsx
index 0ca13b62f..bde44abab 100644
--- a/src/view/com/util/PostCtrls.tsx
+++ b/src/view/com/util/PostCtrls.tsx
@@ -128,10 +128,7 @@ export function PostCtrls(opts: PostCtrlsOpts) {
           hitSlop={HITSLOP}
           onPress={opts.onPressReply}>
           <CommentBottomArrow
-            style={[
-              defaultCtrlColor,
-              opts.big ? {marginTop: 2} : {marginTop: 1},
-            ]}
+            style={[defaultCtrlColor, opts.big ? s.mt2 : styles.mt1]}
             strokeWidth={3}
             size={opts.big ? 20 : 15}
           />
@@ -181,10 +178,7 @@ export function PostCtrls(opts: PostCtrlsOpts) {
               />
             ) : (
               <HeartIcon
-                style={[
-                  defaultCtrlColor,
-                  opts.big ? {marginTop: 1} : undefined,
-                ]}
+                style={[defaultCtrlColor, opts.big ? styles.mt1 : undefined]}
                 strokeWidth={3}
                 size={opts.big ? 20 : 16}
               />
@@ -244,4 +238,7 @@ const styles = StyleSheet.create({
   ctrlIconUpvoted: {
     color: colors.red3,
   },
+  mt1: {
+    marginTop: 1,
+  },
 })
diff --git a/src/view/com/util/PostEmbeds.tsx b/src/view/com/util/PostEmbeds.tsx
index 65518470a..e3fca2538 100644
--- a/src/view/com/util/PostEmbeds.tsx
+++ b/src/view/com/util/PostEmbeds.tsx
@@ -67,7 +67,7 @@ export function PostEmbeds({
             <AutoSizedImage
               uri={embed.images[0].thumb}
               onPress={() => openLightbox(0)}
-              containerStyle={{borderRadius: 8}}
+              containerStyle={styles.singleImage}
             />
           </View>
         )
@@ -120,6 +120,9 @@ const styles = StyleSheet.create({
   imagesContainer: {
     marginTop: 4,
   },
+  singleImage: {
+    borderRadius: 8,
+  },
   extOuter: {
     borderWidth: 1,
     borderRadius: 8,
diff --git a/src/view/com/util/Selector.tsx b/src/view/com/util/Selector.tsx
index 7a8b9b530..87540cf38 100644
--- a/src/view/com/util/Selector.tsx
+++ b/src/view/com/util/Selector.tsx
@@ -41,7 +41,7 @@ export function Selector({
       width: middle.width,
     }
     return [left, middle, right]
-  }, [selectedIndex, items, itemLayouts])
+  }, [selectedIndex, itemLayouts])
 
   const underlineStyle = {
     backgroundColor: pal.colors.text,
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index bd4897ba8..c9c255f46 100644
--- a/src/view/com/util/UserAvatar.tsx
+++ b/src/view/com/util/UserAvatar.tsx
@@ -62,8 +62,8 @@ export function UserAvatar({
     ])
   }, [onSelectNewAvatar])
 
-  const renderSvg = (size: number, initials: string) => (
-    <Svg width={size} height={size} viewBox="0 0 100 100">
+  const renderSvg = (svgSize: number, svgInitials: string) => (
+    <Svg width={svgSize} height={svgSize} viewBox="0 0 100 100">
       <Defs>
         <LinearGradient id="grad" x1="0" y1="0" x2="1" y2="1">
           <Stop offset="0" stopColor={gradients.blue.start} stopOpacity="1" />
@@ -78,7 +78,7 @@ export function UserAvatar({
         x="50"
         y="67"
         textAnchor="middle">
-        {initials}
+        {svgInitials}
       </Text>
     </Svg>
   )
@@ -88,7 +88,11 @@ export function UserAvatar({
     <TouchableOpacity onPress={handleEditAvatar}>
       {avatar ? (
         <Image
-          style={{width: size, height: size, borderRadius: (size / 2) | 0}}
+          style={{
+            width: size,
+            height: size,
+            borderRadius: Math.floor(size / 2),
+          }}
           source={{uri: avatar}}
         />
       ) : (
@@ -104,7 +108,7 @@ export function UserAvatar({
     </TouchableOpacity>
   ) : avatar ? (
     <Image
-      style={{width: size, height: size, borderRadius: (size / 2) | 0}}
+      style={{width: size, height: size, borderRadius: Math.floor(size / 2)}}
       resizeMode="stretch"
       source={{uri: avatar}}
     />
diff --git a/src/view/com/util/UserInfoText.tsx b/src/view/com/util/UserInfoText.tsx
index 151fa54d0..a6daf18b2 100644
--- a/src/view/com/util/UserInfoText.tsx
+++ b/src/view/com/util/UserInfoText.tsx
@@ -1,6 +1,6 @@
 import React, {useState, useEffect} from 'react'
 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 {Text} from './text/Text'
 import {LoadingPlaceholder} from './LoadingPlaceholder'
@@ -53,7 +53,7 @@ export function UserInfoText({
     return () => {
       aborted = true
     }
-  }, [did, store.api.app.bsky])
+  }, [did, store.profiles])
 
   let inner
   if (didFail) {
@@ -73,7 +73,7 @@ export function UserInfoText({
       <LoadingPlaceholder
         width={80}
         height={8}
-        style={{position: 'relative', top: 1, left: 2}}
+        style={styles.loadingPlaceholder}
       />
     )
   }
@@ -91,3 +91,7 @@ export function UserInfoText({
 
   return inner
 }
+
+const styles = StyleSheet.create({
+  loadingPlaceholder: {position: 'relative', top: 1, left: 2},
+})
diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx
index 761553cc5..c8b1b2d97 100644
--- a/src/view/com/util/ViewHeader.tsx
+++ b/src/view/com/util/ViewHeader.tsx
@@ -11,8 +11,8 @@ import {UserAvatar} from './UserAvatar'
 import {Text} from './text/Text'
 import {MagnifyingGlassIcon} from '../../lib/icons'
 import {useStores} from '../../../state'
-import {useTheme} from '../../lib/ThemeContext'
 import {usePalette} from '../../lib/hooks/usePalette'
+import {colors} from '../../lib/styles'
 
 const HITSLOP = {left: 10, top: 10, right: 10, 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
   canGoBack?: boolean
 }) {
-  const theme = useTheme()
   const pal = usePalette('default')
   const store = useStores()
   const onPressBack = () => {
@@ -52,12 +51,12 @@ export const ViewHeader = observer(function ViewHeader({
         testID="viewHeaderBackOrMenuBtn"
         onPress={canGoBack ? onPressBack : onPressMenu}
         hitSlop={BACK_HITSLOP}
-        style={canGoBack ? styles.backIcon : styles.backIconWide}>
+        style={canGoBack ? styles.backBtn : styles.backBtnWide}>
         {canGoBack ? (
           <FontAwesomeIcon
             size={18}
             icon="angle-left"
-            style={[{marginTop: 6}, pal.text]}
+            style={[styles.backIcon, pal.text]}
           />
         ) : (
           <UserAvatar
@@ -96,13 +95,10 @@ export const ViewHeader = observer(function ViewHeader({
               <FontAwesomeIcon icon="signal" style={pal.text} size={16} />
               <FontAwesomeIcon
                 icon="x"
-                style={{
-                  backgroundColor: pal.colors.background,
-                  color: theme.palette.error.background,
-                  position: 'absolute',
-                  right: 7,
-                  bottom: 7,
-                }}
+                style={[
+                  styles.littleXIcon,
+                  {backgroundColor: pal.colors.background},
+                ]}
                 size={8}
               />
             </>
@@ -136,15 +132,18 @@ const styles = StyleSheet.create({
     fontWeight: 'normal',
   },
 
-  backIcon: {
+  backBtn: {
     width: 30,
     height: 30,
   },
-  backIconWide: {
+  backBtnWide: {
     width: 40,
     height: 30,
     marginLeft: 6,
   },
+  backIcon: {
+    marginTop: 6,
+  },
   btn: {
     flexDirection: 'row',
     alignItems: 'center',
@@ -154,4 +153,10 @@ const styles = StyleSheet.create({
     borderRadius: 20,
     marginLeft: 4,
   },
+  littleXIcon: {
+    color: colors.red3,
+    position: 'absolute',
+    right: 7,
+    bottom: 7,
+  },
 })
diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx
index 9ea7bc740..0dd93ec64 100644
--- a/src/view/com/util/ViewSelector.tsx
+++ b/src/view/com/util/ViewSelector.tsx
@@ -5,6 +5,7 @@ import {HorzSwipe} from './gestures/HorzSwipe'
 import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue'
 import {OnScrollCb} from '../../lib/hooks/useOnMainScroll'
 import {clamp} from '../../../lib/numbers'
+import {s} from '../../lib/styles'
 
 const HEADER_ITEM = {_reactKey: '__header__'}
 const SELECTOR_ITEM = {_reactKey: '__selector__'}
@@ -54,7 +55,7 @@ export function ViewSelector({
     setSelectedIndex(clamp(index, 0, sections.length))
   useEffect(() => {
     onSelectView?.(selectedIndex)
-  }, [selectedIndex])
+  }, [selectedIndex, onSelectView])
 
   // rendering
   // =
@@ -98,7 +99,7 @@ export function ViewSelector({
         onScroll={onScroll}
         onRefresh={onRefresh}
         onEndReached={onEndReached}
-        contentContainerStyle={{paddingBottom: 200}}
+        contentContainerStyle={s.contentContainer}
       />
     </HorzSwipe>
   )
diff --git a/src/view/com/util/forms/RadioGroup.tsx b/src/view/com/util/forms/RadioGroup.tsx
index 9abc2345f..b33cd9831 100644
--- a/src/view/com/util/forms/RadioGroup.tsx
+++ b/src/view/com/util/forms/RadioGroup.tsx
@@ -2,6 +2,7 @@ import React, {useState} from 'react'
 import {View} from 'react-native'
 import {RadioButton} from './RadioButton'
 import {ButtonType} from './Button'
+import {s} from '../../../lib/styles'
 
 export interface RadioGroupItem {
   label: string
@@ -29,7 +30,7 @@ export function RadioGroup({
       {items.map((item, i) => (
         <RadioButton
           key={item.key}
-          style={i !== 0 ? {marginTop: 2} : undefined}
+          style={i !== 0 ? s.mt2 : undefined}
           type={type}
           label={item.label}
           isSelected={item.key === selection}
diff --git a/src/view/com/util/gestures/HorzSwipe.tsx b/src/view/com/util/gestures/HorzSwipe.tsx
index 6dcdcf918..22b15afe7 100644
--- a/src/view/com/util/gestures/HorzSwipe.tsx
+++ b/src/view/com/util/gestures/HorzSwipe.tsx
@@ -9,6 +9,7 @@ import {
   View,
 } from 'react-native'
 import {clamp} from 'lodash'
+import {s} from '../../../lib/styles'
 
 interface Props {
   panX: Animated.Value
@@ -111,7 +112,9 @@ export function HorzSwipe({
       (Math.abs(gestureState.dx) > swipeDistanceThreshold / 4 ||
         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, {
         toValue: final,
         duration: 100,
@@ -144,7 +147,7 @@ export function HorzSwipe({
   })
 
   return (
-    <View {...panResponder.panHandlers} style={{flex: 1}}>
+    <View {...panResponder.panHandlers} style={s.h100pct}>
       {children}
     </View>
   )
diff --git a/src/view/com/util/gestures/SwipeAndZoom.tsx b/src/view/com/util/gestures/SwipeAndZoom.tsx
index 881eea094..ee00edab7 100644
--- a/src/view/com/util/gestures/SwipeAndZoom.tsx
+++ b/src/view/com/util/gestures/SwipeAndZoom.tsx
@@ -9,6 +9,7 @@ import {
   View,
 } from 'react-native'
 import {clamp} from 'lodash'
+import {s} from '../../../lib/styles'
 
 export enum Dir {
   None,
@@ -294,7 +295,7 @@ export function SwipeAndZoom({
   })
 
   return (
-    <View {...panResponder.panHandlers} style={{flex: 1}}>
+    <View {...panResponder.panHandlers} style={s.h100pct}>
       {children}
     </View>
   )
diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx
index a711323a9..648bb957f 100644
--- a/src/view/com/util/images/AutoSizedImage.tsx
+++ b/src/view/com/util/images/AutoSizedImage.tsx
@@ -47,9 +47,9 @@ export function AutoSizedImage({
             setImgInfo({width, height})
           }
         },
-        (error: any) => {
+        (err: any) => {
           if (!aborted) {
-            setError(String(error))
+            setError(String(err))
           }
         },
       )
diff --git a/src/view/com/util/images/ImageLayoutGrid.tsx b/src/view/com/util/images/ImageLayoutGrid.tsx
index 5eb5b3c54..8acab7109 100644
--- a/src/view/com/util/images/ImageLayoutGrid.tsx
+++ b/src/view/com/util/images/ImageLayoutGrid.tsx
@@ -105,7 +105,7 @@ function ImageLayoutGridInner({
           <TouchableWithoutFeedback onPress={() => onPress?.(1)}>
             <Image source={{uri: uris[1]}} style={size1} />
           </TouchableWithoutFeedback>
-          <View style={{height: 5}} />
+          <View style={styles.hSpace} />
           <TouchableWithoutFeedback onPress={() => onPress?.(2)}>
             <Image source={{uri: uris[2]}} style={size1} />
           </TouchableWithoutFeedback>
diff --git a/src/view/lib/styles.ts b/src/view/lib/styles.ts
index 0b0145ced..7129867e9 100644
--- a/src/view/lib/styles.ts
+++ b/src/view/lib/styles.ts
@@ -58,6 +58,8 @@ export const gradients = {
 export const s = StyleSheet.create({
   // helpers
   footerSpacer: {height: 100},
+  contentContainer: {paddingBottom: 200},
+  border1: {borderWidth: 1},
 
   // font weights
   fw600: {fontWeight: '600'},
@@ -140,6 +142,7 @@ export const s = StyleSheet.create({
   flexCol: {flexDirection: 'column'},
   flex1: {flex: 1},
   alignCenter: {alignItems: 'center'},
+  alignBaseline: {alignItems: 'baseline'},
 
   // position
   absolute: {position: 'absolute'},
diff --git a/src/view/routes.ts b/src/view/routes.ts
index 908036a41..b5cc014ff 100644
--- a/src/view/routes.ts
+++ b/src/view/routes.ts
@@ -18,7 +18,7 @@ import {Debug} from './screens/Debug'
 import {Log} from './screens/Log'
 
 export type ScreenParams = {
-  navIdx: [number, number]
+  navIdx: string
   params: Record<string, any>
   visible: boolean
   scrollElRef?: MutableRefObject<FlatList<any> | undefined>
diff --git a/src/view/screens/Contacts.tsx b/src/view/screens/Contacts.tsx
index a6cc7244e..cba17f285 100644
--- a/src/view/screens/Contacts.tsx
+++ b/src/view/screens/Contacts.tsx
@@ -17,7 +17,7 @@ export const Contacts = ({navIdx, visible}: ScreenParams) => {
     if (visible) {
       store.nav.setTitle(navIdx, 'Contacts')
     }
-  }, [store, visible])
+  }, [store, visible, navIdx])
 
   const [searchText, onChangeSearchText] = useState('')
   const inputRef = useRef<TextInput | null>(null)
diff --git a/src/view/screens/Debug.tsx b/src/view/screens/Debug.tsx
index f6e2b389c..9365724a0 100644
--- a/src/view/screens/Debug.tsx
+++ b/src/view/screens/Debug.tsx
@@ -4,6 +4,7 @@ import {ViewHeader} from '../com/util/ViewHeader'
 import {ThemeProvider} from '../lib/ThemeContext'
 import {PaletteColorName} from '../lib/ThemeContext'
 import {usePalette} from '../lib/hooks/usePalette'
+import {s} from '../lib/styles'
 
 import {Text} from '../com/util/text/Text'
 import {ViewSelector} from '../com/util/ViewSelector'
@@ -48,7 +49,7 @@ function DebugInner({
   const renderItem = item => {
     return (
       <View>
-        <View style={{paddingTop: 10, paddingHorizontal: 10}}>
+        <View style={[s.pt10, s.pl10, s.pr10]}>
           <ToggleButton
             type="default-light"
             onPress={onToggleColorScheme}
@@ -70,7 +71,7 @@ function DebugInner({
   const items = [{currentView}]
 
   return (
-    <View style={[{flex: 1}, pal.view]}>
+    <View style={[s.h100pct, pal.view]}>
       <ViewHeader title="Debug panel" />
       <ViewSelector
         swipeEnabled
@@ -86,7 +87,7 @@ function DebugInner({
 function Heading({label}: {label: string}) {
   const pal = usePalette('default')
   return (
-    <View style={{paddingTop: 10, paddingBottom: 5}}>
+    <View style={[s.pt10, s.pb5]}>
       <Text type="title-lg" style={pal.text}>
         {label}
       </Text>
@@ -96,7 +97,7 @@ function Heading({label}: {label: string}) {
 
 function BaseView() {
   return (
-    <View style={{paddingHorizontal: 10}}>
+    <View style={[s.pl10, s.pr10]}>
       <Heading label="Typography" />
       <TypographyView />
       <Heading label="Palettes" />
@@ -109,14 +110,14 @@ function BaseView() {
       <EmptyStateView />
       <Heading label="Loading placeholders" />
       <LoadingPlaceholderView />
-      <View style={{height: 200}} />
+      <View style={s.footerSpacer} />
     </View>
   )
 }
 
 function ControlsView() {
   return (
-    <ScrollView style={{paddingHorizontal: 10}}>
+    <ScrollView style={[s.pl10, s.pr10]}>
       <Heading label="Buttons" />
       <ButtonsView />
       <Heading label="Dropdown Buttons" />
@@ -125,15 +126,15 @@ function ControlsView() {
       <ToggleButtonsView />
       <Heading label="Radio Buttons" />
       <RadioButtonsView />
-      <View style={{height: 200}} />
+      <View style={s.footerSpacer} />
     </ScrollView>
   )
 }
 
 function ErrorView() {
   return (
-    <View style={{padding: 10}}>
-      <View style={{marginBottom: 5}}>
+    <View style={s.p10}>
+      <View style={s.mb5}>
         <ErrorScreen
           title="Error screen"
           message="A major error occurred that led the entire screen to fail"
@@ -141,22 +142,22 @@ function ErrorView() {
           onPressTryAgain={() => {}}
         />
       </View>
-      <View style={{marginBottom: 5}}>
+      <View style={s.mb5}>
         <ErrorMessage message="This is an error that occurred while things were being done" />
       </View>
-      <View style={{marginBottom: 5}}>
+      <View style={s.mb5}>
         <ErrorMessage
           message="This is an error that occurred while things were being done"
           numberOfLines={1}
         />
       </View>
-      <View style={{marginBottom: 5}}>
+      <View style={s.mb5}>
         <ErrorMessage
           message="This is an error that occurred while things were being done"
           onPressTryAgain={() => {}}
         />
       </View>
-      <View style={{marginBottom: 5}}>
+      <View style={s.mb5}>
         <ErrorMessage
           message="This is an error that occurred while things were being done"
           onPressTryAgain={() => {}}
@@ -171,16 +172,7 @@ function PaletteView({palette}: {palette: PaletteColorName}) {
   const defaultPal = usePalette('default')
   const pal = usePalette(palette)
   return (
-    <View
-      style={[
-        pal.view,
-        pal.border,
-        {
-          borderWidth: 1,
-          padding: 10,
-          marginBottom: 5,
-        },
-      ]}>
+    <View style={[pal.view, pal.border, s.p10, s.mb5, s.border1]}>
       <Text style={[pal.text]}>{palette} colors</Text>
       <Text style={[pal.textLight]}>Light text</Text>
       <Text style={[pal.link]}>Link text</Text>
@@ -197,21 +189,6 @@ function TypographyView() {
   const pal = usePalette('default')
   return (
     <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]}>
         'xl-thin' lorem ipsum dolor
       </Text>
@@ -300,9 +277,6 @@ function TypographyView() {
       <Text type="button" style={[pal.text]}>
         Button
       </Text>
-      <Text type="overline" style={[pal.text]}>
-        Overline
-      </Text>
     </View>
   )
 }
@@ -325,16 +299,12 @@ function ButtonsView() {
   const buttonStyles = {marginRight: 5}
   return (
     <View style={[defaultPal.view]}>
-      <View
-        style={{
-          flexDirection: 'row',
-          marginBottom: 5,
-        }}>
+      <View style={[s.flexRow, s.mb5]}>
         <Button type="primary" label="Primary solid" style={buttonStyles} />
         <Button type="secondary" label="Secondary solid" style={buttonStyles} />
         <Button type="inverted" label="Inverted solid" style={buttonStyles} />
       </View>
-      <View style={{flexDirection: 'row'}}>
+      <View style={s.flexRow}>
         <Button
           type="primary-outline"
           label="Primary outline"
@@ -346,7 +316,7 @@ function ButtonsView() {
           style={buttonStyles}
         />
       </View>
-      <View style={{flexDirection: 'row'}}>
+      <View style={s.flexRow}>
         <Button
           type="primary-light"
           label="Primary light"
@@ -358,7 +328,7 @@ function ButtonsView() {
           style={buttonStyles}
         />
       </View>
-      <View style={{flexDirection: 'row'}}>
+      <View style={s.flexRow}>
         <Button
           type="default-light"
           label="Default light"
@@ -390,10 +360,7 @@ function DropdownButtonsView() {
   const defaultPal = usePalette('default')
   return (
     <View style={[defaultPal.view]}>
-      <View
-        style={{
-          marginBottom: 5,
-        }}>
+      <View style={s.mb5}>
         <DropdownButton
           type="primary"
           items={DROPDOWN_ITEMS}
@@ -401,10 +368,7 @@ function DropdownButtonsView() {
           label="Primary button"
         />
       </View>
-      <View
-        style={{
-          marginBottom: 5,
-        }}>
+      <View style={s.mb5}>
         <DropdownButton type="bare" items={DROPDOWN_ITEMS} menuWidth={200}>
           <Text>Bare</Text>
         </DropdownButton>
@@ -415,7 +379,7 @@ function DropdownButtonsView() {
 
 function ToggleButtonsView() {
   const defaultPal = usePalette('default')
-  const buttonStyles = {marginBottom: 5}
+  const buttonStyles = s.mb5
   const [isSelected, setIsSelected] = React.useState(false)
   const onToggle = () => setIsSelected(!isSelected)
   return (
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 8c00f4c7c..384ee15e1 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -83,14 +83,14 @@ export const Home = observer(function Home({
   }
 
   return (
-    <View style={s.flex1}>
+    <View style={s.h100pct}>
       <ViewHeader title="Bluesky" subtitle="Private Beta" canGoBack={false} />
       <Feed
         testID="homeFeed"
         key="default"
         feed={store.me.mainFeed}
         scrollElRef={scrollElRef}
-        style={{flex: 1}}
+        style={s.h100pct}
         onPressCompose={onPressCompose}
         onPressTryAgain={onPressTryAgain}
         onScroll={onMainScroll}
@@ -99,9 +99,9 @@ export const Home = observer(function Home({
         <TouchableOpacity
           style={[
             styles.loadLatest,
-            store.shell.minimalShellMode
-              ? {bottom: 35}
-              : {bottom: 60 + clamp(safeAreaInsets.bottom, 15, 30)},
+            !store.shell.minimalShellMode && {
+              bottom: 60 + clamp(safeAreaInsets.bottom, 15, 30),
+            },
           ]}
           onPress={onPressLoadLatest}
           hitSlop={HITSLOP}>
@@ -125,6 +125,7 @@ const styles = StyleSheet.create({
   loadLatest: {
     position: 'absolute',
     left: 20,
+    bottom: 35,
     shadowColor: '#000',
     shadowOpacity: 0.3,
     shadowOffset: {width: 0, height: 1},
diff --git a/src/view/screens/Log.tsx b/src/view/screens/Log.tsx
index 34eed394c..62d79f482 100644
--- a/src/view/screens/Log.tsx
+++ b/src/view/screens/Log.tsx
@@ -21,7 +21,7 @@ export const Log = observer(function Log({navIdx, visible}: ScreenParams) {
     }
     store.shell.setMinimalShellMode(false)
     store.nav.setTitle(navIdx, 'Log')
-  }, [visible, store])
+  }, [visible, store, navIdx])
 
   const toggler = (id: string) => () => {
     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]}>
                     {entry.summary}
                   </Text>
-                  {!!entry.details ? (
+                  {entry.details ? (
                     <FontAwesomeIcon
                       icon={
                         expanded.includes(entry.id) ? 'angle-up' : 'angle-down'
diff --git a/src/view/screens/Login.tsx b/src/view/screens/Login.tsx
index 7d99f1444..accd0f428 100644
--- a/src/view/screens/Login.tsx
+++ b/src/view/screens/Login.tsx
@@ -18,9 +18,9 @@ import {s, colors} from '../lib/styles'
 import {usePalette} from '../lib/hooks/usePalette'
 
 enum ScreenState {
-  SigninOrCreateAccount,
-  Signin,
-  CreateAccount,
+  S_SigninOrCreateAccount,
+  S_Signin,
+  S_CreateAccount,
 }
 
 const SigninOrCreateAccount = ({
@@ -78,58 +78,56 @@ const SigninOrCreateAccount = ({
   )
 }
 
-export const Login = observer(
-  (/*{navigation}: RootTabsScreenProps<'Login'>*/) => {
-    const pal = usePalette('default')
-    const [screenState, setScreenState] = useState<ScreenState>(
-      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>
-      )
-    }
+export const Login = observer(() => {
+  const pal = usePalette('default')
+  const [screenState, setScreenState] = useState<ScreenState>(
+    ScreenState.S_SigninOrCreateAccount,
+  )
 
+  if (screenState === ScreenState.S_SigninOrCreateAccount) {
     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}>
           <ErrorBoundary>
-            {screenState === ScreenState.Signin ? (
-              <Signin
-                onPressBack={() =>
-                  setScreenState(ScreenState.SigninOrCreateAccount)
-                }
-              />
-            ) : undefined}
-            {screenState === ScreenState.CreateAccount ? (
-              <CreateAccount
-                onPressBack={() =>
-                  setScreenState(ScreenState.SigninOrCreateAccount)
-                }
-              />
-            ) : undefined}
+            <SigninOrCreateAccount
+              onPressSignin={() => setScreenState(ScreenState.S_Signin)}
+              onPressCreateAccount={() =>
+                setScreenState(ScreenState.S_CreateAccount)
+              }
+            />
           </ErrorBoundary>
         </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({
   container: {
diff --git a/src/view/screens/NotFound.tsx b/src/view/screens/NotFound.tsx
index 79477fa9b..c5c5ff002 100644
--- a/src/view/screens/NotFound.tsx
+++ b/src/view/screens/NotFound.tsx
@@ -1,5 +1,5 @@
 import React from 'react'
-import {Button, View} from 'react-native'
+import {Button, StyleSheet, View} from 'react-native'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {Text} from '../com/util/text/Text'
 import {useStores} from '../../state'
@@ -9,13 +9,8 @@ export const NotFound = () => {
   return (
     <View testID="notFoundView">
       <ViewHeader title="Page not found" />
-      <View
-        style={{
-          justifyContent: 'center',
-          alignItems: 'center',
-          paddingTop: 100,
-        }}>
-        <Text style={{fontSize: 40, fontWeight: 'bold'}}>Page not found</Text>
+      <View style={styles.container}>
+        <Text style={styles.title}>Page not found</Text>
         <Button
           testID="navigateHomeButton"
           title="Home"
@@ -25,3 +20,15 @@ export const NotFound = () => {
     </View>
   )
 }
+
+const styles = StyleSheet.create({
+  container: {
+    justifyContent: 'center',
+    alignItems: 'center',
+    paddingTop: 100,
+  },
+  title: {
+    fontSize: 40,
+    fontWeight: 'bold',
+  },
+})
diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx
index dd6e07611..9b5dc5970 100644
--- a/src/view/screens/Notifications.tsx
+++ b/src/view/screens/Notifications.tsx
@@ -5,6 +5,7 @@ import {Feed} from '../com/notifications/Feed'
 import {useStores} from '../../state'
 import {ScreenParams} from '../routes'
 import {useOnMainScroll} from '../lib/hooks/useOnMainScroll'
+import {s} from '../lib/styles'
 
 export const Notifications = ({navIdx, visible}: ScreenParams) => {
   const store = useStores()
@@ -24,14 +25,14 @@ export const Notifications = ({navIdx, visible}: ScreenParams) => {
         store.me.notifications.updateReadState()
       })
     store.nav.setTitle(navIdx, 'Notifications')
-  }, [visible, store])
+  }, [visible, store, navIdx])
 
   const onPressTryAgain = () => {
     store.me.notifications.refresh()
   }
 
   return (
-    <View style={{flex: 1}}>
+    <View style={s.h100pct}>
       <ViewHeader title="Notifications" canGoBack={false} />
       <Feed
         view={store.me.notifications}
diff --git a/src/view/screens/Onboard.tsx b/src/view/screens/Onboard.tsx
index 4aa0e6cac..e31b42adc 100644
--- a/src/view/screens/Onboard.tsx
+++ b/src/view/screens/Onboard.tsx
@@ -1,5 +1,5 @@
 import React, {useEffect} from 'react'
-import {View} from 'react-native'
+import {StyleSheet, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
 import {FeatureExplainer} from '../com/onboard/FeatureExplainer'
 import {Follows} from '../com/onboard/Follows'
@@ -14,7 +14,7 @@ export const Onboard = observer(() => {
     if (!OnboardStageOrder.includes(store.onboard.stage)) {
       store.onboard.stop()
     }
-  }, [store.onboard.stage])
+  }, [store.onboard])
 
   let Com
   if (store.onboard.stage === OnboardStage.Explainers) {
@@ -26,8 +26,15 @@ export const Onboard = observer(() => {
   }
 
   return (
-    <View style={{flex: 1, backgroundColor: '#fff'}}>
+    <View style={styles.container}>
       <Com />
     </View>
   )
 })
+
+const styles = StyleSheet.create({
+  container: {
+    height: '100%',
+    backgroundColor: '#fff',
+  },
+})
diff --git a/src/view/screens/PostDownvotedBy.tsx b/src/view/screens/PostDownvotedBy.tsx
index ab110f8f9..1401868d4 100644
--- a/src/view/screens/PostDownvotedBy.tsx
+++ b/src/view/screens/PostDownvotedBy.tsx
@@ -16,7 +16,7 @@ export const PostDownvotedBy = ({navIdx, visible, params}: ScreenParams) => {
       store.nav.setTitle(navIdx, 'Downvoted by')
       store.shell.setMinimalShellMode(false)
     }
-  }, [store, visible])
+  }, [store, visible, navIdx])
 
   return (
     <View>
diff --git a/src/view/screens/PostRepostedBy.tsx b/src/view/screens/PostRepostedBy.tsx
index 4e84617df..bf4d6ec91 100644
--- a/src/view/screens/PostRepostedBy.tsx
+++ b/src/view/screens/PostRepostedBy.tsx
@@ -16,7 +16,7 @@ export const PostRepostedBy = ({navIdx, visible, params}: ScreenParams) => {
       store.nav.setTitle(navIdx, 'Reposted by')
       store.shell.setMinimalShellMode(false)
     }
-  }, [store, visible])
+  }, [store, visible, navIdx])
 
   return (
     <View>
diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx
index c14c93af0..febaddc09 100644
--- a/src/view/screens/PostThread.tsx
+++ b/src/view/screens/PostThread.tsx
@@ -6,6 +6,7 @@ import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread'
 import {PostThreadViewModel} from '../../state/models/post-thread-view'
 import {ScreenParams} from '../routes'
 import {useStores} from '../../state'
+import {s} from '../lib/styles'
 
 export const PostThread = ({navIdx, visible, params}: ScreenParams) => {
   const store = useStores()
@@ -14,18 +15,18 @@ export const PostThread = ({navIdx, visible, params}: ScreenParams) => {
   const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
   const view = useMemo<PostThreadViewModel>(
     () => 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(() => {
     let aborted = false
     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) {
       return threadCleanup
     }
@@ -47,12 +48,12 @@ export const PostThread = ({navIdx, visible, params}: ScreenParams) => {
       aborted = true
       threadCleanup()
     }
-  }, [visible, store.nav, store.log, name])
+  }, [visible, store.nav, store.log, store.shell, name, navIdx, view])
 
   return (
-    <View style={{flex: 1}}>
+    <View style={s.h100pct}>
       <ViewHeader title="Post" subtitle={viewSubtitle} />
-      <View style={{flex: 1}}>
+      <View style={s.h100pct}>
         <PostThreadComponent uri={uri} view={view} />
       </View>
     </View>
diff --git a/src/view/screens/PostUpvotedBy.tsx b/src/view/screens/PostUpvotedBy.tsx
index 7379b852f..4bba222ae 100644
--- a/src/view/screens/PostUpvotedBy.tsx
+++ b/src/view/screens/PostUpvotedBy.tsx
@@ -15,7 +15,7 @@ export const PostUpvotedBy = ({navIdx, visible, params}: ScreenParams) => {
     if (visible) {
       store.nav.setTitle(navIdx, 'Liked by')
     }
-  }, [store, visible])
+  }, [store, visible, navIdx])
 
   return (
     <View>
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index bd60ca61c..7fd813809 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -26,10 +26,14 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
   const [hasSetup, setHasSetup] = useState<boolean>(false)
   const uiState = React.useMemo(
     () => new ProfileUiModel(store, {user: params.name}),
-    [params.user],
+    [params.name, store],
   )
 
   useEffect(() => {
+    store.nav.setTitle(navIdx, params.name)
+  }, [store, navIdx, params.name])
+
+  useEffect(() => {
     let aborted = false
     const feedCleanup = uiState.feed.registerListeners()
     if (!visible) {
@@ -38,7 +42,6 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
     if (hasSetup) {
       uiState.update()
     } else {
-      store.nav.setTitle(navIdx, params.name)
       uiState.setup().then(() => {
         if (aborted) {
           return
@@ -50,7 +53,7 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
       aborted = true
       feedCleanup()
     }
-  }, [visible, params.name, store])
+  }, [visible, store, hasSetup, uiState])
 
   // events
   // =
@@ -139,7 +142,7 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
             <EmptyState
               icon={['far', 'message']}
               message="No posts yet!"
-              style={{paddingVertical: 40}}
+              style={styles.emptyState}
             />
           )
         }
@@ -187,7 +190,7 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
 
 function LoadingMoreFooter() {
   return (
-    <View style={{paddingVertical: 20}}>
+    <View style={styles.loadingMoreFooter}>
       <ActivityIndicator />
     </View>
   )
@@ -202,6 +205,12 @@ const styles = StyleSheet.create({
     paddingVertical: 10,
     paddingHorizontal: 14,
   },
+  emptyState: {
+    paddingVertical: 40,
+  },
+  loadingMoreFooter: {
+    paddingVertical: 20,
+  },
   endItem: {
     paddingTop: 20,
     paddingBottom: 30,
diff --git a/src/view/screens/ProfileFollowers.tsx b/src/view/screens/ProfileFollowers.tsx
index 49b3e2e05..f7520549e 100644
--- a/src/view/screens/ProfileFollowers.tsx
+++ b/src/view/screens/ProfileFollowers.tsx
@@ -14,7 +14,7 @@ export const ProfileFollowers = ({navIdx, visible, params}: ScreenParams) => {
       store.nav.setTitle(navIdx, `Followers of ${name}`)
       store.shell.setMinimalShellMode(false)
     }
-  }, [store, visible, name])
+  }, [store, visible, name, navIdx])
 
   return (
     <View>
diff --git a/src/view/screens/ProfileFollows.tsx b/src/view/screens/ProfileFollows.tsx
index 58df6e76d..65e4004e9 100644
--- a/src/view/screens/ProfileFollows.tsx
+++ b/src/view/screens/ProfileFollows.tsx
@@ -14,7 +14,7 @@ export const ProfileFollows = ({navIdx, visible, params}: ScreenParams) => {
       store.nav.setTitle(navIdx, `Followed by ${name}`)
       store.shell.setMinimalShellMode(false)
     }
-  }, [store, visible, name])
+  }, [store, visible, name, navIdx])
 
   return (
     <View>
diff --git a/src/view/screens/Search.tsx b/src/view/screens/Search.tsx
index 385489c4b..952972222 100644
--- a/src/view/screens/Search.tsx
+++ b/src/view/screens/Search.tsx
@@ -25,7 +25,7 @@ export const Search = ({navIdx, visible, params}: ScreenParams) => {
   const [query, setQuery] = useState<string>('')
   const autocompleteView = useMemo<UserAutocompleteViewModel>(
     () => new UserAutocompleteViewModel(store),
-    [],
+    [store],
   )
   const {name} = params
 
@@ -35,7 +35,7 @@ export const Search = ({navIdx, visible, params}: ScreenParams) => {
       autocompleteView.setup()
       store.nav.setTitle(navIdx, 'Search')
     }
-  }, [store, visible, name])
+  }, [store, visible, name, navIdx, autocompleteView])
 
   const onChangeQuery = (text: string) => {
     setQuery(text)
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index 2c6982685..d659d25d4 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -33,7 +33,7 @@ export const Settings = observer(function Settings({
     }
     store.shell.setMinimalShellMode(false)
     store.nav.setTitle(navIdx, 'Settings')
-  }, [visible, store])
+  }, [visible, store, navIdx])
 
   const onPressSwitchAccount = async (acct: AccountData) => {
     setIsSwitching(true)
@@ -130,8 +130,8 @@ export const Settings = observer(function Settings({
           style={[
             pal.view,
             styles.profile,
+            styles.alignCenter,
             s.mb2,
-            {alignItems: 'center'},
             isSwitching && styles.dimmed,
           ]}
           onPress={isSwitching ? undefined : onPressAddAccount}>
@@ -142,7 +142,7 @@ export const Settings = observer(function Settings({
             </Text>
           </View>
         </TouchableOpacity>
-        <View style={{height: 50}} />
+        <View style={styles.spacer} />
         <Text type="sm-medium" style={[s.mb5]}>
           Developer tools
         </Text>
@@ -168,6 +168,12 @@ const styles = StyleSheet.create({
   dimmed: {
     opacity: 0.5,
   },
+  spacer: {
+    height: 50,
+  },
+  alignCenter: {
+    alignItems: 'center',
+  },
   title: {
     fontSize: 32,
     fontWeight: 'bold',
diff --git a/src/view/shell/mobile/Menu.tsx b/src/view/shell/mobile/Menu.tsx
index 26cb5b9bd..a7d3e2142 100644
--- a/src/view/shell/mobile/Menu.tsx
+++ b/src/view/shell/mobile/Menu.tsx
@@ -23,163 +23,157 @@ import {Text} from '../../com/util/text/Text'
 import {ToggleButton} from '../../com/util/forms/ToggleButton'
 import {usePalette} from '../../lib/hooks/usePalette'
 
-export const Menu = observer(
-  ({visible, onClose}: {visible: boolean; onClose: () => void}) => {
-    const pal = usePalette('default')
-    const store = useStores()
+export const Menu = observer(({onClose}: {onClose: () => void}) => {
+  const pal = usePalette('default')
+  const store = useStores()
 
-    // events
-    // =
+  // events
+  // =
 
-    const onNavigate = (url: string) => {
-      onClose()
-      if (url === '/notifications') {
-        store.nav.switchTo(1, true)
-      } else {
-        store.nav.switchTo(0, true)
-        if (url !== '/') {
-          store.nav.navigate(url)
-        }
+  const onNavigate = (url: string) => {
+    onClose()
+    if (url === '/notifications') {
+      store.nav.switchTo(1, true)
+    } else {
+      store.nav.switchTo(0, true)
+      if (url !== '/') {
+        store.nav.navigate(url)
       }
     }
+  }
 
-    // rendering
-    // =
+  // rendering
+  // =
 
-    const MenuItem = ({
-      icon,
-      label,
-      count,
-      url,
-      bold,
-      onPress,
-    }: {
-      icon: JSX.Element
-      label: string
-      count?: number
-      url?: string
-      bold?: boolean
-      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
-          testID="profileCardButton"
-          onPress={() => onNavigate(`/profile/${store.me.handle}`)}
-          style={styles.profileCard}>
-          <UserAvatar
-            size={60}
-            displayName={store.me.displayName}
-            handle={store.me.handle}
-            avatar={store.me.avatar}
-          />
-          <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>
+  const MenuItem = ({
+    icon,
+    label,
+    count,
+    url,
+    bold,
+    onPress,
+  }: {
+    icon: JSX.Element
+    label: string
+    count?: number
+    url?: string
+    bold?: boolean
+    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>
-        </TouchableOpacity>
-        <TouchableOpacity
-          testID="searchBtn"
-          style={[styles.searchBtn, pal.btn]}
-          onPress={() => onNavigate('/search')}>
-          <MagnifyingGlassIcon
-            style={pal.text as StyleProp<ViewStyle>}
-            size={25}
-          />
-          <Text type="title" style={[pal.text, styles.searchBtnLabel]}>
-            Search
+        ) : 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
+        testID="profileCardButton"
+        onPress={() => onNavigate(`/profile/${store.me.handle}`)}
+        style={styles.profileCard}>
+        <UserAvatar
+          size={60}
+          displayName={store.me.displayName}
+          handle={store.me.handle}
+          avatar={store.me.avatar}
+        />
+        <View style={s.flex1}>
+          <Text
+            type="title-lg"
+            style={[pal.text, styles.profileCardDisplayName]}
+            numberOfLines={1}>
+            {store.me.displayName || store.me.handle}
           </Text>
-        </TouchableOpacity>
-        <View style={[styles.section, pal.border, {paddingTop: 5}]}>
-          <MenuItem
-            icon={
-              <HomeIcon style={pal.text as StyleProp<ViewStyle>} size="26" />
-            }
-            label="Home"
-            url="/"
-          />
-          <MenuItem
-            icon={
-              <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
+            style={[pal.textLight, styles.profileCardHandle]}
+            numberOfLines={1}>
+            @{store.me.handle}
           </Text>
         </View>
-        <View style={s.footerSpacer} />
-      </ScrollView>
-    )
-  },
-)
+      </TouchableOpacity>
+      <TouchableOpacity
+        testID="searchBtn"
+        style={[styles.searchBtn, pal.btn]}
+        onPress={() => onNavigate('/search')}>
+        <MagnifyingGlassIcon
+          style={pal.text as StyleProp<ViewStyle>}
+          size={25}
+        />
+        <Text type="title" style={[pal.text, styles.searchBtnLabel]}>
+          Search
+        </Text>
+      </TouchableOpacity>
+      <View style={[styles.section, pal.border, s.pt5]}>
+        <MenuItem
+          icon={<HomeIcon style={pal.text as StyleProp<ViewStyle>} size="26" />}
+          label="Home"
+          url="/"
+        />
+        <MenuItem
+          icon={<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({
   view: {
diff --git a/src/view/shell/mobile/index.tsx b/src/view/shell/mobile/index.tsx
index fb14211eb..62ab7a2ad 100644
--- a/src/view/shell/mobile/index.tsx
+++ b/src/view/shell/mobile/index.tsx
@@ -32,7 +32,7 @@ import {Text} from '../../com/util/text/Text'
 import {ErrorBoundary} from '../../com/util/ErrorBoundary'
 import {TabsSelector} from './TabsSelector'
 import {Composer} from './Composer'
-import {colors} from '../../lib/styles'
+import {s, colors} from '../../lib/styles'
 import {clamp} from '../../../lib/numbers'
 import {
   GridIcon,
@@ -385,7 +385,7 @@ export const MobileShell: React.FC = observer(() => {
                     />
                     <Animated.View
                       style={[
-                        {height: '100%'},
+                        s.h100pct,
                         screenBg,
                         current
                           ? [
@@ -486,7 +486,7 @@ export const MobileShell: React.FC = observer(() => {
  */
 type ScreenRenderDesc = MatchResult & {
   key: string
-  navIdx: [number, number]
+  navIdx: string
   current: boolean
   previous: boolean
   isNewTab: boolean
@@ -514,7 +514,7 @@ function constructScreenRenderDesc(nav: NavigationModel): {
       hasNewTab = hasNewTab || tab.isNewTab
       return Object.assign(matchRes, {
         key: `t${tab.id}-s${screen.index}`,
-        navIdx: [tab.id, screen.id],
+        navIdx: `${tab.id}-${screen.id}`,
         current: isCurrent,
         previous: isPrevious,
         isNewTab: tab.isNewTab,