about summary refs log tree commit diff
path: root/src/view/com
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com')
-rw-r--r--src/view/com/composer/Autocomplete.tsx4
-rw-r--r--src/view/com/composer/ComposePost.tsx2
-rw-r--r--src/view/com/composer/Prompt.tsx2
-rw-r--r--src/view/com/discover/SuggestedFollows.tsx4
-rw-r--r--src/view/com/lightbox/Lightbox.tsx2
-rw-r--r--src/view/com/login/CreateAccount.tsx2
-rw-r--r--src/view/com/login/Signin.tsx2
-rw-r--r--src/view/com/modals/Confirm.tsx4
-rw-r--r--src/view/com/modals/CreateScene.tsx4
-rw-r--r--src/view/com/modals/EditProfile.tsx4
-rw-r--r--src/view/com/modals/InviteToScene.tsx4
-rw-r--r--src/view/com/modals/ReportAccount.tsx4
-rw-r--r--src/view/com/modals/ReportPost.tsx4
-rw-r--r--src/view/com/modals/ServerInput.tsx2
-rw-r--r--src/view/com/notifications/Feed.tsx5
-rw-r--r--src/view/com/notifications/FeedItem.tsx4
-rw-r--r--src/view/com/notifications/InviteAccepter.tsx2
-rw-r--r--src/view/com/onboard/FeatureExplainer.tsx2
-rw-r--r--src/view/com/onboard/Follows.tsx2
-rw-r--r--src/view/com/post-thread/PostRepostedBy.tsx5
-rw-r--r--src/view/com/post-thread/PostThread.tsx3
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx6
-rw-r--r--src/view/com/post-thread/PostVotedBy.tsx5
-rw-r--r--src/view/com/post/Post.tsx4
-rw-r--r--src/view/com/post/PostText.tsx4
-rw-r--r--src/view/com/posts/Feed.tsx5
-rw-r--r--src/view/com/posts/FeedItem.tsx4
-rw-r--r--src/view/com/profile/ProfileCard.tsx2
-rw-r--r--src/view/com/profile/ProfileFollowers.tsx5
-rw-r--r--src/view/com/profile/ProfileFollows.tsx5
-rw-r--r--src/view/com/profile/ProfileHeader.tsx10
-rw-r--r--src/view/com/profile/ProfileMembers.tsx3
-rw-r--r--src/view/com/util/EmptyState.tsx18
-rw-r--r--src/view/com/util/ErrorMessage.tsx102
-rw-r--r--src/view/com/util/FloatingActionButton.tsx57
-rw-r--r--src/view/com/util/Link.tsx9
-rw-r--r--src/view/com/util/LoadingPlaceholder.tsx25
-rw-r--r--src/view/com/util/Picker.tsx4
-rw-r--r--src/view/com/util/PostCtrls.tsx4
-rw-r--r--src/view/com/util/PostEmbeds.tsx2
-rw-r--r--src/view/com/util/PostMeta.tsx4
-rw-r--r--src/view/com/util/Selector.tsx2
-rw-r--r--src/view/com/util/Text.tsx15
-rw-r--r--src/view/com/util/UserInfoText.tsx2
-rw-r--r--src/view/com/util/ViewHeader.tsx2
-rw-r--r--src/view/com/util/ViewSelector.tsx4
-rw-r--r--src/view/com/util/error/ErrorMessage.tsx76
-rw-r--r--src/view/com/util/error/ErrorScreen.tsx (renamed from src/view/com/util/ErrorScreen.tsx)67
-rw-r--r--src/view/com/util/forms/Button.tsx120
-rw-r--r--src/view/com/util/forms/DropdownButton.tsx (renamed from src/view/com/util/DropdownBtn.tsx)49
-rw-r--r--src/view/com/util/forms/RadioButton.tsx135
-rw-r--r--src/view/com/util/forms/RadioGroup.tsx11
-rw-r--r--src/view/com/util/forms/ToggleButton.tsx165
-rw-r--r--src/view/com/util/images/AutoSizedImage.tsx2
-rw-r--r--src/view/com/util/text/RichText.tsx (renamed from src/view/com/util/RichText.tsx)19
-rw-r--r--src/view/com/util/text/Text.tsx23
56 files changed, 688 insertions, 349 deletions
diff --git a/src/view/com/composer/Autocomplete.tsx b/src/view/com/composer/Autocomplete.tsx
index 1637108f8..b151e0d9e 100644
--- a/src/view/com/composer/Autocomplete.tsx
+++ b/src/view/com/composer/Autocomplete.tsx
@@ -5,8 +5,8 @@ import {
   StyleSheet,
   useWindowDimensions,
 } from 'react-native'
-import {useAnimatedValue} from '../../lib/useAnimatedValue'
-import {Text} from '../util/Text'
+import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue'
+import {Text} from '../util/text/Text'
 import {colors} from '../../lib/styles'
 
 interface AutocompleteItem {
diff --git a/src/view/com/composer/ComposePost.tsx b/src/view/com/composer/ComposePost.tsx
index baa931105..fe310f19b 100644
--- a/src/view/com/composer/ComposePost.tsx
+++ b/src/view/com/composer/ComposePost.tsx
@@ -15,7 +15,7 @@ import LinearGradient from 'react-native-linear-gradient'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {UserAutocompleteViewModel} from '../../../state/models/user-autocomplete-view'
 import {Autocomplete} from './Autocomplete'
-import {Text} from '../util/Text'
+import {Text} from '../util/text/Text'
 import * as Toast from '../util/Toast'
 // @ts-ignore no type definition -prf
 import ProgressCircle from 'react-native-progress/Circle'
diff --git a/src/view/com/composer/Prompt.tsx b/src/view/com/composer/Prompt.tsx
index 2b1559df4..ec63d9501 100644
--- a/src/view/com/composer/Prompt.tsx
+++ b/src/view/com/composer/Prompt.tsx
@@ -3,7 +3,7 @@ import {StyleSheet, TouchableOpacity, View} from 'react-native'
 import {colors} from '../../lib/styles'
 import {useStores} from '../../../state'
 import {UserAvatar} from '../util/UserAvatar'
-import {Text} from '../util/Text'
+import {Text} from '../util/text/Text'
 
 export function ComposePrompt({
   noAvi = false,
diff --git a/src/view/com/discover/SuggestedFollows.tsx b/src/view/com/discover/SuggestedFollows.tsx
index b78bae889..77bd94d5a 100644
--- a/src/view/com/discover/SuggestedFollows.tsx
+++ b/src/view/com/discover/SuggestedFollows.tsx
@@ -10,9 +10,9 @@ import LinearGradient from 'react-native-linear-gradient'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {observer} from 'mobx-react-lite'
 import _omit from 'lodash.omit'
-import {ErrorScreen} from '../util/ErrorScreen'
+import {ErrorScreen} from '../util/error/ErrorScreen'
 import {Link} from '../util/Link'
-import {Text} from '../util/Text'
+import {Text} from '../util/text/Text'
 import {UserAvatar} from '../util/UserAvatar'
 import * as Toast from '../util/Toast'
 import {useStores} from '../../../state'
diff --git a/src/view/com/lightbox/Lightbox.tsx b/src/view/com/lightbox/Lightbox.tsx
index 36c51764f..849354aea 100644
--- a/src/view/com/lightbox/Lightbox.tsx
+++ b/src/view/com/lightbox/Lightbox.tsx
@@ -10,7 +10,7 @@ import {observer} from 'mobx-react-lite'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {SwipeAndZoom, Dir} from '../util/gestures/SwipeAndZoom'
 import {useStores} from '../../../state'
-import {useAnimatedValue} from '../../lib/useAnimatedValue'
+import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue'
 
 import * as models from '../../../state/models/shell-ui'
 
diff --git a/src/view/com/login/CreateAccount.tsx b/src/view/com/login/CreateAccount.tsx
index f97eb7a0f..689a4f384 100644
--- a/src/view/com/login/CreateAccount.tsx
+++ b/src/view/com/login/CreateAccount.tsx
@@ -15,7 +15,7 @@ import * as EmailValidator from 'email-validator'
 import {Logo} from './Logo'
 import {Picker} from '../util/Picker'
 import {TextLink} from '../util/Link'
-import {Text} from '../util/Text'
+import {Text} from '../util/text/Text'
 import {s, colors} from '../../lib/styles'
 import {
   makeValidHandle,
diff --git a/src/view/com/login/Signin.tsx b/src/view/com/login/Signin.tsx
index 45728d3b3..f76507d71 100644
--- a/src/view/com/login/Signin.tsx
+++ b/src/view/com/login/Signin.tsx
@@ -12,7 +12,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import * as EmailValidator from 'email-validator'
 import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api'
 import {Logo} from './Logo'
-import {Text} from '../util/Text'
+import {Text} from '../util/text/Text'
 import {s, colors} from '../../lib/styles'
 import {createFullHandle, toNiceDomain} from '../../../lib/strings'
 import {useStores, RootStoreModel, DEFAULT_SERVICE} from '../../../state'
diff --git a/src/view/com/modals/Confirm.tsx b/src/view/com/modals/Confirm.tsx
index a18043f1a..7545e36a6 100644
--- a/src/view/com/modals/Confirm.tsx
+++ b/src/view/com/modals/Confirm.tsx
@@ -6,10 +6,10 @@ import {
   View,
 } from 'react-native'
 import LinearGradient from 'react-native-linear-gradient'
-import {Text} from '../util/Text'
+import {Text} from '../util/text/Text'
 import {useStores} from '../../../state'
 import {s, colors, gradients} from '../../lib/styles'
-import {ErrorMessage} from '../util/ErrorMessage'
+import {ErrorMessage} from '../util/error/ErrorMessage'
 
 export const snapPoints = ['50%']
 
diff --git a/src/view/com/modals/CreateScene.tsx b/src/view/com/modals/CreateScene.tsx
index 0d47aa4bd..60c240546 100644
--- a/src/view/com/modals/CreateScene.tsx
+++ b/src/view/com/modals/CreateScene.tsx
@@ -9,8 +9,8 @@ import {
 import LinearGradient from 'react-native-linear-gradient'
 import {BottomSheetScrollView, BottomSheetTextInput} from '@gorhom/bottom-sheet'
 import {AppBskyActorCreateScene} from '@atproto/api'
-import {ErrorMessage} from '../util/ErrorMessage'
-import {Text} from '../util/Text'
+import {ErrorMessage} from '../util/error/ErrorMessage'
+import {Text} from '../util/text/Text'
 import {useStores} from '../../../state'
 import {s, colors, gradients} from '../../lib/styles'
 import {
diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx
index bd97ced53..8a3f016ad 100644
--- a/src/view/com/modals/EditProfile.tsx
+++ b/src/view/com/modals/EditProfile.tsx
@@ -9,8 +9,8 @@ import {
 import LinearGradient from 'react-native-linear-gradient'
 import {BottomSheetScrollView, BottomSheetTextInput} from '@gorhom/bottom-sheet'
 import {Image as PickedImage} from 'react-native-image-crop-picker'
-import {Text} from '../util/Text'
-import {ErrorMessage} from '../util/ErrorMessage'
+import {Text} from '../util/text/Text'
+import {ErrorMessage} from '../util/error/ErrorMessage'
 import {useStores} from '../../../state'
 import {ProfileViewModel} from '../../../state/models/profile-view'
 import {s, colors, gradients} from '../../lib/styles'
diff --git a/src/view/com/modals/InviteToScene.tsx b/src/view/com/modals/InviteToScene.tsx
index 28380b6a8..a73440179 100644
--- a/src/view/com/modals/InviteToScene.tsx
+++ b/src/view/com/modals/InviteToScene.tsx
@@ -19,8 +19,8 @@ import {
 import _omit from 'lodash.omit'
 import {AtUri} from '../../../third-party/uri'
 import {ProfileCard} from '../profile/ProfileCard'
-import {ErrorMessage} from '../util/ErrorMessage'
-import {Text} from '../util/Text'
+import {ErrorMessage} from '../util/error/ErrorMessage'
+import {Text} from '../util/text/Text'
 import {useStores} from '../../../state'
 import * as apilib from '../../../state/lib/api'
 import {ProfileViewModel} from '../../../state/models/profile-view'
diff --git a/src/view/com/modals/ReportAccount.tsx b/src/view/com/modals/ReportAccount.tsx
index 582e24238..bf4d5f5a0 100644
--- a/src/view/com/modals/ReportAccount.tsx
+++ b/src/view/com/modals/ReportAccount.tsx
@@ -9,8 +9,8 @@ import LinearGradient from 'react-native-linear-gradient'
 import {useStores} from '../../../state'
 import {s, colors, gradients} from '../../lib/styles'
 import {RadioGroup, RadioGroupItem} from '../util/forms/RadioGroup'
-import {Text} from '../util/Text'
-import {ErrorMessage} from '../util/ErrorMessage'
+import {Text} from '../util/text/Text'
+import {ErrorMessage} from '../util/error/ErrorMessage'
 
 const ITEMS: RadioGroupItem[] = [
   {key: 'spam', label: 'Spam or excessive repeat posts'},
diff --git a/src/view/com/modals/ReportPost.tsx b/src/view/com/modals/ReportPost.tsx
index 6f134032a..d4684069a 100644
--- a/src/view/com/modals/ReportPost.tsx
+++ b/src/view/com/modals/ReportPost.tsx
@@ -9,8 +9,8 @@ import LinearGradient from 'react-native-linear-gradient'
 import {useStores} from '../../../state'
 import {s, colors, gradients} from '../../lib/styles'
 import {RadioGroup, RadioGroupItem} from '../util/forms/RadioGroup'
-import {Text} from '../util/Text'
-import {ErrorMessage} from '../util/ErrorMessage'
+import {Text} from '../util/text/Text'
+import {ErrorMessage} from '../util/error/ErrorMessage'
 
 const ITEMS: RadioGroupItem[] = [
   {key: 'spam', label: 'Spam or excessive repeat posts'},
diff --git a/src/view/com/modals/ServerInput.tsx b/src/view/com/modals/ServerInput.tsx
index 0d1e0e911..884fb91e6 100644
--- a/src/view/com/modals/ServerInput.tsx
+++ b/src/view/com/modals/ServerInput.tsx
@@ -2,7 +2,7 @@ import React, {useState} from 'react'
 import {Platform, StyleSheet, TouchableOpacity, View} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {BottomSheetScrollView, BottomSheetTextInput} from '@gorhom/bottom-sheet'
-import {Text} from '../util/Text'
+import {Text} from '../util/text/Text'
 import {useStores} from '../../../state'
 import {s, colors} from '../../lib/styles'
 import {
diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx
index c986bca57..91a01db4d 100644
--- a/src/view/com/notifications/Feed.tsx
+++ b/src/view/com/notifications/Feed.tsx
@@ -4,9 +4,9 @@ import {View, FlatList} from 'react-native'
 import {NotificationsViewModel} from '../../../state/models/notifications-view'
 import {FeedItem} from './FeedItem'
 import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
-import {ErrorMessage} from '../util/ErrorMessage'
+import {ErrorMessage} from '../util/error/ErrorMessage'
 import {EmptyState} from '../util/EmptyState'
-import {OnScrollCb} from '../../lib/useOnMainScroll'
+import {OnScrollCb} from '../../lib/hooks/useOnMainScroll'
 
 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
 
@@ -54,7 +54,6 @@ export const Feed = observer(function Feed({
       {view.isLoading && !data && <NotificationFeedLoadingPlaceholder />}
       {view.hasError && (
         <ErrorMessage
-          dark
           message={view.error}
           style={{margin: 6}}
           onPressTryAgain={onPressTryAgain}
diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index 16374a9f9..c6b286cdb 100644
--- a/src/view/com/notifications/FeedItem.tsx
+++ b/src/view/com/notifications/FeedItem.tsx
@@ -8,9 +8,9 @@ import {PostThreadViewModel} from '../../../state/models/post-thread-view'
 import {s, colors} from '../../lib/styles'
 import {ago, pluralize} from '../../../lib/strings'
 import {UpIconSolid} from '../../lib/icons'
-import {Text} from '../util/Text'
+import {Text} from '../util/text/Text'
 import {UserAvatar} from '../util/UserAvatar'
-import {ErrorMessage} from '../util/ErrorMessage'
+import {ErrorMessage} from '../util/error/ErrorMessage'
 import {Post} from '../post/Post'
 import {Link} from '../util/Link'
 import {InviteAccepter} from './InviteAccepter'
diff --git a/src/view/com/notifications/InviteAccepter.tsx b/src/view/com/notifications/InviteAccepter.tsx
index 4df8b2e4f..eefe7a273 100644
--- a/src/view/com/notifications/InviteAccepter.tsx
+++ b/src/view/com/notifications/InviteAccepter.tsx
@@ -8,7 +8,7 @@ import {ConfirmModal} from '../../../state/models/shell-ui'
 import {useStores} from '../../../state'
 import {ProfileCard} from '../profile/ProfileCard'
 import * as Toast from '../util/Toast'
-import {Text} from '../util/Text'
+import {Text} from '../util/text/Text'
 import {s, colors, gradients} from '../../lib/styles'
 
 export function InviteAccepter({item}: {item: NotificationsViewItemModel}) {
diff --git a/src/view/com/onboard/FeatureExplainer.tsx b/src/view/com/onboard/FeatureExplainer.tsx
index 31863ad50..ecc1b9692 100644
--- a/src/view/com/onboard/FeatureExplainer.tsx
+++ b/src/view/com/onboard/FeatureExplainer.tsx
@@ -10,7 +10,7 @@ import {
 } from 'react-native'
 import {TabView, SceneMap, Route, TabBarProps} from 'react-native-tab-view'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {Text} from '../util/Text'
+import {Text} from '../util/text/Text'
 import {UserGroupIcon} from '../../lib/icons'
 import {useStores} from '../../../state'
 import {s} from '../../lib/styles'
diff --git a/src/view/com/onboard/Follows.tsx b/src/view/com/onboard/Follows.tsx
index 4026c879a..76eff3f4b 100644
--- a/src/view/com/onboard/Follows.tsx
+++ b/src/view/com/onboard/Follows.tsx
@@ -2,7 +2,7 @@ import React from 'react'
 import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
 import {SuggestedFollows} from '../discover/SuggestedFollows'
-import {Text} from '../util/Text'
+import {Text} from '../util/text/Text'
 import {useStores} from '../../../state'
 import {s} from '../../lib/styles'
 
diff --git a/src/view/com/post-thread/PostRepostedBy.tsx b/src/view/com/post-thread/PostRepostedBy.tsx
index 6328b34e7..0efdfe2e4 100644
--- a/src/view/com/post-thread/PostRepostedBy.tsx
+++ b/src/view/com/post-thread/PostRepostedBy.tsx
@@ -6,9 +6,9 @@ import {
   RepostedByViewItemModel,
 } from '../../../state/models/reposted-by-view'
 import {UserAvatar} from '../util/UserAvatar'
-import {ErrorMessage} from '../util/ErrorMessage'
+import {ErrorMessage} from '../util/error/ErrorMessage'
 import {Link} from '../util/Link'
-import {Text} from '../util/Text'
+import {Text} from '../util/text/Text'
 import {useStores} from '../../../state'
 import {s, colors} from '../../lib/styles'
 
@@ -57,7 +57,6 @@ export const PostRepostedBy = observer(function PostRepostedBy({
     return (
       <View>
         <ErrorMessage
-          dark
           message={view.error}
           style={{margin: 6}}
           onPressTryAgain={onRefresh}
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index ecc0d48f5..8c22cc8b7 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -6,7 +6,7 @@ import {
   PostThreadViewPostModel,
 } from '../../../state/models/post-thread-view'
 import {PostThreadItem} from './PostThreadItem'
-import {ErrorMessage} from '../util/ErrorMessage'
+import {ErrorMessage} from '../util/error/ErrorMessage'
 
 export const PostThread = observer(function PostThread({
   uri,
@@ -57,7 +57,6 @@ export const PostThread = observer(function PostThread({
     return (
       <View>
         <ErrorMessage
-          dark
           message={view.error}
           style={{margin: 6}}
           onPressTryAgain={onRefresh}
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 1a0b0ff87..c39bebbbe 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -7,9 +7,9 @@ import {AppBskyFeedPost} from '@atproto/api'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {PostThreadViewPostModel} from '../../../state/models/post-thread-view'
 import {Link} from '../util/Link'
-import {RichText} from '../util/RichText'
-import {Text} from '../util/Text'
-import {PostDropdownBtn} from '../util/DropdownBtn'
+import {RichText} from '../util/text/RichText'
+import {Text} from '../util/text/Text'
+import {PostDropdownBtn} from '../util/forms/DropdownButton'
 import * as Toast from '../util/Toast'
 import {UserAvatar} from '../util/UserAvatar'
 import {s, colors} from '../../lib/styles'
diff --git a/src/view/com/post-thread/PostVotedBy.tsx b/src/view/com/post-thread/PostVotedBy.tsx
index f3773e47b..96a335919 100644
--- a/src/view/com/post-thread/PostVotedBy.tsx
+++ b/src/view/com/post-thread/PostVotedBy.tsx
@@ -6,8 +6,8 @@ import {
   VotesViewItemModel,
 } from '../../../state/models/votes-view'
 import {Link} from '../util/Link'
-import {Text} from '../util/Text'
-import {ErrorMessage} from '../util/ErrorMessage'
+import {Text} from '../util/text/Text'
+import {ErrorMessage} from '../util/error/ErrorMessage'
 import {UserAvatar} from '../util/UserAvatar'
 import {useStores} from '../../../state'
 import {s, colors} from '../../lib/styles'
@@ -57,7 +57,6 @@ export const PostVotedBy = observer(function PostVotedBy({
     return (
       <View>
         <ErrorMessage
-          dark
           message={view.error}
           style={{margin: 6}}
           onPressTryAgain={onRefresh}
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index d9cc94315..a058acf8e 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -17,8 +17,8 @@ import {UserInfoText} from '../util/UserInfoText'
 import {PostMeta} from '../util/PostMeta'
 import {PostEmbeds} from '../util/PostEmbeds'
 import {PostCtrls} from '../util/PostCtrls'
-import {Text} from '../util/Text'
-import {RichText} from '../util/RichText'
+import {Text} from '../util/text/Text'
+import {RichText} from '../util/text/RichText'
 import * as Toast from '../util/Toast'
 import {UserAvatar} from '../util/UserAvatar'
 import {useStores} from '../../../state'
diff --git a/src/view/com/post/PostText.tsx b/src/view/com/post/PostText.tsx
index 90765a836..436768292 100644
--- a/src/view/com/post/PostText.tsx
+++ b/src/view/com/post/PostText.tsx
@@ -2,8 +2,8 @@ import React, {useState, useEffect} from 'react'
 import {observer} from 'mobx-react-lite'
 import {View} from 'react-native'
 import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
-import {ErrorMessage} from '../util/ErrorMessage'
-import {Text} from '../util/Text'
+import {ErrorMessage} from '../util/error/ErrorMessage'
+import {Text} from '../util/text/Text'
 import {PostModel} from '../../../state/models/post'
 import {useStores} from '../../../state'
 
diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx
index e34513794..02141acef 100644
--- a/src/view/com/posts/Feed.tsx
+++ b/src/view/com/posts/Feed.tsx
@@ -9,11 +9,11 @@ import {
 } from 'react-native'
 import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
 import {EmptyState} from '../util/EmptyState'
-import {ErrorMessage} from '../util/ErrorMessage'
+import {ErrorMessage} from '../util/error/ErrorMessage'
 import {FeedModel} from '../../../state/models/feed-view'
 import {FeedItem} from './FeedItem'
 import {ComposePrompt} from '../composer/Prompt'
-import {OnScrollCb} from '../../lib/useOnMainScroll'
+import {OnScrollCb} from '../../lib/hooks/useOnMainScroll'
 
 const COMPOSE_PROMPT_ITEM = {_reactKey: '__prompt__'}
 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
@@ -80,7 +80,6 @@ export const Feed = observer(function Feed({
       {feed.isLoading && !data && <PostFeedLoadingPlaceholder />}
       {feed.hasError && (
         <ErrorMessage
-          dark
           message={feed.error}
           style={{margin: 6}}
           onPressTryAgain={onPressTryAgain}
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index 9fd9d46b2..8670ed9a3 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -8,12 +8,12 @@ import {AppBskyFeedPost} from '@atproto/api'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {FeedItemModel} from '../../../state/models/feed-view'
 import {Link} from '../util/Link'
-import {Text} from '../util/Text'
+import {Text} from '../util/text/Text'
 import {UserInfoText} from '../util/UserInfoText'
 import {PostMeta} from '../util/PostMeta'
 import {PostCtrls} from '../util/PostCtrls'
 import {PostEmbeds} from '../util/PostEmbeds'
-import {RichText} from '../util/RichText'
+import {RichText} from '../util/text/RichText'
 import * as Toast from '../util/Toast'
 import {UserAvatar} from '../util/UserAvatar'
 import {s, colors} from '../../lib/styles'
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index 1d5b93a4c..6ce5f29e5 100644
--- a/src/view/com/profile/ProfileCard.tsx
+++ b/src/view/com/profile/ProfileCard.tsx
@@ -1,7 +1,7 @@
 import React from 'react'
 import {StyleSheet, TouchableOpacity, View} from 'react-native'
 import {Link} from '../util/Link'
-import {Text} from '../util/Text'
+import {Text} from '../util/text/Text'
 import {UserAvatar} from '../util/UserAvatar'
 import {s, colors} from '../../lib/styles'
 
diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx
index 280173f9e..0d088d7a3 100644
--- a/src/view/com/profile/ProfileFollowers.tsx
+++ b/src/view/com/profile/ProfileFollowers.tsx
@@ -6,8 +6,8 @@ import {
   FollowerItem,
 } from '../../../state/models/user-followers-view'
 import {Link} from '../util/Link'
-import {Text} from '../util/Text'
-import {ErrorMessage} from '../util/ErrorMessage'
+import {Text} from '../util/text/Text'
+import {ErrorMessage} from '../util/error/ErrorMessage'
 import {UserAvatar} from '../util/UserAvatar'
 import {useStores} from '../../../state'
 import {s, colors} from '../../lib/styles'
@@ -57,7 +57,6 @@ export const ProfileFollowers = observer(function ProfileFollowers({
     return (
       <View>
         <ErrorMessage
-          dark
           message={view.error}
           style={{margin: 6}}
           onPressTryAgain={onRefresh}
diff --git a/src/view/com/profile/ProfileFollows.tsx b/src/view/com/profile/ProfileFollows.tsx
index 5f8cdd68c..2cd471b05 100644
--- a/src/view/com/profile/ProfileFollows.tsx
+++ b/src/view/com/profile/ProfileFollows.tsx
@@ -7,8 +7,8 @@ import {
 } from '../../../state/models/user-follows-view'
 import {useStores} from '../../../state'
 import {Link} from '../util/Link'
-import {Text} from '../util/Text'
-import {ErrorMessage} from '../util/ErrorMessage'
+import {Text} from '../util/text/Text'
+import {ErrorMessage} from '../util/error/ErrorMessage'
 import {UserAvatar} from '../util/UserAvatar'
 import {s, colors} from '../../lib/styles'
 
@@ -57,7 +57,6 @@ export const ProfileFollows = observer(function ProfileFollows({
     return (
       <View>
         <ErrorMessage
-          dark
           message={view.error}
           style={{margin: 6}}
           onPressTryAgain={onRefresh}
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index c9da0d96c..131c82b75 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -16,11 +16,11 @@ import {
 import {pluralize} from '../../../lib/strings'
 import {s, colors} from '../../lib/styles'
 import {getGradient} from '../../lib/asset-gen'
-import {DropdownBtn, DropdownItem} from '../util/DropdownBtn'
+import {DropdownButton, DropdownItem} from '../util/forms/DropdownButton'
 import * as Toast from '../util/Toast'
 import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
-import {Text} from '../util/Text'
-import {RichText} from '../util/RichText'
+import {Text} from '../util/text/Text'
+import {RichText} from '../util/text/RichText'
 import {UserAvatar} from '../util/UserAvatar'
 import {UserBanner} from '../util/UserBanner'
 import {UserInfoText} from '../util/UserInfoText'
@@ -195,11 +195,11 @@ export const ProfileHeader = observer(function ProfileHeader({
             </>
           )}
           {dropdownItems?.length ? (
-            <DropdownBtn
+            <DropdownButton
               items={dropdownItems}
               style={[styles.btn, styles.secondaryBtn]}>
               <FontAwesomeIcon icon="ellipsis" style={[s.gray5]} />
-            </DropdownBtn>
+            </DropdownButton>
           ) : undefined}
         </View>
         <View style={styles.displayNameLine}>
diff --git a/src/view/com/profile/ProfileMembers.tsx b/src/view/com/profile/ProfileMembers.tsx
index 251ece41a..0e34865b9 100644
--- a/src/view/com/profile/ProfileMembers.tsx
+++ b/src/view/com/profile/ProfileMembers.tsx
@@ -3,7 +3,7 @@ import {observer} from 'mobx-react-lite'
 import {ActivityIndicator, FlatList, View} from 'react-native'
 import {MembersViewModel, MemberItem} from '../../../state/models/members-view'
 import {ProfileCard} from './ProfileCard'
-import {ErrorMessage} from '../util/ErrorMessage'
+import {ErrorMessage} from '../util/error/ErrorMessage'
 import {useStores} from '../../../state'
 
 export const ProfileMembers = observer(function ProfileMembers({
@@ -49,7 +49,6 @@ export const ProfileMembers = observer(function ProfileMembers({
     return (
       <View>
         <ErrorMessage
-          dark
           message={view.error}
           style={{margin: 6}}
           onPressTryAgain={onRefresh}
diff --git a/src/view/com/util/EmptyState.tsx b/src/view/com/util/EmptyState.tsx
index 8d98807e3..d9a317fae 100644
--- a/src/view/com/util/EmptyState.tsx
+++ b/src/view/com/util/EmptyState.tsx
@@ -2,9 +2,9 @@ import React from 'react'
 import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
 import {IconProp} from '@fortawesome/fontawesome-svg-core'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {Text} from './Text'
+import {Text} from './text/Text'
 import {UserGroupIcon} from '../../lib/icons'
-import {colors} from '../../lib/styles'
+import {usePalette} from '../../lib/hooks/usePalette'
 
 export function EmptyState({
   icon,
@@ -15,16 +15,23 @@ export function EmptyState({
   message: string
   style?: StyleProp<ViewStyle>
 }) {
+  const pal = usePalette('default')
   return (
     <View style={[styles.container, style]}>
       <View style={styles.iconContainer}>
         {icon === 'user-group' ? (
           <UserGroupIcon size="64" style={styles.icon} />
         ) : (
-          <FontAwesomeIcon icon={icon} size={64} style={styles.icon} />
+          <FontAwesomeIcon
+            icon={icon}
+            size={64}
+            style={[styles.icon, pal.textLight]}
+          />
         )}
       </View>
-      <Text style={styles.text}>{message}</Text>
+      <Text type="body1" style={[pal.textLight, styles.text]}>
+        {message}
+      </Text>
     </View>
   )
 }
@@ -40,12 +47,9 @@ const styles = StyleSheet.create({
   icon: {
     marginLeft: 'auto',
     marginRight: 'auto',
-    color: colors.gray3,
   },
   text: {
     textAlign: 'center',
-    color: colors.gray5,
     paddingTop: 16,
-    fontSize: 16,
   },
 })
diff --git a/src/view/com/util/ErrorMessage.tsx b/src/view/com/util/ErrorMessage.tsx
deleted file mode 100644
index b87b77baa..000000000
--- a/src/view/com/util/ErrorMessage.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-import React from 'react'
-import {
-  StyleSheet,
-  TouchableOpacity,
-  StyleProp,
-  View,
-  ViewStyle,
-} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import LinearGradient from 'react-native-linear-gradient'
-import {Text} from './Text'
-import {colors, gradients} from '../../lib/styles'
-
-export function ErrorMessage({
-  message,
-  numberOfLines,
-  dark,
-  style,
-  onPressTryAgain,
-}: {
-  message: string
-  numberOfLines?: number
-  dark?: boolean
-  style?: StyleProp<ViewStyle>
-  onPressTryAgain?: () => void
-}) {
-  const inner = (
-    <>
-      <View style={[styles.errorIcon, dark ? styles.darkErrorIcon : undefined]}>
-        <FontAwesomeIcon
-          icon="exclamation"
-          style={{color: dark ? colors.red3 : colors.white}}
-          size={16}
-        />
-      </View>
-      <Text
-        style={[styles.message, dark ? styles.darkMessage : undefined]}
-        numberOfLines={numberOfLines}>
-        {message}
-      </Text>
-      {onPressTryAgain && (
-        <TouchableOpacity style={styles.btn} onPress={onPressTryAgain}>
-          <FontAwesomeIcon
-            icon="arrows-rotate"
-            style={{color: dark ? colors.white : colors.red4}}
-            size={16}
-          />
-        </TouchableOpacity>
-      )}
-    </>
-  )
-  if (dark) {
-    return (
-      <LinearGradient
-        colors={[gradients.error.start, gradients.error.end]}
-        start={{x: 0.5, y: 0}}
-        end={{x: 1, y: 1}}
-        style={[styles.outer, style]}>
-        {inner}
-      </LinearGradient>
-    )
-  }
-  return <View style={[styles.outer, style]}>{inner}</View>
-}
-
-const styles = StyleSheet.create({
-  outer: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    backgroundColor: colors.red1,
-    borderWidth: 1,
-    borderColor: colors.red3,
-    borderRadius: 6,
-    paddingVertical: 8,
-    paddingHorizontal: 8,
-  },
-  errorIcon: {
-    backgroundColor: colors.red4,
-    borderRadius: 12,
-    width: 24,
-    height: 24,
-    alignItems: 'center',
-    justifyContent: 'center',
-    marginRight: 8,
-  },
-  darkErrorIcon: {
-    backgroundColor: colors.white,
-  },
-  message: {
-    flex: 1,
-    color: colors.red4,
-    paddingRight: 10,
-  },
-  darkMessage: {
-    color: colors.white,
-    fontWeight: '600',
-  },
-  btn: {
-    paddingHorizontal: 4,
-    paddingVertical: 4,
-  },
-})
diff --git a/src/view/com/util/FloatingActionButton.tsx b/src/view/com/util/FloatingActionButton.tsx
deleted file mode 100644
index 21c4fba6c..000000000
--- a/src/view/com/util/FloatingActionButton.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import React from 'react'
-import {
-  GestureResponderEvent,
-  StyleSheet,
-  TouchableWithoutFeedback,
-  View,
-} from 'react-native'
-import LinearGradient from 'react-native-linear-gradient'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {IconProp} from '@fortawesome/fontawesome-svg-core'
-import {colors, gradients} from '../../lib/styles'
-import * as zIndex from '../../lib/z-index'
-
-type OnPress = ((event: GestureResponderEvent) => void) | undefined
-export function FAB({icon, onPress}: {icon: IconProp; onPress: OnPress}) {
-  return (
-    <TouchableWithoutFeedback onPress={onPress}>
-      <View style={styles.outer}>
-        <LinearGradient
-          colors={[gradients.purple.start, gradients.purple.end]}
-          start={{x: 0, y: 0}}
-          end={{x: 1, y: 1}}
-          style={styles.inner}>
-          <FontAwesomeIcon
-            size={24}
-            icon={icon}
-            color={colors.white}
-            style={styles.icon}
-          />
-        </LinearGradient>
-      </View>
-    </TouchableWithoutFeedback>
-  )
-}
-
-const styles = StyleSheet.create({
-  outer: {
-    position: 'absolute',
-    zIndex: zIndex.FAB,
-    right: 22,
-    bottom: 14,
-    width: 60,
-    height: 60,
-    borderRadius: 30,
-    shadowColor: '#000',
-    shadowOpacity: 0.3,
-    shadowOffset: {width: 0, height: 1},
-  },
-  inner: {
-    width: 60,
-    height: 60,
-    borderRadius: 30,
-    justifyContent: 'center',
-    alignItems: 'center',
-  },
-  icon: {},
-})
diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx
index 2bb553575..05573d999 100644
--- a/src/view/com/util/Link.tsx
+++ b/src/view/com/util/Link.tsx
@@ -9,7 +9,8 @@ import {
   View,
   ViewStyle,
 } from 'react-native'
-import {Text} from './Text'
+import {Text} from './text/Text'
+import {TypographyVariant} from '../../lib/ThemeContext'
 import {useStores, RootStoreModel} from '../../../state'
 import {convertBskyAppUrlIfNeeded} from '../../../lib/strings'
 
@@ -57,14 +58,14 @@ export const Link = observer(function Link({
 })
 
 export const TextLink = observer(function Link({
+  type = 'body1',
   style,
   href,
-  title,
   text,
 }: {
+  type: TypographyVariant
   style?: StyleProp<TextStyle>
   href: string
-  title?: string
   text: string
 }) {
   const store = useStores()
@@ -75,7 +76,7 @@ export const TextLink = observer(function Link({
     handleLink(store, href, true)
   }
   return (
-    <Text style={style} onPress={onPress} onLongPress={onLongPress}>
+    <Text type={type} style={style} onPress={onPress} onLongPress={onLongPress}>
       {text}
     </Text>
   )
diff --git a/src/view/com/util/LoadingPlaceholder.tsx b/src/view/com/util/LoadingPlaceholder.tsx
index 9c2d0398f..15488167f 100644
--- a/src/view/com/util/LoadingPlaceholder.tsx
+++ b/src/view/com/util/LoadingPlaceholder.tsx
@@ -3,6 +3,7 @@ import {StyleSheet, StyleProp, View, ViewStyle} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {UpIcon} from '../../lib/icons'
 import {s, colors} from '../../lib/styles'
+import {useTheme} from '../../lib/ThemeContext'
 
 export function LoadingPlaceholder({
   width,
@@ -13,13 +14,14 @@ export function LoadingPlaceholder({
   height: string | number
   style?: StyleProp<ViewStyle>
 }) {
+  const theme = useTheme()
   return (
     <View
       style={[
         {
           width,
           height,
-          backgroundColor: '#e7e9ea',
+          backgroundColor: theme.palette.default.backgroundLight,
           borderRadius: 6,
           overflow: 'hidden',
         },
@@ -29,7 +31,7 @@ export function LoadingPlaceholder({
         style={{
           width,
           height,
-          backgroundColor: '#e7e9ea',
+          backgroundColor: theme.palette.default.backgroundLight,
         }}
       />
     </View>
@@ -41,6 +43,7 @@ export function PostLoadingPlaceholder({
 }: {
   style?: StyleProp<ViewStyle>
 }) {
+  const theme = useTheme()
   return (
     <View style={[styles.post, style]}>
       <LoadingPlaceholder width={50} height={50} style={styles.avatar} />
@@ -52,16 +55,24 @@ export function PostLoadingPlaceholder({
         <View style={s.flexRow}>
           <View style={s.flex1}>
             <FontAwesomeIcon
-              style={s.gray3}
+              style={{color: theme.palette.default.icon}}
               icon={['far', 'comment']}
               size={14}
             />
           </View>
           <View style={s.flex1}>
-            <FontAwesomeIcon style={s.gray3} icon="retweet" size={18} />
+            <FontAwesomeIcon
+              style={{color: theme.palette.default.icon}}
+              icon="retweet"
+              size={18}
+            />
           </View>
           <View style={s.flex1}>
-            <UpIcon style={s.gray3} size={17} strokeWidth={1.7} />
+            <UpIcon
+              style={{color: theme.palette.default.icon}}
+              size={17}
+              strokeWidth={1.7}
+            />
           </View>
           <View style={s.flex1}></View>
         </View>
@@ -125,8 +136,6 @@ export function NotificationFeedLoadingPlaceholder() {
 const styles = StyleSheet.create({
   post: {
     flexDirection: 'row',
-    backgroundColor: colors.white,
-    borderRadius: 6,
     padding: 10,
     margin: 1,
   },
@@ -135,8 +144,6 @@ const styles = StyleSheet.create({
     marginRight: 10,
   },
   notification: {
-    backgroundColor: colors.white,
-    borderRadius: 6,
     padding: 10,
     paddingLeft: 46,
     margin: 1,
diff --git a/src/view/com/util/Picker.tsx b/src/view/com/util/Picker.tsx
index 84a627b6d..208ec0492 100644
--- a/src/view/com/util/Picker.tsx
+++ b/src/view/com/util/Picker.tsx
@@ -1,3 +1,5 @@
+// TODO: replaceme with something in the design system
+
 import React, {useRef} from 'react'
 import {
   StyleProp,
@@ -13,7 +15,7 @@ import {
   FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
 import RootSiblings from 'react-native-root-siblings'
-import {Text} from './Text'
+import {Text} from './text/Text'
 import {colors} from '../../lib/styles'
 
 interface PickerItem {
diff --git a/src/view/com/util/PostCtrls.tsx b/src/view/com/util/PostCtrls.tsx
index 264210768..ac10e92fe 100644
--- a/src/view/com/util/PostCtrls.tsx
+++ b/src/view/com/util/PostCtrls.tsx
@@ -2,10 +2,10 @@ import React from 'react'
 import {Animated, StyleSheet, TouchableOpacity, View} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import ReactNativeHapticFeedback from 'react-native-haptic-feedback'
-import {Text} from './Text'
+import {Text} from './text/Text'
 import {UpIcon, UpIconSolid} from '../../lib/icons'
 import {s, colors} from '../../lib/styles'
-import {useAnimatedValue} from '../../lib/useAnimatedValue'
+import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue'
 
 interface PostCtrlsOpts {
   big?: boolean
diff --git a/src/view/com/util/PostEmbeds.tsx b/src/view/com/util/PostEmbeds.tsx
index 1c980465a..839110a21 100644
--- a/src/view/com/util/PostEmbeds.tsx
+++ b/src/view/com/util/PostEmbeds.tsx
@@ -2,7 +2,7 @@ import React from 'react'
 import {ImageStyle, StyleSheet, StyleProp, View, ViewStyle} from 'react-native'
 import {AppBskyEmbedImages, AppBskyEmbedExternal} from '@atproto/api'
 import {Link} from '../util/Link'
-import {Text} from '../util/Text'
+import {Text} from './text/Text'
 import {colors} from '../../lib/styles'
 import {AutoSizedImage} from './images/AutoSizedImage'
 import {ImagesLightbox} from '../../../state/models/shell-ui'
diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx
index 77dfbb485..fae3a4c83 100644
--- a/src/view/com/util/PostMeta.tsx
+++ b/src/view/com/util/PostMeta.tsx
@@ -2,8 +2,8 @@ import React from 'react'
 import {StyleSheet, View} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {Link} from '../util/Link'
-import {Text} from '../util/Text'
-import {PostDropdownBtn} from '../util/DropdownBtn'
+import {Text} from './text/Text'
+import {PostDropdownBtn} from './forms/DropdownButton'
 import {s} from '../../lib/styles'
 import {ago} from '../../../lib/strings'
 
diff --git a/src/view/com/util/Selector.tsx b/src/view/com/util/Selector.tsx
index 954360b32..211c5b902 100644
--- a/src/view/com/util/Selector.tsx
+++ b/src/view/com/util/Selector.tsx
@@ -5,7 +5,7 @@ import {
   TouchableWithoutFeedback,
   View,
 } from 'react-native'
-import {Text} from './Text'
+import {Text} from './text/Text'
 import {colors} from '../../lib/styles'
 
 interface Layout {
diff --git a/src/view/com/util/Text.tsx b/src/view/com/util/Text.tsx
deleted file mode 100644
index acf7589e0..000000000
--- a/src/view/com/util/Text.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import React from 'react'
-import {Text as RNText, TextProps} from 'react-native'
-import {s} from '../../lib/styles'
-
-export function Text({
-  children,
-  style,
-  ...props
-}: React.PropsWithChildren<TextProps>) {
-  return (
-    <RNText style={[s.black, style]} {...props}>
-      {children}
-    </RNText>
-  )
-}
diff --git a/src/view/com/util/UserInfoText.tsx b/src/view/com/util/UserInfoText.tsx
index cdd1f4d91..f5ed07d63 100644
--- a/src/view/com/util/UserInfoText.tsx
+++ b/src/view/com/util/UserInfoText.tsx
@@ -2,7 +2,7 @@ import React, {useState, useEffect} from 'react'
 import {AppBskyActorGetProfile as GetProfile} from '@atproto/api'
 import {StyleProp, TextStyle} from 'react-native'
 import {Link} from './Link'
-import {Text} from './Text'
+import {Text} from './text/Text'
 import {LoadingPlaceholder} from './LoadingPlaceholder'
 import {useStores} from '../../../state'
 
diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx
index e14c2412d..c6eaba5dd 100644
--- a/src/view/com/util/ViewHeader.tsx
+++ b/src/view/com/util/ViewHeader.tsx
@@ -8,7 +8,7 @@ import {
 } from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {UserAvatar} from './UserAvatar'
-import {Text} from './Text'
+import {Text} from './text/Text'
 import {s, colors} from '../../lib/styles'
 import {MagnifyingGlassIcon} from '../../lib/icons'
 import {useStores} from '../../../state'
diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx
index e436e41b2..a29ee9d26 100644
--- a/src/view/com/util/ViewSelector.tsx
+++ b/src/view/com/util/ViewSelector.tsx
@@ -7,8 +7,8 @@ import {
 } from 'react-native'
 import {Selector} from './Selector'
 import {HorzSwipe} from './gestures/HorzSwipe'
-import {useAnimatedValue} from '../../lib/useAnimatedValue'
-import {OnScrollCb} from '../../lib/useOnMainScroll'
+import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue'
+import {OnScrollCb} from '../../lib/hooks/useOnMainScroll'
 
 const HEADER_ITEM = {_reactKey: '__header__'}
 const SELECTOR_ITEM = {_reactKey: '__selector__'}
diff --git a/src/view/com/util/error/ErrorMessage.tsx b/src/view/com/util/error/ErrorMessage.tsx
new file mode 100644
index 000000000..905268d3e
--- /dev/null
+++ b/src/view/com/util/error/ErrorMessage.tsx
@@ -0,0 +1,76 @@
+import React from 'react'
+import {
+  StyleSheet,
+  TouchableOpacity,
+  StyleProp,
+  View,
+  ViewStyle,
+} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {Text} from '../text/Text'
+import {colors} from '../../../lib/styles'
+import {useTheme} from '../../../lib/ThemeContext'
+import {usePalette} from '../../../lib/hooks/usePalette'
+
+export function ErrorMessage({
+  message,
+  numberOfLines,
+  style,
+  onPressTryAgain,
+}: {
+  message: string
+  numberOfLines?: number
+  style?: StyleProp<ViewStyle>
+  onPressTryAgain?: () => void
+}) {
+  const theme = useTheme()
+  const pal = usePalette('error')
+  return (
+    <View style={[styles.outer, pal.view, style]}>
+      <View
+        style={[styles.errorIcon, {backgroundColor: theme.palette.error.icon}]}>
+        <FontAwesomeIcon icon="exclamation" style={pal.text} size={16} />
+      </View>
+      <Text
+        type="body2"
+        style={[styles.message, pal.text]}
+        numberOfLines={numberOfLines}>
+        {message}
+      </Text>
+      {onPressTryAgain && (
+        <TouchableOpacity style={styles.btn} onPress={onPressTryAgain}>
+          <FontAwesomeIcon
+            icon="arrows-rotate"
+            style={{color: theme.palette.error.icon}}
+            size={18}
+          />
+        </TouchableOpacity>
+      )}
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  outer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingVertical: 8,
+    paddingHorizontal: 8,
+  },
+  errorIcon: {
+    borderRadius: 12,
+    width: 24,
+    height: 24,
+    alignItems: 'center',
+    justifyContent: 'center',
+    marginRight: 8,
+  },
+  message: {
+    flex: 1,
+    paddingRight: 10,
+  },
+  btn: {
+    paddingHorizontal: 4,
+    paddingVertical: 4,
+  },
+})
diff --git a/src/view/com/util/ErrorScreen.tsx b/src/view/com/util/error/ErrorScreen.tsx
index d0e1e2755..6db54a9f2 100644
--- a/src/view/com/util/ErrorScreen.tsx
+++ b/src/view/com/util/error/ErrorScreen.tsx
@@ -1,8 +1,10 @@
 import React from 'react'
 import {StyleSheet, TouchableOpacity, View} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {Text} from './Text'
-import {colors} from '../../lib/styles'
+import {Text} from '../text/Text'
+import {colors} from '../../../lib/styles'
+import {useTheme} from '../../../lib/ThemeContext'
+import {usePalette} from '../../../lib/hooks/usePalette'
 
 export function ErrorScreen({
   title,
@@ -15,10 +17,16 @@ export function ErrorScreen({
   details?: string
   onPressTryAgain?: () => void
 }) {
+  const theme = useTheme()
+  const pal = usePalette('error')
   return (
-    <View style={styles.outer}>
+    <View style={[styles.outer, pal.view]}>
       <View style={styles.errorIconContainer}>
-        <View style={styles.errorIcon}>
+        <View
+          style={[
+            styles.errorIcon,
+            {backgroundColor: theme.palette.error.icon},
+          ]}>
           <FontAwesomeIcon
             icon="exclamation"
             style={{color: colors.white}}
@@ -26,18 +34,30 @@ export function ErrorScreen({
           />
         </View>
       </View>
-      <Text style={styles.title}>{title}</Text>
-      <Text style={styles.message}>{message}</Text>
-      {details && <Text style={styles.details}>{details}</Text>}
+      <Text type="h3" style={[styles.title, pal.text]}>
+        {title}
+      </Text>
+      <Text style={[styles.message, pal.textLight]}>{message}</Text>
+      {details && (
+        <Text
+          type="body2"
+          style={[
+            styles.details,
+            pal.textInverted,
+            {backgroundColor: theme.palette.default.background},
+          ]}>
+          {details}
+        </Text>
+      )}
       {onPressTryAgain && (
         <View style={styles.btnContainer}>
-          <TouchableOpacity style={styles.btn} onPress={onPressTryAgain}>
-            <FontAwesomeIcon
-              icon="arrows-rotate"
-              style={{color: colors.white}}
-              size={16}
-            />
-            <Text style={styles.btnText}>Try again</Text>
+          <TouchableOpacity
+            style={[styles.btn, {backgroundColor: theme.palette.error.icon}]}
+            onPress={onPressTryAgain}>
+            <FontAwesomeIcon icon="arrows-rotate" style={pal.text} size={16} />
+            <Text type="button" style={[styles.btnText, pal.text]}>
+              Try again
+            </Text>
           </TouchableOpacity>
         </View>
       )}
@@ -48,32 +68,19 @@ export function ErrorScreen({
 const styles = StyleSheet.create({
   outer: {
     flex: 1,
-    backgroundColor: colors.red1,
-    borderWidth: 1,
-    borderColor: colors.red3,
-    borderRadius: 6,
     paddingVertical: 30,
     paddingHorizontal: 14,
-    margin: 10,
   },
   title: {
     textAlign: 'center',
-    color: colors.red4,
-    fontSize: 24,
     marginBottom: 10,
   },
   message: {
     textAlign: 'center',
-    color: colors.red4,
     marginBottom: 20,
   },
   details: {
     textAlign: 'center',
-    color: colors.black,
-    backgroundColor: colors.white,
-    borderWidth: 1,
-    borderColor: colors.gray5,
-    borderRadius: 6,
     paddingVertical: 10,
     paddingHorizontal: 14,
     overflow: 'hidden',
@@ -85,23 +92,17 @@ const styles = StyleSheet.create({
   btn: {
     flexDirection: 'row',
     alignItems: 'center',
-    backgroundColor: colors.red4,
-    borderRadius: 6,
     paddingHorizontal: 16,
     paddingVertical: 10,
   },
   btnText: {
     marginLeft: 5,
-    color: colors.white,
-    fontSize: 16,
-    fontWeight: 'bold',
   },
   errorIconContainer: {
     alignItems: 'center',
     marginBottom: 10,
   },
   errorIcon: {
-    backgroundColor: colors.red4,
     borderRadius: 30,
     width: 50,
     height: 50,
diff --git a/src/view/com/util/forms/Button.tsx b/src/view/com/util/forms/Button.tsx
new file mode 100644
index 000000000..b5c4da19d
--- /dev/null
+++ b/src/view/com/util/forms/Button.tsx
@@ -0,0 +1,120 @@
+import React from 'react'
+import {
+  StyleProp,
+  StyleSheet,
+  TextStyle,
+  TouchableOpacity,
+  ViewStyle,
+} from 'react-native'
+import {Text} from '../text/Text'
+import {useTheme} from '../../../lib/ThemeContext'
+import {choose} from '../../../../lib/functions'
+
+export type ButtonType =
+  | 'primary'
+  | 'secondary'
+  | 'inverted'
+  | 'primary-outline'
+  | 'secondary-outline'
+  | 'primary-light'
+  | 'secondary-light'
+  | 'default-light'
+
+export function Button({
+  type = 'primary',
+  label,
+  style,
+  onPress,
+  children,
+}: React.PropsWithChildren<{
+  type?: ButtonType
+  label?: string
+  style?: StyleProp<ViewStyle>
+  onPress?: () => void
+}>) {
+  const theme = useTheme()
+  const outerStyle = choose<ViewStyle, Record<ButtonType, ViewStyle>>(type, {
+    primary: {
+      backgroundColor: theme.palette.primary.background,
+    },
+    secondary: {
+      backgroundColor: theme.palette.secondary.background,
+    },
+    inverted: {
+      backgroundColor: theme.palette.inverted.background,
+    },
+    'primary-outline': {
+      backgroundColor: theme.palette.default.background,
+      borderWidth: 1,
+      borderColor: theme.palette.primary.border,
+    },
+    'secondary-outline': {
+      backgroundColor: theme.palette.default.background,
+      borderWidth: 1,
+      borderColor: theme.palette.secondary.border,
+    },
+    'primary-light': {
+      backgroundColor: theme.palette.default.background,
+    },
+    'secondary-light': {
+      backgroundColor: theme.palette.default.background,
+    },
+    'default-light': {
+      backgroundColor: theme.palette.default.background,
+    },
+  })
+  const labelStyle = choose<TextStyle, Record<ButtonType, TextStyle>>(type, {
+    primary: {
+      color: theme.palette.primary.text,
+      fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
+    },
+    secondary: {
+      color: theme.palette.secondary.text,
+      fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
+    },
+    inverted: {
+      color: theme.palette.inverted.text,
+      fontWeight: theme.palette.inverted.isLowContrast ? '500' : undefined,
+    },
+    'primary-outline': {
+      color: theme.palette.primary.textInverted,
+      fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
+    },
+    'secondary-outline': {
+      color: theme.palette.secondary.textInverted,
+      fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
+    },
+    'primary-light': {
+      color: theme.palette.primary.textInverted,
+      fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
+    },
+    'secondary-light': {
+      color: theme.palette.secondary.textInverted,
+      fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
+    },
+    'default-light': {
+      color: theme.palette.default.text,
+      fontWeight: theme.palette.default.isLowContrast ? '500' : undefined,
+    },
+  })
+  return (
+    <TouchableOpacity
+      style={[outerStyle, styles.outer, style]}
+      onPress={onPress}>
+      {label ? (
+        <Text type="button" style={[labelStyle]}>
+          {label}
+        </Text>
+      ) : (
+        children
+      )}
+    </TouchableOpacity>
+  )
+}
+
+const styles = StyleSheet.create({
+  outer: {
+    paddingHorizontal: 10,
+    paddingVertical: 8,
+  },
+})
diff --git a/src/view/com/util/DropdownBtn.tsx b/src/view/com/util/forms/DropdownButton.tsx
index 3c6421934..c81ccf6c5 100644
--- a/src/view/com/util/DropdownBtn.tsx
+++ b/src/view/com/util/forms/DropdownButton.tsx
@@ -11,12 +11,13 @@ import {
 import {IconProp} from '@fortawesome/fontawesome-svg-core'
 import RootSiblings from 'react-native-root-siblings'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {Text} from './Text'
-import {colors} from '../../lib/styles'
-import {toShareUrl} from '../../../lib/strings'
-import {useStores} from '../../../state'
-import {ReportPostModal, ConfirmModal} from '../../../state/models/shell-ui'
-import {TABS_ENABLED} from '../../../build-flags'
+import {Text} from '../text/Text'
+import {Button, ButtonType} from './Button'
+import {colors} from '../../../lib/styles'
+import {toShareUrl} from '../../../../lib/strings'
+import {useStores} from '../../../../state'
+import {ReportPostModal, ConfirmModal} from '../../../../state/models/shell-ui'
+import {TABS_ENABLED} from '../../../../build-flags'
 
 const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10}
 
@@ -26,14 +27,20 @@ export interface DropdownItem {
   onPress: () => void
 }
 
-export function DropdownBtn({
+export type DropdownButtonType = ButtonType | 'bare'
+
+export function DropdownButton({
+  type = 'bare',
   style,
   items,
+  label,
   menuWidth,
   children,
 }: {
+  type: DropdownButtonType
   style?: StyleProp<ViewStyle>
   items: DropdownItem[]
+  label?: string
   menuWidth?: number
   children?: React.ReactNode
 }) {
@@ -62,14 +69,23 @@ export function DropdownBtn({
     )
   }
 
+  if (type === 'bare') {
+    return (
+      <TouchableOpacity
+        style={style}
+        onPress={onPress}
+        hitSlop={HITSLOP}
+        ref={ref}>
+        {children}
+      </TouchableOpacity>
+    )
+  }
   return (
-    <TouchableOpacity
-      style={style}
-      onPress={onPress}
-      hitSlop={HITSLOP}
-      ref={ref}>
-      {children}
-    </TouchableOpacity>
+    <View ref={ref}>
+      <Button onPress={onPress} style={style} label={label}>
+        {children}
+      </Button>
+    </View>
   )
 }
 
@@ -77,7 +93,6 @@ export function PostDropdownBtn({
   style,
   children,
   itemHref,
-  itemTitle,
   isAuthor,
   onCopyPostText,
   onDeletePost,
@@ -141,9 +156,9 @@ export function PostDropdownBtn({
   ].filter(Boolean) as DropdownItem[]
 
   return (
-    <DropdownBtn style={style} items={dropdownItems} menuWidth={200}>
+    <DropdownButton style={style} items={dropdownItems} menuWidth={200}>
       {children}
-    </DropdownBtn>
+    </DropdownButton>
   )
 }
 
diff --git a/src/view/com/util/forms/RadioButton.tsx b/src/view/com/util/forms/RadioButton.tsx
index 9da404bea..81489c447 100644
--- a/src/view/com/util/forms/RadioButton.tsx
+++ b/src/view/com/util/forms/RadioButton.tsx
@@ -1,24 +1,126 @@
 import React from 'react'
-import {StyleSheet, TouchableOpacity, View} from 'react-native'
-import {Text} from '../Text'
-import {colors} from '../../../lib/styles'
+import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
+import {Text} from '../text/Text'
+import {Button, ButtonType} from './Button'
+import {useTheme} from '../../../lib/ThemeContext'
+import {choose} from '../../../../lib/functions'
 
 export function RadioButton({
+  type = 'default-light',
   label,
   isSelected,
+  style,
   onPress,
 }: {
+  type?: ButtonType
   label: string
   isSelected: boolean
+  style?: StyleProp<ViewStyle>
   onPress: () => void
 }) {
+  const theme = useTheme()
+  const circleStyle = choose<TextStyle, Record<ButtonType, TextStyle>>(type, {
+    primary: {
+      borderColor: theme.palette.primary.text,
+    },
+    secondary: {
+      borderColor: theme.palette.secondary.text,
+    },
+    inverted: {
+      borderColor: theme.palette.inverted.text,
+    },
+    'primary-outline': {
+      borderColor: theme.palette.primary.border,
+    },
+    'secondary-outline': {
+      borderColor: theme.palette.secondary.border,
+    },
+    'primary-light': {
+      borderColor: theme.palette.primary.border,
+    },
+    'secondary-light': {
+      borderColor: theme.palette.secondary.border,
+    },
+    'default-light': {
+      borderColor: theme.palette.default.border,
+    },
+  })
+  const circleFillStyle = choose<TextStyle, Record<ButtonType, TextStyle>>(
+    type,
+    {
+      primary: {
+        backgroundColor: theme.palette.primary.text,
+      },
+      secondary: {
+        backgroundColor: theme.palette.secondary.text,
+      },
+      inverted: {
+        backgroundColor: theme.palette.inverted.text,
+      },
+      'primary-outline': {
+        backgroundColor: theme.palette.primary.background,
+      },
+      'secondary-outline': {
+        backgroundColor: theme.palette.secondary.background,
+      },
+      'primary-light': {
+        backgroundColor: theme.palette.primary.background,
+      },
+      'secondary-light': {
+        backgroundColor: theme.palette.secondary.background,
+      },
+      'default-light': {
+        backgroundColor: theme.palette.primary.background,
+      },
+    },
+  )
+  const labelStyle = choose<TextStyle, Record<ButtonType, TextStyle>>(type, {
+    primary: {
+      color: theme.palette.primary.text,
+      fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
+    },
+    secondary: {
+      color: theme.palette.secondary.text,
+      fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
+    },
+    inverted: {
+      color: theme.palette.inverted.text,
+      fontWeight: theme.palette.inverted.isLowContrast ? '500' : undefined,
+    },
+    'primary-outline': {
+      color: theme.palette.primary.textInverted,
+      fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
+    },
+    'secondary-outline': {
+      color: theme.palette.secondary.textInverted,
+      fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
+    },
+    'primary-light': {
+      color: theme.palette.primary.textInverted,
+      fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
+    },
+    'secondary-light': {
+      color: theme.palette.secondary.textInverted,
+      fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
+    },
+    'default-light': {
+      color: theme.palette.default.text,
+      fontWeight: theme.palette.default.isLowContrast ? '500' : undefined,
+    },
+  })
   return (
-    <TouchableOpacity style={styles.outer} onPress={onPress}>
-      <View style={styles.circle}>
-        {isSelected ? <View style={styles.circleFill} /> : undefined}
+    <Button type={type} onPress={onPress} style={style}>
+      <View style={styles.outer}>
+        <View style={[circleStyle, styles.circle]}>
+          {isSelected ? (
+            <View style={[circleFillStyle, styles.circleFill]} />
+          ) : undefined}
+        </View>
+        <Text type="button" style={[labelStyle, styles.label]}>
+          {label}
+        </Text>
       </View>
-      <Text style={styles.label}>{label}</Text>
-    </TouchableOpacity>
+    </Button>
   )
 }
 
@@ -26,30 +128,21 @@ const styles = StyleSheet.create({
   outer: {
     flexDirection: 'row',
     alignItems: 'center',
-    marginBottom: 5,
-    borderRadius: 8,
-    borderWidth: 1,
-    borderColor: colors.gray2,
-    paddingHorizontal: 10,
-    paddingVertical: 8,
   },
   circle: {
-    width: 30,
-    height: 30,
+    width: 26,
+    height: 26,
     borderRadius: 15,
     padding: 4,
     borderWidth: 1,
-    borderColor: colors.gray3,
     marginRight: 10,
   },
   circleFill: {
-    width: 20,
-    height: 20,
+    width: 16,
+    height: 16,
     borderRadius: 10,
-    backgroundColor: colors.blue3,
   },
   label: {
     flex: 1,
-    fontSize: 17,
   },
 })
diff --git a/src/view/com/util/forms/RadioGroup.tsx b/src/view/com/util/forms/RadioGroup.tsx
index 6684cde5c..9abc2345f 100644
--- a/src/view/com/util/forms/RadioGroup.tsx
+++ b/src/view/com/util/forms/RadioGroup.tsx
@@ -1,6 +1,7 @@
 import React, {useState} from 'react'
 import {View} from 'react-native'
 import {RadioButton} from './RadioButton'
+import {ButtonType} from './Button'
 
 export interface RadioGroupItem {
   label: string
@@ -8,22 +9,28 @@ export interface RadioGroupItem {
 }
 
 export function RadioGroup({
+  type,
   items,
+  initialSelection = '',
   onSelect,
 }: {
+  type?: ButtonType
   items: RadioGroupItem[]
+  initialSelection?: string
   onSelect: (key: string) => void
 }) {
-  const [selection, setSelection] = useState<string>('')
+  const [selection, setSelection] = useState<string>(initialSelection)
   const onSelectInner = (key: string) => {
     setSelection(key)
     onSelect(key)
   }
   return (
     <View>
-      {items.map(item => (
+      {items.map((item, i) => (
         <RadioButton
           key={item.key}
+          style={i !== 0 ? {marginTop: 2} : undefined}
+          type={type}
           label={item.label}
           isSelected={item.key === selection}
           onPress={() => onSelectInner(item.key)}
diff --git a/src/view/com/util/forms/ToggleButton.tsx b/src/view/com/util/forms/ToggleButton.tsx
new file mode 100644
index 000000000..77e8fa203
--- /dev/null
+++ b/src/view/com/util/forms/ToggleButton.tsx
@@ -0,0 +1,165 @@
+import React from 'react'
+import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
+import {Text} from '../text/Text'
+import {Button, ButtonType} from './Button'
+import {useTheme} from '../../../lib/ThemeContext'
+import {choose} from '../../../../lib/functions'
+import {colors} from '../../../lib/styles'
+
+export function ToggleButton({
+  type = 'default-light',
+  label,
+  isSelected,
+  style,
+  onPress,
+}: {
+  type?: ButtonType
+  label: string
+  isSelected: boolean
+  style?: StyleProp<ViewStyle>
+  onPress: () => void
+}) {
+  const theme = useTheme()
+  const circleStyle = choose<TextStyle, Record<ButtonType, TextStyle>>(type, {
+    primary: {
+      borderColor: theme.palette.primary.text,
+    },
+    secondary: {
+      borderColor: theme.palette.secondary.text,
+    },
+    inverted: {
+      borderColor: theme.palette.inverted.text,
+    },
+    'primary-outline': {
+      borderColor: theme.palette.primary.border,
+    },
+    'secondary-outline': {
+      borderColor: theme.palette.secondary.border,
+    },
+    'primary-light': {
+      borderColor: theme.palette.primary.border,
+    },
+    'secondary-light': {
+      borderColor: theme.palette.secondary.border,
+    },
+    'default-light': {
+      borderColor: theme.palette.default.border,
+    },
+  })
+  const circleFillStyle = choose<TextStyle, Record<ButtonType, TextStyle>>(
+    type,
+    {
+      primary: {
+        backgroundColor: theme.palette.primary.text,
+        opacity: isSelected ? 1 : 0.33,
+      },
+      secondary: {
+        backgroundColor: theme.palette.secondary.text,
+        opacity: isSelected ? 1 : 0.33,
+      },
+      inverted: {
+        backgroundColor: theme.palette.inverted.text,
+        opacity: isSelected ? 1 : 0.33,
+      },
+      'primary-outline': {
+        backgroundColor: theme.palette.primary.background,
+        opacity: isSelected ? 1 : 0.5,
+      },
+      'secondary-outline': {
+        backgroundColor: theme.palette.secondary.background,
+        opacity: isSelected ? 1 : 0.5,
+      },
+      'primary-light': {
+        backgroundColor: theme.palette.primary.background,
+        opacity: isSelected ? 1 : 0.5,
+      },
+      'secondary-light': {
+        backgroundColor: theme.palette.secondary.background,
+        opacity: isSelected ? 1 : 0.5,
+      },
+      'default-light': {
+        backgroundColor: isSelected
+          ? theme.palette.primary.background
+          : colors.gray3,
+      },
+    },
+  )
+  const labelStyle = choose<TextStyle, Record<ButtonType, TextStyle>>(type, {
+    primary: {
+      color: theme.palette.primary.text,
+      fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
+    },
+    secondary: {
+      color: theme.palette.secondary.text,
+      fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
+    },
+    inverted: {
+      color: theme.palette.inverted.text,
+      fontWeight: theme.palette.inverted.isLowContrast ? '500' : undefined,
+    },
+    'primary-outline': {
+      color: theme.palette.primary.textInverted,
+      fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
+    },
+    'secondary-outline': {
+      color: theme.palette.secondary.textInverted,
+      fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
+    },
+    'primary-light': {
+      color: theme.palette.primary.textInverted,
+      fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
+    },
+    'secondary-light': {
+      color: theme.palette.secondary.textInverted,
+      fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
+    },
+    'default-light': {
+      color: theme.palette.default.text,
+      fontWeight: theme.palette.default.isLowContrast ? '500' : undefined,
+    },
+  })
+  return (
+    <Button type={type} onPress={onPress} style={style}>
+      <View style={styles.outer}>
+        <View style={[circleStyle, styles.circle]}>
+          <View
+            style={[
+              circleFillStyle,
+              styles.circleFill,
+              isSelected ? styles.circleFillSelected : undefined,
+            ]}
+          />
+        </View>
+        <Text type="button" style={[labelStyle, styles.label]}>
+          {label}
+        </Text>
+      </View>
+    </Button>
+  )
+}
+
+const styles = StyleSheet.create({
+  outer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+  },
+  circle: {
+    width: 42,
+    height: 26,
+    borderRadius: 15,
+    padding: 4,
+    borderWidth: 1,
+    marginRight: 10,
+  },
+  circleFill: {
+    width: 16,
+    height: 16,
+    borderRadius: 10,
+  },
+  circleFillSelected: {
+    marginLeft: 16,
+  },
+  label: {
+    flex: 1,
+  },
+})
diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx
index 05425eb31..9de443b7f 100644
--- a/src/view/com/util/images/AutoSizedImage.tsx
+++ b/src/view/com/util/images/AutoSizedImage.tsx
@@ -9,7 +9,7 @@ import {
   View,
   ViewStyle,
 } from 'react-native'
-import {Text} from '../Text'
+import {Text} from '../text/Text'
 import {colors} from '../../../lib/styles'
 
 const MAX_HEIGHT = 300
diff --git a/src/view/com/util/RichText.tsx b/src/view/com/util/text/RichText.tsx
index d6f193f9d..c9ed4b58e 100644
--- a/src/view/com/util/RichText.tsx
+++ b/src/view/com/util/text/RichText.tsx
@@ -1,9 +1,11 @@
 import React from 'react'
 import {TextStyle, StyleProp} from 'react-native'
-import {TextLink} from './Link'
+import {TextLink} from '../Link'
 import {Text} from './Text'
-import {s} from '../../lib/styles'
-import {toShortUrl} from '../../../lib/strings'
+import {s} from '../../../lib/styles'
+import {toShortUrl} from '../../../../lib/strings'
+import {TypographyVariant} from '../../../lib/ThemeContext'
+import {usePalette} from '../../../lib/hooks/usePalette'
 
 type TextSlice = {start: number; end: number}
 type Entity = {
@@ -13,16 +15,19 @@ type Entity = {
 }
 
 export function RichText({
+  type = 'body1',
   text,
   entities,
   style,
   numberOfLines,
 }: {
+  type: TypographyVariant
   text: string
   entities?: Entity[]
   style?: StyleProp<TextStyle>
   numberOfLines?: number
 }) {
+  const pal = usePalette('default')
   if (!entities?.length) {
     if (/^\p{Extended_Pictographic}+$/u.test(text) && text.length <= 5) {
       style = {
@@ -47,18 +52,20 @@ export function RichText({
         els.push(
           <TextLink
             key={key}
+            type={type}
             text={segment.text}
             href={`/profile/${segment.entity.value}`}
-            style={[style, s.blue3]}
+            style={[style, pal.link]}
           />,
         )
       } else if (segment.entity.type === 'link') {
         els.push(
           <TextLink
             key={key}
+            type={type}
             text={toShortUrl(segment.text)}
             href={segment.entity.value}
-            style={[style, s.blue3]}
+            style={[style, pal.link]}
           />,
         )
       }
@@ -66,7 +73,7 @@ export function RichText({
     key++
   }
   return (
-    <Text style={style} numberOfLines={numberOfLines}>
+    <Text type={type} style={[pal.text, style]} numberOfLines={numberOfLines}>
       {els}
     </Text>
   )
diff --git a/src/view/com/util/text/Text.tsx b/src/view/com/util/text/Text.tsx
new file mode 100644
index 000000000..549eb2901
--- /dev/null
+++ b/src/view/com/util/text/Text.tsx
@@ -0,0 +1,23 @@
+import React from 'react'
+import {Text as RNText, TextProps} from 'react-native'
+import {s} from '../../../lib/styles'
+import {useTheme, TypographyVariant} from '../../../lib/ThemeContext'
+
+export type CustomTextProps = TextProps & {
+  type?: TypographyVariant
+}
+
+export function Text({
+  type = 'body1',
+  children,
+  style,
+  ...props
+}: React.PropsWithChildren<CustomTextProps>) {
+  const theme = useTheme()
+  const typography = theme.typography[type]
+  return (
+    <RNText style={[s.black, typography, style]} {...props}>
+      {children}
+    </RNText>
+  )
+}