about summary refs log tree commit diff
path: root/src/view
diff options
context:
space:
mode:
Diffstat (limited to 'src/view')
-rw-r--r--src/view/com/auth/create/CreateAccount.tsx8
-rw-r--r--src/view/com/auth/create/Step1.tsx9
-rw-r--r--src/view/com/auth/create/Step2.tsx4
-rw-r--r--src/view/com/auth/create/Step3.tsx1
-rw-r--r--src/view/com/auth/login/Login.tsx10
-rw-r--r--src/view/com/composer/Composer.tsx73
-rw-r--r--src/view/com/composer/char-progress/CharProgress.tsx18
-rw-r--r--src/view/com/composer/photos/OpenCameraBtn.tsx6
-rw-r--r--src/view/com/composer/photos/SelectPhotoBtn.tsx6
-rw-r--r--src/view/com/composer/text-input/TextInput.tsx53
-rw-r--r--src/view/com/composer/text-input/TextInput.web.tsx15
-rw-r--r--src/view/com/discover/SuggestedFollows.tsx11
-rw-r--r--src/view/com/modals/ChangeHandle.tsx6
-rw-r--r--src/view/com/modals/Confirm.tsx4
-rw-r--r--src/view/com/modals/DeleteAccount.tsx4
-rw-r--r--src/view/com/modals/EditProfile.tsx14
-rw-r--r--src/view/com/modals/ReportAccount.tsx25
-rw-r--r--src/view/com/modals/ReportPost.tsx23
-rw-r--r--src/view/com/modals/Repost.tsx14
-rw-r--r--src/view/com/notifications/FeedItem.tsx23
-rw-r--r--src/view/com/pager/FeedsTabBar.tsx9
-rw-r--r--src/view/com/pager/Pager.tsx4
-rw-r--r--src/view/com/pager/TabBar.tsx9
-rw-r--r--src/view/com/post-thread/PostLikedBy.tsx (renamed from src/view/com/post-thread/PostVotedBy.tsx)19
-rw-r--r--src/view/com/post-thread/PostRepostedBy.tsx1
-rw-r--r--src/view/com/post-thread/PostThread.tsx60
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx271
-rw-r--r--src/view/com/post/Post.tsx17
-rw-r--r--src/view/com/posts/Feed.tsx1
-rw-r--r--src/view/com/posts/FeedItem.tsx24
-rw-r--r--src/view/com/profile/FollowButton.tsx7
-rw-r--r--src/view/com/profile/ProfileCard.tsx17
-rw-r--r--src/view/com/profile/ProfileFollowers.tsx3
-rw-r--r--src/view/com/profile/ProfileFollows.tsx3
-rw-r--r--src/view/com/profile/ProfileHeader.tsx185
-rw-r--r--src/view/com/search/SearchResults.tsx1
-rw-r--r--src/view/com/util/Link.tsx11
-rw-r--r--src/view/com/util/PostCtrls.tsx40
-rw-r--r--src/view/com/util/PostEmbeds/YoutubeEmbed.tsx119
-rw-r--r--src/view/com/util/PostMeta.tsx10
-rw-r--r--src/view/com/util/UserAvatar.tsx7
-rw-r--r--src/view/com/util/UserBanner.tsx21
-rw-r--r--src/view/com/util/ViewHeader.tsx2
-rw-r--r--src/view/com/util/ViewSelector.tsx63
-rw-r--r--src/view/com/util/forms/Button.tsx5
-rw-r--r--src/view/com/util/forms/DropdownButton.tsx27
-rw-r--r--src/view/com/util/forms/RadioButton.tsx4
-rw-r--r--src/view/com/util/forms/RadioGroup.tsx3
-rw-r--r--src/view/com/util/images/AutoSizedImage.tsx24
-rw-r--r--src/view/com/util/post-embeds/ExternalLinkEmbed.tsx (renamed from src/view/com/util/PostEmbeds/ExternalLinkEmbed.tsx)15
-rw-r--r--src/view/com/util/post-embeds/QuoteEmbed.tsx (renamed from src/view/com/util/PostEmbeds/QuoteEmbed.tsx)30
-rw-r--r--src/view/com/util/post-embeds/YoutubeEmbed.tsx55
-rw-r--r--src/view/com/util/post-embeds/index.tsx (renamed from src/view/com/util/PostEmbeds/index.tsx)61
-rw-r--r--src/view/com/util/text/RichText.tsx106
-rw-r--r--src/view/screens/Home.tsx25
-rw-r--r--src/view/screens/NotFound.tsx14
-rw-r--r--src/view/screens/Notifications.tsx3
-rw-r--r--src/view/screens/PostLikedBy.tsx (renamed from src/view/screens/PostUpvotedBy.tsx)8
-rw-r--r--src/view/screens/PostThread.tsx2
-rw-r--r--src/view/screens/Profile.tsx3
-rw-r--r--src/view/screens/Search.tsx1
-rw-r--r--src/view/shell/BottomBar.tsx7
-rw-r--r--src/view/shell/Drawer.tsx2
-rw-r--r--src/view/shell/index.tsx37
64 files changed, 927 insertions, 736 deletions
diff --git a/src/view/com/auth/create/CreateAccount.tsx b/src/view/com/auth/create/CreateAccount.tsx
index 618c15cf5..6ece903d6 100644
--- a/src/view/com/auth/create/CreateAccount.tsx
+++ b/src/view/com/auth/create/CreateAccount.tsx
@@ -75,16 +75,14 @@ export const CreateAccount = observer(
             {model.step === 3 && <Step3 model={model} />}
           </View>
           <View style={[s.flexRow, s.pl20, s.pr20]}>
-            <TouchableOpacity onPress={onPressBackInner}>
+            <TouchableOpacity onPress={onPressBackInner} testID="backBtn">
               <Text type="xl" style={pal.link}>
                 Back
               </Text>
             </TouchableOpacity>
             <View style={s.flex1} />
             {model.canNext ? (
-              <TouchableOpacity
-                testID="createAccountButton"
-                onPress={onPressNext}>
+              <TouchableOpacity testID="nextBtn" onPress={onPressNext}>
                 {model.isProcessing ? (
                   <ActivityIndicator />
                 ) : (
@@ -95,7 +93,7 @@ export const CreateAccount = observer(
               </TouchableOpacity>
             ) : model.didServiceDescriptionFetchFail ? (
               <TouchableOpacity
-                testID="registerRetryButton"
+                testID="retryConnectBtn"
                 onPress={onPressRetryConnect}>
                 <Text type="xl-bold" style={[pal.link, s.pr5]}>
                   Retry
diff --git a/src/view/com/auth/create/Step1.tsx b/src/view/com/auth/create/Step1.tsx
index 0a628f9d0..ca964ede2 100644
--- a/src/view/com/auth/create/Step1.tsx
+++ b/src/view/com/auth/create/Step1.tsx
@@ -60,12 +60,14 @@ export const Step1 = observer(({model}: {model: CreateAccountModel}) => {
         This is the company that keeps you online.
       </Text>
       <Option
+        testID="blueskyServerBtn"
         isSelected={isDefaultSelected}
         label="Bluesky"
         help="&nbsp;(default)"
         onPress={onPressDefault}
       />
       <Option
+        testID="otherServerBtn"
         isSelected={!isDefaultSelected}
         label="Other"
         onPress={onPressOther}>
@@ -74,6 +76,7 @@ export const Step1 = observer(({model}: {model: CreateAccountModel}) => {
             Enter the address of your provider:
           </Text>
           <TextInput
+            testID="customServerInput"
             icon="globe"
             placeholder="Hosting provider address"
             value={model.serviceUrl}
@@ -83,12 +86,14 @@ export const Step1 = observer(({model}: {model: CreateAccountModel}) => {
           {LOGIN_INCLUDE_DEV_SERVERS && (
             <View style={[s.flexRow, s.mt10]}>
               <Button
+                testID="stagingServerBtn"
                 type="default"
                 style={s.mr5}
                 label="Staging"
                 onPress={() => onDebugChangeServiceUrl(STAGING_SERVICE)}
               />
               <Button
+                testID="localDevServerBtn"
                 type="default"
                 label="Dev Server"
                 onPress={() => onDebugChangeServiceUrl(LOCAL_DEV_SERVICE)}
@@ -112,11 +117,13 @@ function Option({
   label,
   help,
   onPress,
+  testID,
 }: React.PropsWithChildren<{
   isSelected: boolean
   label: string
   help?: string
   onPress: () => void
+  testID?: string
 }>) {
   const theme = useTheme()
   const pal = usePalette('default')
@@ -129,7 +136,7 @@ function Option({
 
   return (
     <View style={[styles.option, pal.border]}>
-      <TouchableWithoutFeedback onPress={onPress}>
+      <TouchableWithoutFeedback onPress={onPress} testID={testID}>
         <View style={styles.optionHeading}>
           <View style={[styles.circle, pal.border]}>
             {isSelected ? (
diff --git a/src/view/com/auth/create/Step2.tsx b/src/view/com/auth/create/Step2.tsx
index f115bf6ac..8df997bd3 100644
--- a/src/view/com/auth/create/Step2.tsx
+++ b/src/view/com/auth/create/Step2.tsx
@@ -59,6 +59,7 @@ export const Step2 = observer(({model}: {model: CreateAccountModel}) => {
               Email address
             </Text>
             <TextInput
+              testID="emailInput"
               icon="envelope"
               placeholder="Enter your email address"
               value={model.email}
@@ -72,6 +73,7 @@ export const Step2 = observer(({model}: {model: CreateAccountModel}) => {
               Password
             </Text>
             <TextInput
+              testID="passwordInput"
               icon="lock"
               placeholder="Choose your password"
               value={model.password}
@@ -86,7 +88,7 @@ export const Step2 = observer(({model}: {model: CreateAccountModel}) => {
               Legal check
             </Text>
             <TouchableOpacity
-              testID="registerIs13Input"
+              testID="is13Input"
               style={[styles.toggleBtn, pal.border]}
               onPress={() => model.setIs13(!model.is13)}>
               <View style={[pal.borderDark, styles.checkbox]}>
diff --git a/src/view/com/auth/create/Step3.tsx b/src/view/com/auth/create/Step3.tsx
index 652591171..13ab39a10 100644
--- a/src/view/com/auth/create/Step3.tsx
+++ b/src/view/com/auth/create/Step3.tsx
@@ -17,6 +17,7 @@ export const Step3 = observer(({model}: {model: CreateAccountModel}) => {
       <StepHeader step="3" title="Your user handle" />
       <View style={s.pb10}>
         <TextInput
+          testID="handleInput"
           icon="at"
           placeholder="eg alice"
           value={model.handle}
diff --git a/src/view/com/auth/login/Login.tsx b/src/view/com/auth/login/Login.tsx
index f99e72daa..eff1642f0 100644
--- a/src/view/com/auth/login/Login.tsx
+++ b/src/view/com/auth/login/Login.tsx
@@ -13,7 +13,7 @@ import {
   FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
 import * as EmailValidator from 'email-validator'
-import AtpAgent from '@atproto/api'
+import {BskyAgent} from '@atproto/api'
 import {useAnalytics} from 'lib/analytics'
 import {Text} from '../../util/text/Text'
 import {UserAvatar} from '../../util/UserAvatar'
@@ -506,8 +506,8 @@ const ForgotPasswordForm = ({
     setIsProcessing(true)
 
     try {
-      const agent = new AtpAgent({service: serviceUrl})
-      await agent.api.com.atproto.account.requestPasswordReset({email})
+      const agent = new BskyAgent({service: serviceUrl})
+      await agent.com.atproto.server.requestPasswordReset({email})
       onEmailSent()
     } catch (e: any) {
       const errMsg = e.toString()
@@ -648,8 +648,8 @@ const SetNewPasswordForm = ({
     setIsProcessing(true)
 
     try {
-      const agent = new AtpAgent({service: serviceUrl})
-      await agent.api.com.atproto.account.resetPassword({
+      const agent = new BskyAgent({service: serviceUrl})
+      await agent.com.atproto.server.resetPassword({
         token: resetCode,
         password,
       })
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 572eea927..6009debdd 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -1,4 +1,4 @@
-import React, {useEffect, useRef, useState} from 'react'
+import React from 'react'
 import {observer} from 'mobx-react-lite'
 import {
   ActivityIndicator,
@@ -13,6 +13,7 @@ import {
 } from 'react-native'
 import LinearGradient from 'react-native-linear-gradient'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {RichText} from '@atproto/api'
 import {useAnalytics} from 'lib/analytics'
 import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view'
 import {ExternalEmbed} from './ExternalEmbed'
@@ -30,11 +31,11 @@ import {SelectPhotoBtn} from './photos/SelectPhotoBtn'
 import {OpenCameraBtn} from './photos/OpenCameraBtn'
 import {SelectedPhotos} from './photos/SelectedPhotos'
 import {usePalette} from 'lib/hooks/usePalette'
-import QuoteEmbed from '../util/PostEmbeds/QuoteEmbed'
+import QuoteEmbed from '../util/post-embeds/QuoteEmbed'
 import {useExternalLinkFetch} from './useExternalLinkFetch'
 import {isDesktopWeb} from 'platform/detection'
 
-const MAX_TEXT_LENGTH = 256
+const MAX_GRAPHEME_LENGTH = 300
 
 export const ComposePost = observer(function ComposePost({
   replyTo,
@@ -50,17 +51,23 @@ export const ComposePost = observer(function ComposePost({
   const {track} = useAnalytics()
   const pal = usePalette('default')
   const store = useStores()
-  const textInput = useRef<TextInputRef>(null)
-  const [isProcessing, setIsProcessing] = useState(false)
-  const [processingState, setProcessingState] = useState('')
-  const [error, setError] = useState('')
-  const [text, setText] = useState('')
-  const [quote, setQuote] = useState<ComposerOpts['quote'] | undefined>(
+  const textInput = React.useRef<TextInputRef>(null)
+  const [isProcessing, setIsProcessing] = React.useState(false)
+  const [processingState, setProcessingState] = React.useState('')
+  const [error, setError] = React.useState('')
+  const [richtext, setRichText] = React.useState(new RichText({text: ''}))
+  const graphemeLength = React.useMemo(
+    () => richtext.graphemeLength,
+    [richtext],
+  )
+  const [quote, setQuote] = React.useState<ComposerOpts['quote'] | undefined>(
     initQuote,
   )
   const {extLink, setExtLink} = useExternalLinkFetch({setQuote})
-  const [suggestedLinks, setSuggestedLinks] = useState<Set<string>>(new Set())
-  const [selectedPhotos, setSelectedPhotos] = useState<string[]>([])
+  const [suggestedLinks, setSuggestedLinks] = React.useState<Set<string>>(
+    new Set(),
+  )
+  const [selectedPhotos, setSelectedPhotos] = React.useState<string[]>([])
 
   const autocompleteView = React.useMemo<UserAutocompleteViewModel>(
     () => new UserAutocompleteViewModel(store),
@@ -78,11 +85,11 @@ export const ComposePost = observer(function ComposePost({
   }, [textInput, onClose])
 
   // initial setup
-  useEffect(() => {
+  React.useEffect(() => {
     autocompleteView.setup()
   }, [autocompleteView])
 
-  useEffect(() => {
+  React.useEffect(() => {
     // HACK
     // wait a moment before focusing the input to resolve some layout bugs with the keyboard-avoiding-view
     // -prf
@@ -132,18 +139,18 @@ export const ComposePost = observer(function ComposePost({
     if (isProcessing) {
       return
     }
-    if (text.length > MAX_TEXT_LENGTH) {
+    if (richtext.graphemeLength > MAX_GRAPHEME_LENGTH) {
       return
     }
     setError('')
-    if (text.trim().length === 0 && selectedPhotos.length === 0) {
+    if (richtext.text.trim().length === 0 && selectedPhotos.length === 0) {
       setError('Did you want to say anything?')
       return false
     }
     setIsProcessing(true)
     try {
       await apilib.post(store, {
-        rawText: text,
+        rawText: richtext.text,
         replyTo: replyTo?.uri,
         images: selectedPhotos,
         quote: quote,
@@ -172,7 +179,7 @@ export const ComposePost = observer(function ComposePost({
     Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`)
   }, [
     isProcessing,
-    text,
+    richtext,
     setError,
     setIsProcessing,
     replyTo,
@@ -187,7 +194,7 @@ export const ComposePost = observer(function ComposePost({
     track,
   ])
 
-  const canPost = text.length <= MAX_TEXT_LENGTH
+  const canPost = graphemeLength <= MAX_GRAPHEME_LENGTH
 
   const selectTextInputPlaceholder = replyTo
     ? 'Write your reply'
@@ -215,7 +222,7 @@ export const ComposePost = observer(function ComposePost({
               </View>
             ) : canPost ? (
               <TouchableOpacity
-                testID="composerPublishButton"
+                testID="composerPublishBtn"
                 onPress={onPressPublish}>
                 <LinearGradient
                   colors={[gradients.blueLight.start, gradients.blueLight.end]}
@@ -271,42 +278,41 @@ export const ComposePost = observer(function ComposePost({
               <UserAvatar avatar={store.me.avatar} size={50} />
               <TextInput
                 ref={textInput}
-                text={text}
+                richtext={richtext}
                 placeholder={selectTextInputPlaceholder}
                 suggestedLinks={suggestedLinks}
                 autocompleteView={autocompleteView}
-                onTextChanged={setText}
+                setRichText={setRichText}
                 onPhotoPasted={onPhotoPasted}
                 onSuggestedLinksChanged={setSuggestedLinks}
                 onError={setError}
               />
             </View>
 
-            {quote ? (
-              <View style={s.mt5}>
-                <QuoteEmbed quote={quote} />
-              </View>
-            ) : undefined}
-
             <SelectedPhotos
               selectedPhotos={selectedPhotos}
               onSelectPhotos={onSelectPhotos}
             />
-            {!selectedPhotos.length && extLink && (
+            {selectedPhotos.length === 0 && extLink && (
               <ExternalEmbed
                 link={extLink}
                 onRemove={() => setExtLink(undefined)}
               />
             )}
+            {quote ? (
+              <View style={s.mt5}>
+                <QuoteEmbed quote={quote} />
+              </View>
+            ) : undefined}
           </ScrollView>
           {!extLink &&
           selectedPhotos.length === 0 &&
-          suggestedLinks.size > 0 &&
-          !quote ? (
+          suggestedLinks.size > 0 ? (
             <View style={s.mb5}>
               {Array.from(suggestedLinks).map(url => (
                 <TouchableOpacity
                   key={`suggested-${url}`}
+                  testID="addLinkCardBtn"
                   style={[pal.borderDark, styles.addExtLinkBtn]}
                   onPress={() => onPressAddLinkCard(url)}>
                   <Text style={pal.text}>
@@ -318,17 +324,17 @@ export const ComposePost = observer(function ComposePost({
           ) : null}
           <View style={[pal.border, styles.bottomBar]}>
             <SelectPhotoBtn
-              enabled={!quote && selectedPhotos.length < 4}
+              enabled={selectedPhotos.length < 4}
               selectedPhotos={selectedPhotos}
               onSelectPhotos={setSelectedPhotos}
             />
             <OpenCameraBtn
-              enabled={!quote && selectedPhotos.length < 4}
+              enabled={selectedPhotos.length < 4}
               selectedPhotos={selectedPhotos}
               onSelectPhotos={setSelectedPhotos}
             />
             <View style={s.flex1} />
-            <CharProgress count={text.length} />
+            <CharProgress count={graphemeLength} />
           </View>
         </SafeAreaView>
       </TouchableWithoutFeedback>
@@ -408,6 +414,7 @@ const styles = StyleSheet.create({
     borderRadius: 24,
     paddingHorizontal: 16,
     paddingVertical: 12,
+    marginHorizontal: 10,
     marginBottom: 4,
   },
   bottomBar: {
diff --git a/src/view/com/composer/char-progress/CharProgress.tsx b/src/view/com/composer/char-progress/CharProgress.tsx
index b17cad1ba..eaaaea5e5 100644
--- a/src/view/com/composer/char-progress/CharProgress.tsx
+++ b/src/view/com/composer/char-progress/CharProgress.tsx
@@ -8,26 +8,24 @@ import ProgressPie from 'react-native-progress/Pie'
 import {s} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 
-const MAX_TEXT_LENGTH = 256
-const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH
+const MAX_LENGTH = 300
+const DANGER_LENGTH = MAX_LENGTH
 
 export function CharProgress({count}: {count: number}) {
   const pal = usePalette('default')
-  const textColor = count > DANGER_TEXT_LENGTH ? '#e60000' : pal.colors.text
-  const circleColor = count > DANGER_TEXT_LENGTH ? '#e60000' : pal.colors.link
+  const textColor = count > DANGER_LENGTH ? '#e60000' : pal.colors.text
+  const circleColor = count > DANGER_LENGTH ? '#e60000' : pal.colors.link
   return (
     <>
-      <Text style={[s.mr10, {color: textColor}]}>
-        {MAX_TEXT_LENGTH - count}
-      </Text>
+      <Text style={[s.mr10, {color: textColor}]}>{MAX_LENGTH - count}</Text>
       <View>
-        {count > DANGER_TEXT_LENGTH ? (
+        {count > DANGER_LENGTH ? (
           <ProgressPie
             size={30}
             borderWidth={4}
             borderColor={circleColor}
             color={circleColor}
-            progress={Math.min((count - MAX_TEXT_LENGTH) / MAX_TEXT_LENGTH, 1)}
+            progress={Math.min((count - MAX_LENGTH) / MAX_LENGTH, 1)}
           />
         ) : (
           <ProgressCircle
@@ -35,7 +33,7 @@ export function CharProgress({count}: {count: number}) {
             borderWidth={1}
             borderColor={pal.colors.border}
             color={circleColor}
-            progress={count / MAX_TEXT_LENGTH}
+            progress={count / MAX_LENGTH}
           />
         )}
       </View>
diff --git a/src/view/com/composer/photos/OpenCameraBtn.tsx b/src/view/com/composer/photos/OpenCameraBtn.tsx
index cf4a4c7d1..118728781 100644
--- a/src/view/com/composer/photos/OpenCameraBtn.tsx
+++ b/src/view/com/composer/photos/OpenCameraBtn.tsx
@@ -76,7 +76,11 @@ export function OpenCameraBtn({
       hitSlop={HITSLOP}>
       <FontAwesomeIcon
         icon="camera"
-        style={(enabled ? pal.link : pal.textLight) as FontAwesomeIconStyle}
+        style={
+          (enabled
+            ? pal.link
+            : [pal.textLight, s.dimmed]) as FontAwesomeIconStyle
+        }
         size={24}
       />
     </TouchableOpacity>
diff --git a/src/view/com/composer/photos/SelectPhotoBtn.tsx b/src/view/com/composer/photos/SelectPhotoBtn.tsx
index bdcb0534a..888118a85 100644
--- a/src/view/com/composer/photos/SelectPhotoBtn.tsx
+++ b/src/view/com/composer/photos/SelectPhotoBtn.tsx
@@ -86,7 +86,11 @@ export function SelectPhotoBtn({
       hitSlop={HITSLOP}>
       <FontAwesomeIcon
         icon={['far', 'image']}
-        style={(enabled ? pal.link : pal.textLight) as FontAwesomeIconStyle}
+        style={
+          (enabled
+            ? pal.link
+            : [pal.textLight, s.dimmed]) as FontAwesomeIconStyle
+        }
         size={24}
       />
     </TouchableOpacity>
diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx
index e72b41f0a..393d168fe 100644
--- a/src/view/com/composer/text-input/TextInput.tsx
+++ b/src/view/com/composer/text-input/TextInput.tsx
@@ -9,13 +9,13 @@ import PasteInput, {
   PastedFile,
   PasteInputRef,
 } from '@mattermost/react-native-paste-input'
+import {AppBskyRichtextFacet, RichText} from '@atproto/api'
 import isEqual from 'lodash.isequal'
 import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view'
 import {Autocomplete} from './mobile/Autocomplete'
 import {Text} from 'view/com/util/text/Text'
 import {useStores} from 'state/index'
 import {cleanError} from 'lib/strings/errors'
-import {detectLinkables, extractEntities} from 'lib/strings/rich-text-detection'
 import {getImageDim} from 'lib/media/manip'
 import {cropAndCompressFlow} from 'lib/media/picker'
 import {getMentionAt, insertMentionAt} from 'lib/strings/mention-manip'
@@ -33,11 +33,11 @@ export interface TextInputRef {
 }
 
 interface TextInputProps {
-  text: string
+  richtext: RichText
   placeholder: string
   suggestedLinks: Set<string>
   autocompleteView: UserAutocompleteViewModel
-  onTextChanged: (v: string) => void
+  setRichText: (v: RichText) => void
   onPhotoPasted: (uri: string) => void
   onSuggestedLinksChanged: (uris: Set<string>) => void
   onError: (err: string) => void
@@ -51,11 +51,11 @@ interface Selection {
 export const TextInput = React.forwardRef(
   (
     {
-      text,
+      richtext,
       placeholder,
       suggestedLinks,
       autocompleteView,
-      onTextChanged,
+      setRichText,
       onPhotoPasted,
       onSuggestedLinksChanged,
       onError,
@@ -92,7 +92,9 @@ export const TextInput = React.forwardRef(
 
     const onChangeText = React.useCallback(
       (newText: string) => {
-        onTextChanged(newText)
+        const newRt = new RichText({text: newText})
+        newRt.detectFacetsWithoutResolution()
+        setRichText(newRt)
 
         const prefix = getMentionAt(
           newText,
@@ -105,20 +107,21 @@ export const TextInput = React.forwardRef(
           autocompleteView.setActive(false)
         }
 
-        const ents = extractEntities(newText)?.filter(
-          ent => ent.type === 'link',
-        )
-        const set = new Set(ents ? ents.map(e => e.value) : [])
+        const set: Set<string> = new Set()
+        if (newRt.facets) {
+          for (const facet of newRt.facets) {
+            for (const feature of facet.features) {
+              if (AppBskyRichtextFacet.isLink(feature)) {
+                set.add(feature.uri)
+              }
+            }
+          }
+        }
         if (!isEqual(set, suggestedLinks)) {
           onSuggestedLinksChanged(set)
         }
       },
-      [
-        onTextChanged,
-        autocompleteView,
-        suggestedLinks,
-        onSuggestedLinksChanged,
-      ],
+      [setRichText, autocompleteView, suggestedLinks, onSuggestedLinksChanged],
     )
 
     const onPaste = React.useCallback(
@@ -159,31 +162,35 @@ export const TextInput = React.forwardRef(
     const onSelectAutocompleteItem = React.useCallback(
       (item: string) => {
         onChangeText(
-          insertMentionAt(text, textInputSelection.current?.start || 0, item),
+          insertMentionAt(
+            richtext.text,
+            textInputSelection.current?.start || 0,
+            item,
+          ),
         )
         autocompleteView.setActive(false)
       },
-      [onChangeText, text, autocompleteView],
+      [onChangeText, richtext, autocompleteView],
     )
 
     const textDecorated = React.useMemo(() => {
       let i = 0
-      return detectLinkables(text).map(v => {
-        if (typeof v === 'string') {
+      return Array.from(richtext.segments()).map(segment => {
+        if (!segment.facet) {
           return (
             <Text key={i++} style={[pal.text, styles.textInputFormatting]}>
-              {v}
+              {segment.text}
             </Text>
           )
         } else {
           return (
             <Text key={i++} style={[pal.link, styles.textInputFormatting]}>
-              {v.link}
+              {segment.text}
             </Text>
           )
         }
       })
-    }, [text, pal.link, pal.text])
+    }, [richtext, pal.link, pal.text])
 
     return (
       <View style={styles.container}>
diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx
index 4b23e891b..ad891fa5b 100644
--- a/src/view/com/composer/text-input/TextInput.web.tsx
+++ b/src/view/com/composer/text-input/TextInput.web.tsx
@@ -1,5 +1,6 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
+import {RichText} from '@atproto/api'
 import {useEditor, EditorContent, JSONContent} from '@tiptap/react'
 import {Document} from '@tiptap/extension-document'
 import {Link} from '@tiptap/extension-link'
@@ -17,11 +18,11 @@ export interface TextInputRef {
 }
 
 interface TextInputProps {
-  text: string
+  richtext: RichText
   placeholder: string
   suggestedLinks: Set<string>
   autocompleteView: UserAutocompleteViewModel
-  onTextChanged: (v: string) => void
+  setRichText: (v: RichText) => void
   onPhotoPasted: (uri: string) => void
   onSuggestedLinksChanged: (uris: Set<string>) => void
   onError: (err: string) => void
@@ -30,11 +31,11 @@ interface TextInputProps {
 export const TextInput = React.forwardRef(
   (
     {
-      text,
+      richtext,
       placeholder,
       suggestedLinks,
       autocompleteView,
-      onTextChanged,
+      setRichText,
       // onPhotoPasted, TODO
       onSuggestedLinksChanged,
     }: // onError, TODO
@@ -60,15 +61,15 @@ export const TextInput = React.forwardRef(
         }),
         Text,
       ],
-      content: text,
+      content: richtext.text.toString(),
       autofocus: true,
       editable: true,
       injectCSS: true,
       onUpdate({editor: editorProp}) {
         const json = editorProp.getJSON()
 
-        const newText = editorJsonToText(json).trim()
-        onTextChanged(newText)
+        const newRt = new RichText({text: editorJsonToText(json).trim()})
+        setRichText(newRt)
 
         const newSuggestedLinks = new Set(editorJsonToLinks(json))
         if (!isEqual(newSuggestedLinks, suggestedLinks)) {
diff --git a/src/view/com/discover/SuggestedFollows.tsx b/src/view/com/discover/SuggestedFollows.tsx
index 0d09038ba..e4ada5204 100644
--- a/src/view/com/discover/SuggestedFollows.tsx
+++ b/src/view/com/discover/SuggestedFollows.tsx
@@ -1,6 +1,6 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
-import {AppBskyActorRef, AppBskyActorProfile} from '@atproto/api'
+import {AppBskyActorDefs} from '@atproto/api'
 import {RefWithInfoAndFollowers} from 'state/models/discovery/foafs'
 import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
 import {Text} from '../util/text/Text'
@@ -12,9 +12,9 @@ export const SuggestedFollows = ({
 }: {
   title: string
   suggestions: (
-    | AppBskyActorRef.WithInfo
+    | AppBskyActorDefs.ProfileViewBasic
+    | AppBskyActorDefs.ProfileView
     | RefWithInfoAndFollowers
-    | AppBskyActorProfile.View
   )[]
 }) => {
   const pal = usePalette('default')
@@ -28,7 +28,6 @@ export const SuggestedFollows = ({
           <ProfileCardWithFollowBtn
             key={item.did}
             did={item.did}
-            declarationCid={item.declaration.cid}
             handle={item.handle}
             displayName={item.displayName}
             avatar={item.avatar}
@@ -36,12 +35,12 @@ export const SuggestedFollows = ({
             noBorder
             description={
               item.description
-                ? (item as AppBskyActorProfile.View).description
+                ? (item as AppBskyActorDefs.ProfileView).description
                 : ''
             }
             followers={
               item.followers
-                ? (item.followers as AppBskyActorProfile.View[])
+                ? (item.followers as AppBskyActorDefs.ProfileView[])
                 : undefined
             }
           />
diff --git a/src/view/com/modals/ChangeHandle.tsx b/src/view/com/modals/ChangeHandle.tsx
index f15f7ca43..37bad6957 100644
--- a/src/view/com/modals/ChangeHandle.tsx
+++ b/src/view/com/modals/ChangeHandle.tsx
@@ -105,7 +105,7 @@ export function Component({onChanged}: {onChanged: () => void}) {
       track('EditHandle:SetNewHandle')
       const newHandle = isCustom ? handle : createFullHandle(handle, userDomain)
       store.log.debug(`Updating handle to ${newHandle}`)
-      await store.api.com.atproto.handle.update({
+      await store.agent.updateHandle({
         handle: newHandle,
       })
       store.shell.closeModal()
@@ -310,7 +310,7 @@ function CustomHandleForm({
     try {
       setIsVerifying(true)
       setError('')
-      const res = await store.api.com.atproto.handle.resolve({handle})
+      const res = await store.agent.com.atproto.identity.resolveHandle({handle})
       if (res.data.did === store.me.did) {
         setCanSave(true)
       } else {
@@ -331,7 +331,7 @@ function CustomHandleForm({
     canSave,
     onPressSave,
     store.log,
-    store.api,
+    store.agent,
   ])
 
   // rendering
diff --git a/src/view/com/modals/Confirm.tsx b/src/view/com/modals/Confirm.tsx
index 60c104f99..2bfcf4118 100644
--- a/src/view/com/modals/Confirm.tsx
+++ b/src/view/com/modals/Confirm.tsx
@@ -39,7 +39,7 @@ export function Component({
     }
   }
   return (
-    <View style={[s.flex1, s.pl10, s.pr10]}>
+    <View testID="confirmModal" style={[s.flex1, s.pl10, s.pr10]}>
       <Text style={styles.title}>{title}</Text>
       {typeof message === 'string' ? (
         <Text style={styles.description}>{message}</Text>
@@ -56,7 +56,7 @@ export function Component({
           <ActivityIndicator />
         </View>
       ) : (
-        <TouchableOpacity style={s.mt10} onPress={onPress}>
+        <TouchableOpacity testID="confirmBtn" style={s.mt10} onPress={onPress}>
           <LinearGradient
             colors={[gradients.blueLight.start, gradients.blueLight.end]}
             start={{x: 0, y: 0}}
diff --git a/src/view/com/modals/DeleteAccount.tsx b/src/view/com/modals/DeleteAccount.tsx
index 23cd9eb82..353122163 100644
--- a/src/view/com/modals/DeleteAccount.tsx
+++ b/src/view/com/modals/DeleteAccount.tsx
@@ -32,7 +32,7 @@ export function Component({}: {}) {
     setError('')
     setIsProcessing(true)
     try {
-      await store.api.com.atproto.account.requestDelete()
+      await store.agent.com.atproto.server.requestAccountDelete()
       setIsEmailSent(true)
     } catch (e: any) {
       setError(cleanError(e))
@@ -43,7 +43,7 @@ export function Component({}: {}) {
     setError('')
     setIsProcessing(true)
     try {
-      await store.api.com.atproto.account.delete({
+      await store.agent.com.atproto.server.deleteAccount({
         did: store.me.did,
         password,
         token: confirmCode,
diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx
index 6eb21d17d..0b81d7f39 100644
--- a/src/view/com/modals/EditProfile.tsx
+++ b/src/view/com/modals/EditProfile.tsx
@@ -123,7 +123,7 @@ export function Component({
   }
 
   return (
-    <View style={[s.flex1, pal.view]}>
+    <View style={[s.flex1, pal.view]} testID="editProfileModal">
       <ScrollView style={styles.inner}>
         <Text style={[styles.title, pal.text]}>Edit my profile</Text>
         <View style={styles.photos}>
@@ -147,6 +147,7 @@ export function Component({
         <View>
           <Text style={[styles.label, pal.text]}>Display Name</Text>
           <TextInput
+            testID="editProfileDisplayNameInput"
             style={[styles.textInput, pal.text]}
             placeholder="e.g. Alice Roberts"
             placeholderTextColor={colors.gray4}
@@ -157,6 +158,7 @@ export function Component({
         <View style={s.pb10}>
           <Text style={[styles.label, pal.text]}>Description</Text>
           <TextInput
+            testID="editProfileDescriptionInput"
             style={[styles.textArea, pal.text]}
             placeholder="e.g. Artist, dog-lover, and memelord."
             placeholderTextColor={colors.gray4}
@@ -171,7 +173,10 @@ export function Component({
             <ActivityIndicator />
           </View>
         ) : (
-          <TouchableOpacity style={s.mt10} onPress={onPressSave}>
+          <TouchableOpacity
+            testID="editProfileSaveBtn"
+            style={s.mt10}
+            onPress={onPressSave}>
             <LinearGradient
               colors={[gradients.blueLight.start, gradients.blueLight.end]}
               start={{x: 0, y: 0}}
@@ -181,7 +186,10 @@ export function Component({
             </LinearGradient>
           </TouchableOpacity>
         )}
-        <TouchableOpacity style={s.mt5} onPress={onPressCancel}>
+        <TouchableOpacity
+          testID="editProfileCancelBtn"
+          style={s.mt5}
+          onPress={onPressCancel}>
           <View style={[styles.btn]}>
             <Text style={[s.black, s.bold, pal.text]}>Cancel</Text>
           </View>
diff --git a/src/view/com/modals/ReportAccount.tsx b/src/view/com/modals/ReportAccount.tsx
index c9ee004b8..601bccbd1 100644
--- a/src/view/com/modals/ReportAccount.tsx
+++ b/src/view/com/modals/ReportAccount.tsx
@@ -5,7 +5,7 @@ import {
   TouchableOpacity,
   View,
 } from 'react-native'
-import {ComAtprotoReportReasonType} from '@atproto/api'
+import {ComAtprotoModerationDefs} from '@atproto/api'
 import LinearGradient from 'react-native-linear-gradient'
 import {useStores} from 'state/index'
 import {s, colors, gradients} from 'lib/styles'
@@ -39,16 +39,16 @@ export function Component({did}: {did: string}) {
     setIsProcessing(true)
     try {
       // NOTE: we should update the lexicon of reasontype to include more options -prf
-      let reasonType = ComAtprotoReportReasonType.OTHER
+      let reasonType = ComAtprotoModerationDefs.REASONOTHER
       if (issue === 'spam') {
-        reasonType = ComAtprotoReportReasonType.SPAM
+        reasonType = ComAtprotoModerationDefs.REASONSPAM
       }
       const reason = ITEMS.find(item => item.key === issue)?.label || ''
-      await store.api.com.atproto.report.create({
+      await store.agent.com.atproto.moderation.createReport({
         reasonType,
         reason,
         subject: {
-          $type: 'com.atproto.repo.repoRef',
+          $type: 'com.atproto.admin.defs#repoRef',
           did,
         },
       })
@@ -61,12 +61,18 @@ export function Component({did}: {did: string}) {
     }
   }
   return (
-    <View style={[s.flex1, s.pl10, s.pr10, pal.view]}>
+    <View
+      testID="reportAccountModal"
+      style={[s.flex1, s.pl10, s.pr10, pal.view]}>
       <Text style={[pal.text, styles.title]}>Report account</Text>
       <Text style={[pal.textLight, styles.description]}>
         What is the issue with this account?
       </Text>
-      <RadioGroup items={ITEMS} onSelect={onSelectIssue} />
+      <RadioGroup
+        testID="reportAccountRadios"
+        items={ITEMS}
+        onSelect={onSelectIssue}
+      />
       {error ? (
         <View style={s.mt10}>
           <ErrorMessage message={error} />
@@ -77,7 +83,10 @@ export function Component({did}: {did: string}) {
           <ActivityIndicator />
         </View>
       ) : issue ? (
-        <TouchableOpacity style={s.mt10} onPress={onPress}>
+        <TouchableOpacity
+          testID="sendReportBtn"
+          style={s.mt10}
+          onPress={onPress}>
           <LinearGradient
             colors={[gradients.blueLight.start, gradients.blueLight.end]}
             start={{x: 0, y: 0}}
diff --git a/src/view/com/modals/ReportPost.tsx b/src/view/com/modals/ReportPost.tsx
index 3e876c6c8..01a132af0 100644
--- a/src/view/com/modals/ReportPost.tsx
+++ b/src/view/com/modals/ReportPost.tsx
@@ -5,7 +5,7 @@ import {
   TouchableOpacity,
   View,
 } from 'react-native'
-import {ComAtprotoReportReasonType} from '@atproto/api'
+import {ComAtprotoModerationDefs} from '@atproto/api'
 import LinearGradient from 'react-native-linear-gradient'
 import {useStores} from 'state/index'
 import {s, colors, gradients} from 'lib/styles'
@@ -46,16 +46,16 @@ export function Component({
     setIsProcessing(true)
     try {
       // NOTE: we should update the lexicon of reasontype to include more options -prf
-      let reasonType = ComAtprotoReportReasonType.OTHER
+      let reasonType = ComAtprotoModerationDefs.REASONOTHER
       if (issue === 'spam') {
-        reasonType = ComAtprotoReportReasonType.SPAM
+        reasonType = ComAtprotoModerationDefs.REASONSPAM
       }
       const reason = ITEMS.find(item => item.key === issue)?.label || ''
-      await store.api.com.atproto.report.create({
+      await store.agent.createModerationReport({
         reasonType,
         reason,
         subject: {
-          $type: 'com.atproto.repo.recordRef',
+          $type: 'com.atproto.repo.strongRef',
           uri: postUri,
           cid: postCid,
         },
@@ -69,12 +69,16 @@ export function Component({
     }
   }
   return (
-    <View style={[s.flex1, s.pl10, s.pr10, pal.view]}>
+    <View testID="reportPostModal" style={[s.flex1, s.pl10, s.pr10, pal.view]}>
       <Text style={[pal.text, styles.title]}>Report post</Text>
       <Text style={[pal.textLight, styles.description]}>
         What is the issue with this post?
       </Text>
-      <RadioGroup items={ITEMS} onSelect={onSelectIssue} />
+      <RadioGroup
+        testID="reportPostRadios"
+        items={ITEMS}
+        onSelect={onSelectIssue}
+      />
       {error ? (
         <View style={s.mt10}>
           <ErrorMessage message={error} />
@@ -85,7 +89,10 @@ export function Component({
           <ActivityIndicator />
         </View>
       ) : issue ? (
-        <TouchableOpacity style={s.mt10} onPress={onPress}>
+        <TouchableOpacity
+          testID="sendReportBtn"
+          style={s.mt10}
+          onPress={onPress}>
           <LinearGradient
             colors={[gradients.blueLight.start, gradients.blueLight.end]}
             start={{x: 0, y: 0}}
diff --git a/src/view/com/modals/Repost.tsx b/src/view/com/modals/Repost.tsx
index b4669a046..d5ed66b70 100644
--- a/src/view/com/modals/Repost.tsx
+++ b/src/view/com/modals/Repost.tsx
@@ -26,22 +26,28 @@ export function Component({
   }
 
   return (
-    <View style={[s.flex1, pal.view, styles.container]}>
+    <View testID="repostModal" style={[s.flex1, pal.view, styles.container]}>
       <View style={s.pb20}>
-        <TouchableOpacity style={[styles.actionBtn]} onPress={onRepost}>
+        <TouchableOpacity
+          testID="repostBtn"
+          style={[styles.actionBtn]}
+          onPress={onRepost}>
           <RepostIcon strokeWidth={2} size={24} style={s.blue3} />
           <Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
             {!isReposted ? 'Repost' : 'Undo repost'}
           </Text>
         </TouchableOpacity>
-        <TouchableOpacity style={[styles.actionBtn]} onPress={onQuote}>
+        <TouchableOpacity
+          testID="quoteBtn"
+          style={[styles.actionBtn]}
+          onPress={onQuote}>
           <FontAwesomeIcon icon="quote-left" size={24} style={s.blue3} />
           <Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
             Quote Post
           </Text>
         </TouchableOpacity>
       </View>
-      <TouchableOpacity onPress={onPress}>
+      <TouchableOpacity testID="cancelBtn" onPress={onPress}>
         <LinearGradient
           colors={[gradients.blueLight.start, gradients.blueLight.end]}
           start={{x: 0, y: 0}}
diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index 1c2299b03..7d584e8e6 100644
--- a/src/view/com/notifications/FeedItem.tsx
+++ b/src/view/com/notifications/FeedItem.tsx
@@ -47,10 +47,10 @@ export const FeedItem = observer(function FeedItem({
   const pal = usePalette('default')
   const [isAuthorsExpanded, setAuthorsExpanded] = React.useState<boolean>(false)
   const itemHref = React.useMemo(() => {
-    if (item.isUpvote || item.isRepost) {
+    if (item.isLike || item.isRepost) {
       const urip = new AtUri(item.subjectUri)
       return `/profile/${urip.host}/post/${urip.rkey}`
-    } else if (item.isFollow || item.isAssertion) {
+    } else if (item.isFollow) {
       return `/profile/${item.author.handle}`
     } else if (item.isReply) {
       const urip = new AtUri(item.uri)
@@ -59,9 +59,9 @@ export const FeedItem = observer(function FeedItem({
     return ''
   }, [item])
   const itemTitle = React.useMemo(() => {
-    if (item.isUpvote || item.isRepost) {
+    if (item.isLike || item.isRepost) {
       return 'Post'
-    } else if (item.isFollow || item.isAssertion) {
+    } else if (item.isFollow) {
       return item.author.handle
     } else if (item.isReply) {
       return 'Post'
@@ -77,7 +77,7 @@ export const FeedItem = observer(function FeedItem({
     return <View />
   }
 
-  if (item.isReply || item.isMention) {
+  if (item.isReply || item.isMention || item.isQuote) {
     if (item.additionalPost?.error) {
       // hide errors - it doesnt help the user to show them
       return <View />
@@ -103,7 +103,7 @@ export const FeedItem = observer(function FeedItem({
   let action = ''
   let icon: Props['icon'] | 'HeartIconSolid'
   let iconStyle: Props['style'] = []
-  if (item.isUpvote) {
+  if (item.isLike) {
     action = 'liked your post'
     icon = 'HeartIconSolid'
     iconStyle = [
@@ -114,9 +114,6 @@ export const FeedItem = observer(function FeedItem({
     action = 'reposted your post'
     icon = 'retweet'
     iconStyle = [s.green3 as FontAwesomeIconStyle]
-  } else if (item.isReply) {
-    action = 'replied to your post'
-    icon = ['far', 'comment']
   } else if (item.isFollow) {
     action = 'followed you'
     icon = 'user-plus'
@@ -208,7 +205,7 @@ export const FeedItem = observer(function FeedItem({
               </View>
             </View>
           </TouchableWithoutFeedback>
-          {item.isUpvote || item.isRepost ? (
+          {item.isLike || item.isRepost || item.isQuote ? (
             <AdditionalPostText additionalPost={item.additionalPost} />
           ) : (
             <></>
@@ -352,9 +349,9 @@ function AdditionalPostText({
     return <View />
   }
   const text = additionalPost.thread?.postRecord.text
-  const images = (
-    additionalPost.thread.post.embed as AppBskyEmbedImages.Presented
-  )?.images
+  const images = AppBskyEmbedImages.isView(additionalPost.thread.post.embed)
+    ? additionalPost.thread.post.embed.images
+    : undefined
   return (
     <>
       {text?.length > 0 && <Text style={pal.textLight}>{text}</Text>}
diff --git a/src/view/com/pager/FeedsTabBar.tsx b/src/view/com/pager/FeedsTabBar.tsx
index 9831218ec..76e0a6fc6 100644
--- a/src/view/com/pager/FeedsTabBar.tsx
+++ b/src/view/com/pager/FeedsTabBar.tsx
@@ -9,7 +9,9 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
 
 export const FeedsTabBar = observer(
-  (props: RenderTabBarFnProps & {onPressSelected: () => void}) => {
+  (
+    props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
+  ) => {
     const store = useStores()
     const pal = usePalette('default')
     const interp = useAnimatedValue(0)
@@ -32,7 +34,10 @@ export const FeedsTabBar = observer(
 
     return (
       <Animated.View style={[pal.view, styles.tabBar, transform]}>
-        <TouchableOpacity style={styles.tabBarAvi} onPress={onPressAvi}>
+        <TouchableOpacity
+          testID="viewHeaderDrawerBtn"
+          style={styles.tabBarAvi}
+          onPress={onPressAvi}>
           <UserAvatar avatar={store.me.avatar} size={30} />
         </TouchableOpacity>
         <TabBar
diff --git a/src/view/com/pager/Pager.tsx b/src/view/com/pager/Pager.tsx
index 416828a27..34747db6d 100644
--- a/src/view/com/pager/Pager.tsx
+++ b/src/view/com/pager/Pager.tsx
@@ -20,6 +20,7 @@ interface Props {
   initialPage?: number
   renderTabBar: RenderTabBarFn
   onPageSelected?: (index: number) => void
+  testID?: string
 }
 export const Pager = ({
   children,
@@ -27,6 +28,7 @@ export const Pager = ({
   initialPage = 0,
   renderTabBar,
   onPageSelected,
+  testID,
 }: React.PropsWithChildren<Props>) => {
   const [selectedPage, setSelectedPage] = React.useState(0)
   const position = useAnimatedValue(0)
@@ -49,7 +51,7 @@ export const Pager = ({
   )
 
   return (
-    <View>
+    <View testID={testID}>
       {tabBarPosition === 'top' &&
         renderTabBar({
           selectedPage,
diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx
index 0b45d95f5..2070898bf 100644
--- a/src/view/com/pager/TabBar.tsx
+++ b/src/view/com/pager/TabBar.tsx
@@ -15,6 +15,7 @@ interface Layout {
 }
 
 export interface TabBarProps {
+  testID?: string
   selectedPage: number
   items: string[]
   position: Animated.Value
@@ -26,6 +27,7 @@ export interface TabBarProps {
 }
 
 export function TabBar({
+  testID,
   selectedPage,
   items,
   position,
@@ -92,12 +94,15 @@ export function TabBar({
   }
 
   return (
-    <View style={[pal.view, styles.outer]} onLayout={onLayout}>
+    <View testID={testID} style={[pal.view, styles.outer]} onLayout={onLayout}>
       <Animated.View style={[styles.indicator, indicatorStyle]} />
       {items.map((item, i) => {
         const selected = i === selectedPage
         return (
-          <TouchableWithoutFeedback key={i} onPress={() => onPressItem(i)}>
+          <TouchableWithoutFeedback
+            key={i}
+            testID={testID ? `${testID}-${item}` : undefined}
+            onPress={() => onPressItem(i)}>
             <View
               style={
                 indicatorPosition === 'top' ? styles.itemTop : styles.itemBottom
diff --git a/src/view/com/post-thread/PostVotedBy.tsx b/src/view/com/post-thread/PostLikedBy.tsx
index f86798097..9fb46702e 100644
--- a/src/view/com/post-thread/PostVotedBy.tsx
+++ b/src/view/com/post-thread/PostLikedBy.tsx
@@ -2,24 +2,18 @@ import React, {useEffect} from 'react'
 import {observer} from 'mobx-react-lite'
 import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
 import {CenteredView, FlatList} from '../util/Views'
-import {VotesViewModel, VoteItem} from 'state/models/votes-view'
+import {LikesViewModel, LikeItem} from 'state/models/likes-view'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
 import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
 
-export const PostVotedBy = observer(function PostVotedBy({
-  uri,
-  direction,
-}: {
-  uri: string
-  direction: 'up' | 'down'
-}) {
+export const PostLikedBy = observer(function PostVotedBy({uri}: {uri: string}) {
   const pal = usePalette('default')
   const store = useStores()
   const view = React.useMemo(
-    () => new VotesViewModel(store, {uri, direction}),
-    [store, uri, direction],
+    () => new LikesViewModel(store, {uri}),
+    [store, uri],
   )
 
   useEffect(() => {
@@ -55,11 +49,10 @@ export const PostVotedBy = observer(function PostVotedBy({
 
   // loaded
   // =
-  const renderItem = ({item}: {item: VoteItem}) => (
+  const renderItem = ({item}: {item: LikeItem}) => (
     <ProfileCardWithFollowBtn
       key={item.actor.did}
       did={item.actor.did}
-      declarationCid={item.actor.declaration.cid}
       handle={item.actor.handle}
       displayName={item.actor.displayName}
       avatar={item.actor.avatar}
@@ -68,7 +61,7 @@ export const PostVotedBy = observer(function PostVotedBy({
   )
   return (
     <FlatList
-      data={view.votes}
+      data={view.likes}
       keyExtractor={item => item.actor.did}
       refreshControl={
         <RefreshControl
diff --git a/src/view/com/post-thread/PostRepostedBy.tsx b/src/view/com/post-thread/PostRepostedBy.tsx
index fda54469c..147d0271f 100644
--- a/src/view/com/post-thread/PostRepostedBy.tsx
+++ b/src/view/com/post-thread/PostRepostedBy.tsx
@@ -64,7 +64,6 @@ export const PostRepostedBy = observer(function PostRepostedBy({
     <ProfileCardWithFollowBtn
       key={item.did}
       did={item.did}
-      declarationCid={item.declaration.cid}
       handle={item.handle}
       displayName={item.displayName}
       avatar={item.avatar}
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index d0452331b..569c6e392 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -1,17 +1,30 @@
 import React, {useRef} from 'react'
 import {observer} from 'mobx-react-lite'
-import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
+import {
+  ActivityIndicator,
+  RefreshControl,
+  StyleSheet,
+  TouchableOpacity,
+  View,
+} from 'react-native'
 import {CenteredView, FlatList} from '../util/Views'
 import {
   PostThreadViewModel,
   PostThreadViewPostModel,
 } from 'state/models/post-thread-view'
+import {
+  FontAwesomeIcon,
+  FontAwesomeIconStyle,
+} from '@fortawesome/react-native-fontawesome'
 import {PostThreadItem} from './PostThreadItem'
 import {ComposePrompt} from '../composer/Prompt'
 import {ErrorMessage} from '../util/error/ErrorMessage'
+import {Text} from '../util/text/Text'
 import {s} from 'lib/styles'
 import {isDesktopWeb} from 'platform/detection'
 import {usePalette} from 'lib/hooks/usePalette'
+import {useNavigation} from '@react-navigation/native'
+import {NavigationProp} from 'lib/routes/types'
 
 const REPLY_PROMPT = {_reactKey: '__reply__', _isHighlightedPost: false}
 const BOTTOM_BORDER = {
@@ -32,6 +45,7 @@ export const PostThread = observer(function PostThread({
   const pal = usePalette('default')
   const ref = useRef<FlatList>(null)
   const [isRefreshing, setIsRefreshing] = React.useState(false)
+  const navigation = useNavigation<NavigationProp>()
   const posts = React.useMemo(() => {
     if (view.thread) {
       return Array.from(flattenThread(view.thread)).concat([BOTTOM_BORDER])
@@ -41,6 +55,7 @@ export const PostThread = observer(function PostThread({
 
   // events
   // =
+
   const onRefresh = React.useCallback(async () => {
     setIsRefreshing(true)
     try {
@@ -50,6 +65,7 @@ export const PostThread = observer(function PostThread({
     }
     setIsRefreshing(false)
   }, [view, setIsRefreshing])
+
   const onLayout = React.useCallback(() => {
     const index = posts.findIndex(post => post._isHighlightedPost)
     if (index !== -1) {
@@ -60,6 +76,7 @@ export const PostThread = observer(function PostThread({
       })
     }
   }, [posts, ref])
+
   const onScrollToIndexFailed = React.useCallback(
     (info: {
       index: number
@@ -73,6 +90,15 @@ export const PostThread = observer(function PostThread({
     },
     [ref],
   )
+
+  const onPressBack = React.useCallback(() => {
+    if (navigation.canGoBack()) {
+      navigation.goBack()
+    } else {
+      navigation.navigate('Home')
+    }
+  }, [navigation])
+
   const renderItem = React.useCallback(
     ({item}: {item: YieldedItem}) => {
       if (item === REPLY_PROMPT) {
@@ -104,6 +130,30 @@ export const PostThread = observer(function PostThread({
   // error
   // =
   if (view.hasError) {
+    if (view.notFound) {
+      return (
+        <CenteredView>
+          <View style={[pal.view, pal.border, styles.notFoundContainer]}>
+            <Text type="title-lg" style={[pal.text, s.mb5]}>
+              Post not found
+            </Text>
+            <Text type="md" style={[pal.text, s.mb10]}>
+              The post may have been deleted.
+            </Text>
+            <TouchableOpacity onPress={onPressBack}>
+              <Text type="2xl" style={pal.link}>
+                <FontAwesomeIcon
+                  icon="angle-left"
+                  style={[pal.link as FontAwesomeIconStyle, s.mr5]}
+                  size={14}
+                />
+                Back
+              </Text>
+            </TouchableOpacity>
+          </View>
+        </CenteredView>
+      )
+    }
     return (
       <CenteredView>
         <ErrorMessage message={view.error} onPressTryAgain={onRefresh} />
@@ -159,12 +209,18 @@ function* flattenThread(
         yield* flattenThread(reply as PostThreadViewPostModel)
       }
     }
-  } else if (!isAscending && !post.parent && post.post.replyCount > 0) {
+  } else if (!isAscending && !post.parent && post.post.replyCount) {
     post._hasMore = true
   }
 }
 
 const styles = StyleSheet.create({
+  notFoundContainer: {
+    margin: 10,
+    paddingHorizontal: 18,
+    paddingVertical: 14,
+    borderRadius: 6,
+  },
   bottomBorder: {
     borderBottomWidth: 1,
   },
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 17c7943d9..cf2148060 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -19,7 +19,7 @@ import {ago} from 'lib/strings/time'
 import {pluralize} from 'lib/strings/helpers'
 import {useStores} from 'state/index'
 import {PostMeta} from '../util/PostMeta'
-import {PostEmbeds} from '../util/PostEmbeds'
+import {PostEmbeds} from '../util/post-embeds'
 import {PostCtrls} from '../util/PostCtrls'
 import {PostMutedWrapper} from '../util/PostMuted'
 import {ErrorMessage} from '../util/error/ErrorMessage'
@@ -38,7 +38,7 @@ export const PostThreadItem = observer(function PostThreadItem({
   const store = useStores()
   const [deleted, setDeleted] = React.useState(false)
   const record = item.postRecord
-  const hasEngagement = item.post.upvoteCount || item.post.repostCount
+  const hasEngagement = item.post.likeCount || item.post.repostCount
 
   const itemUri = item.post.uri
   const itemCid = item.post.cid
@@ -49,11 +49,11 @@ export const PostThreadItem = observer(function PostThreadItem({
   const itemTitle = `Post by ${item.post.author.handle}`
   const authorHref = `/profile/${item.post.author.handle}`
   const authorTitle = item.post.author.handle
-  const upvotesHref = React.useMemo(() => {
+  const likesHref = React.useMemo(() => {
     const urip = new AtUri(item.post.uri)
-    return `/profile/${item.post.author.handle}/post/${urip.rkey}/upvoted-by`
+    return `/profile/${item.post.author.handle}/post/${urip.rkey}/liked-by`
   }, [item.post.uri, item.post.author.handle])
-  const upvotesTitle = 'Likes on this post'
+  const likesTitle = 'Likes on this post'
   const repostsHref = React.useMemo(() => {
     const urip = new AtUri(item.post.uri)
     return `/profile/${item.post.author.handle}/post/${urip.rkey}/reposted-by`
@@ -80,10 +80,10 @@ export const PostThreadItem = observer(function PostThreadItem({
       .toggleRepost()
       .catch(e => store.log.error('Failed to toggle repost', e))
   }, [item, store])
-  const onPressToggleUpvote = React.useCallback(() => {
+  const onPressToggleLike = React.useCallback(() => {
     return item
-      .toggleUpvote()
-      .catch(e => store.log.error('Failed to toggle upvote', e))
+      .toggleLike()
+      .catch(e => store.log.error('Failed to toggle like', e))
   }, [item, store])
   const onCopyPostText = React.useCallback(() => {
     Clipboard.setString(record?.text || '')
@@ -125,153 +125,151 @@ export const PostThreadItem = observer(function PostThreadItem({
 
   if (item._isHighlightedPost) {
     return (
-      <>
-        <View
-          style={[
-            styles.outer,
-            styles.outerHighlighted,
-            {borderTopColor: pal.colors.border},
-            pal.view,
-          ]}>
-          <View style={styles.layout}>
-            <View style={styles.layoutAvi}>
-              <Link href={authorHref} title={authorTitle} asAnchor>
-                <UserAvatar size={52} avatar={item.post.author.avatar} />
-              </Link>
-            </View>
-            <View style={styles.layoutContent}>
-              <View style={[styles.meta, styles.metaExpandedLine1]}>
-                <View style={[s.flexRow, s.alignBaseline]}>
-                  <Link
-                    style={styles.metaItem}
-                    href={authorHref}
-                    title={authorTitle}>
-                    <Text
-                      type="xl-bold"
-                      style={[pal.text]}
-                      numberOfLines={1}
-                      lineHeight={1.2}>
-                      {item.post.author.displayName || item.post.author.handle}
-                    </Text>
-                  </Link>
-                  <Text type="md" style={[styles.metaItem, pal.textLight]}>
-                    &middot; {ago(item.post.indexedAt)}
-                  </Text>
-                </View>
-                <View style={s.flex1} />
-                <PostDropdownBtn
-                  style={styles.metaItem}
-                  itemUri={itemUri}
-                  itemCid={itemCid}
-                  itemHref={itemHref}
-                  itemTitle={itemTitle}
-                  isAuthor={item.post.author.did === store.me.did}
-                  onCopyPostText={onCopyPostText}
-                  onOpenTranslate={onOpenTranslate}
-                  onDeletePost={onDeletePost}>
-                  <FontAwesomeIcon
-                    icon="ellipsis-h"
-                    size={14}
-                    style={[s.mt2, s.mr5, pal.textLight]}
-                  />
-                </PostDropdownBtn>
-              </View>
-              <View style={styles.meta}>
+      <View
+        testID={`postThreadItem-by-${item.post.author.handle}`}
+        style={[
+          styles.outer,
+          styles.outerHighlighted,
+          {borderTopColor: pal.colors.border},
+          pal.view,
+        ]}>
+        <View style={styles.layout}>
+          <View style={styles.layoutAvi}>
+            <Link href={authorHref} title={authorTitle} asAnchor>
+              <UserAvatar size={52} avatar={item.post.author.avatar} />
+            </Link>
+          </View>
+          <View style={styles.layoutContent}>
+            <View style={[styles.meta, styles.metaExpandedLine1]}>
+              <View style={[s.flexRow, s.alignBaseline]}>
                 <Link
                   style={styles.metaItem}
                   href={authorHref}
                   title={authorTitle}>
-                  <Text type="md" style={[pal.textLight]} numberOfLines={1}>
-                    @{item.post.author.handle}
+                  <Text
+                    type="xl-bold"
+                    style={[pal.text]}
+                    numberOfLines={1}
+                    lineHeight={1.2}>
+                    {item.post.author.displayName || item.post.author.handle}
                   </Text>
                 </Link>
+                <Text type="md" style={[styles.metaItem, pal.textLight]}>
+                  &middot; {ago(item.post.indexedAt)}
+                </Text>
               </View>
-            </View>
-          </View>
-          <View style={[s.pl10, s.pr10, s.pb10]}>
-            {item.richText?.text ? (
-              <View
-                style={[
-                  styles.postTextContainer,
-                  styles.postTextLargeContainer,
-                ]}>
-                <RichText
-                  type="post-text-lg"
-                  richText={item.richText}
-                  lineHeight={1.3}
-                />
-              </View>
-            ) : undefined}
-            <PostEmbeds embed={item.post.embed} style={s.mb10} />
-            {item._isHighlightedPost && hasEngagement ? (
-              <View style={[styles.expandedInfo, pal.border]}>
-                {item.post.repostCount ? (
-                  <Link
-                    style={styles.expandedInfoItem}
-                    href={repostsHref}
-                    title={repostsTitle}>
-                    <Text type="lg" style={pal.textLight}>
-                      <Text type="xl-bold" style={pal.text}>
-                        {item.post.repostCount}
-                      </Text>{' '}
-                      {pluralize(item.post.repostCount, 'repost')}
-                    </Text>
-                  </Link>
-                ) : (
-                  <></>
-                )}
-                {item.post.upvoteCount ? (
-                  <Link
-                    style={styles.expandedInfoItem}
-                    href={upvotesHref}
-                    title={upvotesTitle}>
-                    <Text type="lg" style={pal.textLight}>
-                      <Text type="xl-bold" style={pal.text}>
-                        {item.post.upvoteCount}
-                      </Text>{' '}
-                      {pluralize(item.post.upvoteCount, 'like')}
-                    </Text>
-                  </Link>
-                ) : (
-                  <></>
-                )}
-              </View>
-            ) : (
-              <></>
-            )}
-            <View style={[s.pl10, s.pb5]}>
-              <PostCtrls
-                big
+              <View style={s.flex1} />
+              <PostDropdownBtn
+                testID="postDropdownBtn"
+                style={styles.metaItem}
                 itemUri={itemUri}
                 itemCid={itemCid}
                 itemHref={itemHref}
                 itemTitle={itemTitle}
-                author={{
-                  avatar: item.post.author.avatar!,
-                  handle: item.post.author.handle,
-                  displayName: item.post.author.displayName!,
-                }}
-                text={item.richText?.text || record.text}
-                indexedAt={item.post.indexedAt}
                 isAuthor={item.post.author.did === store.me.did}
-                isReposted={!!item.post.viewer.repost}
-                isUpvoted={!!item.post.viewer.upvote}
-                onPressReply={onPressReply}
-                onPressToggleRepost={onPressToggleRepost}
-                onPressToggleUpvote={onPressToggleUpvote}
                 onCopyPostText={onCopyPostText}
                 onOpenTranslate={onOpenTranslate}
-                onDeletePost={onDeletePost}
+                onDeletePost={onDeletePost}>
+                <FontAwesomeIcon
+                  icon="ellipsis-h"
+                  size={14}
+                  style={[s.mt2, s.mr5, pal.textLight]}
+                />
+              </PostDropdownBtn>
+            </View>
+            <View style={styles.meta}>
+              <Link
+                style={styles.metaItem}
+                href={authorHref}
+                title={authorTitle}>
+                <Text type="md" style={[pal.textLight]} numberOfLines={1}>
+                  @{item.post.author.handle}
+                </Text>
+              </Link>
+            </View>
+          </View>
+        </View>
+        <View style={[s.pl10, s.pr10, s.pb10]}>
+          {item.richText?.text ? (
+            <View
+              style={[styles.postTextContainer, styles.postTextLargeContainer]}>
+              <RichText
+                type="post-text-lg"
+                richText={item.richText}
+                lineHeight={1.3}
               />
             </View>
+          ) : undefined}
+          <PostEmbeds embed={item.post.embed} style={s.mb10} />
+          {item._isHighlightedPost && hasEngagement ? (
+            <View style={[styles.expandedInfo, pal.border]}>
+              {item.post.repostCount ? (
+                <Link
+                  style={styles.expandedInfoItem}
+                  href={repostsHref}
+                  title={repostsTitle}>
+                  <Text testID="repostCount" type="lg" style={pal.textLight}>
+                    <Text type="xl-bold" style={pal.text}>
+                      {item.post.repostCount}
+                    </Text>{' '}
+                    {pluralize(item.post.repostCount, 'repost')}
+                  </Text>
+                </Link>
+              ) : (
+                <></>
+              )}
+              {item.post.likeCount ? (
+                <Link
+                  style={styles.expandedInfoItem}
+                  href={likesHref}
+                  title={likesTitle}>
+                  <Text testID="likeCount" type="lg" style={pal.textLight}>
+                    <Text type="xl-bold" style={pal.text}>
+                      {item.post.likeCount}
+                    </Text>{' '}
+                    {pluralize(item.post.likeCount, 'like')}
+                  </Text>
+                </Link>
+              ) : (
+                <></>
+              )}
+            </View>
+          ) : (
+            <></>
+          )}
+          <View style={[s.pl10, s.pb5]}>
+            <PostCtrls
+              big
+              itemUri={itemUri}
+              itemCid={itemCid}
+              itemHref={itemHref}
+              itemTitle={itemTitle}
+              author={{
+                avatar: item.post.author.avatar!,
+                handle: item.post.author.handle,
+                displayName: item.post.author.displayName!,
+              }}
+              text={item.richText?.text || record.text}
+              indexedAt={item.post.indexedAt}
+              isAuthor={item.post.author.did === store.me.did}
+              isReposted={!!item.post.viewer?.repost}
+              isLiked={!!item.post.viewer?.like}
+              onPressReply={onPressReply}
+              onPressToggleRepost={onPressToggleRepost}
+              onPressToggleLike={onPressToggleLike}
+              onCopyPostText={onCopyPostText}
+              onOpenTranslate={onOpenTranslate}
+              onDeletePost={onDeletePost}
+            />
           </View>
         </View>
-      </>
+      </View>
     )
   } else {
     return (
       <PostMutedWrapper isMuted={item.post.author.viewer?.muted === true}>
         <Link
+          testID={`postThreadItem-by-${item.post.author.handle}`}
           style={[styles.outer, {borderTopColor: pal.colors.border}, pal.view]}
           href={itemHref}
           title={itemTitle}
@@ -305,7 +303,6 @@ export const PostThreadItem = observer(function PostThreadItem({
                 timestamp={item.post.indexedAt}
                 postHref={itemHref}
                 did={item.post.author.did}
-                declarationCid={item.post.author.declaration.cid}
               />
               {item.richText?.text ? (
                 <View style={styles.postTextContainer}>
@@ -333,12 +330,12 @@ export const PostThreadItem = observer(function PostThreadItem({
                 isAuthor={item.post.author.did === store.me.did}
                 replyCount={item.post.replyCount}
                 repostCount={item.post.repostCount}
-                upvoteCount={item.post.upvoteCount}
-                isReposted={!!item.post.viewer.repost}
-                isUpvoted={!!item.post.viewer.upvote}
+                likeCount={item.post.likeCount}
+                isReposted={!!item.post.viewer?.repost}
+                isLiked={!!item.post.viewer?.like}
                 onPressReply={onPressReply}
                 onPressToggleRepost={onPressToggleRepost}
-                onPressToggleUpvote={onPressToggleUpvote}
+                onPressToggleLike={onPressToggleLike}
                 onCopyPostText={onCopyPostText}
                 onOpenTranslate={onOpenTranslate}
                 onDeletePost={onDeletePost}
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index a6c66d143..6b3dc3ac6 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -15,7 +15,7 @@ import {PostThreadViewModel} from 'state/models/post-thread-view'
 import {Link} from '../util/Link'
 import {UserInfoText} from '../util/UserInfoText'
 import {PostMeta} from '../util/PostMeta'
-import {PostEmbeds} from '../util/PostEmbeds'
+import {PostEmbeds} from '../util/post-embeds'
 import {PostCtrls} from '../util/PostCtrls'
 import {PostMutedWrapper} from '../util/PostMuted'
 import {Text} from '../util/text/Text'
@@ -118,10 +118,10 @@ export const Post = observer(function Post({
       .toggleRepost()
       .catch(e => store.log.error('Failed to toggle repost', e))
   }
-  const onPressToggleUpvote = () => {
+  const onPressToggleLike = () => {
     return item
-      .toggleUpvote()
-      .catch(e => store.log.error('Failed to toggle upvote', e))
+      .toggleLike()
+      .catch(e => store.log.error('Failed to toggle like', e))
   }
   const onCopyPostText = () => {
     Clipboard.setString(record.text)
@@ -166,7 +166,6 @@ export const Post = observer(function Post({
               timestamp={item.post.indexedAt}
               postHref={itemHref}
               did={item.post.author.did}
-              declarationCid={item.post.author.declaration.cid}
             />
             {replyAuthorDid !== '' && (
               <View style={[s.flexRow, s.mb2, s.alignCenter]}>
@@ -211,12 +210,12 @@ export const Post = observer(function Post({
               isAuthor={item.post.author.did === store.me.did}
               replyCount={item.post.replyCount}
               repostCount={item.post.repostCount}
-              upvoteCount={item.post.upvoteCount}
-              isReposted={!!item.post.viewer.repost}
-              isUpvoted={!!item.post.viewer.upvote}
+              likeCount={item.post.likeCount}
+              isReposted={!!item.post.viewer?.repost}
+              isLiked={!!item.post.viewer?.like}
               onPressReply={onPressReply}
               onPressToggleRepost={onPressToggleRepost}
-              onPressToggleUpvote={onPressToggleUpvote}
+              onPressToggleLike={onPressToggleLike}
               onCopyPostText={onCopyPostText}
               onOpenTranslate={onOpenTranslate}
               onDeletePost={onDeletePost}
diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx
index 4154cbe75..d07afca34 100644
--- a/src/view/com/posts/Feed.tsx
+++ b/src/view/com/posts/Feed.tsx
@@ -128,6 +128,7 @@ export const Feed = observer(function Feed({
     <View testID={testID} style={style}>
       {data.length > 0 && (
         <FlatList
+          testID={testID ? `${testID}-flatlist` : undefined}
           ref={scrollElRef}
           data={data}
           keyExtractor={item => item._reactKey}
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index 573b92fd3..734034a89 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -13,7 +13,7 @@ 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 {PostEmbeds} from '../util/post-embeds'
 import {PostMutedWrapper} from '../util/PostMuted'
 import {RichText} from '../util/text/RichText'
 import * as Toast from '../util/Toast'
@@ -79,11 +79,11 @@ export const FeedItem = observer(function ({
       .toggleRepost()
       .catch(e => store.log.error('Failed to toggle repost', e))
   }
-  const onPressToggleUpvote = () => {
+  const onPressToggleLike = () => {
     track('FeedItem:PostLike')
     return item
-      .toggleUpvote()
-      .catch(e => store.log.error('Failed to toggle upvote', e))
+      .toggleLike()
+      .catch(e => store.log.error('Failed to toggle like', e))
   }
   const onCopyPostText = () => {
     Clipboard.setString(record?.text || '')
@@ -127,7 +127,12 @@ export const FeedItem = observer(function ({
 
   return (
     <PostMutedWrapper isMuted={isMuted}>
-      <Link style={outerStyles} href={itemHref} title={itemTitle} noFeedback>
+      <Link
+        testID={`feedItem-by-${item.post.author.handle}`}
+        style={outerStyles}
+        href={itemHref}
+        title={itemTitle}
+        noFeedback>
         {isThreadChild && (
           <View
             style={[styles.topReplyLine, {borderColor: pal.colors.replyLine}]}
@@ -189,7 +194,6 @@ export const FeedItem = observer(function ({
               timestamp={item.post.indexedAt}
               postHref={itemHref}
               did={item.post.author.did}
-              declarationCid={item.post.author.declaration.cid}
               showFollowBtn={showFollowBtn}
             />
             {!isThreadChild && replyAuthorDid !== '' && (
@@ -239,12 +243,12 @@ export const FeedItem = observer(function ({
               isAuthor={item.post.author.did === store.me.did}
               replyCount={item.post.replyCount}
               repostCount={item.post.repostCount}
-              upvoteCount={item.post.upvoteCount}
-              isReposted={!!item.post.viewer.repost}
-              isUpvoted={!!item.post.viewer.upvote}
+              likeCount={item.post.likeCount}
+              isReposted={!!item.post.viewer?.repost}
+              isLiked={!!item.post.viewer?.like}
               onPressReply={onPressReply}
               onPressToggleRepost={onPressToggleRepost}
-              onPressToggleUpvote={onPressToggleUpvote}
+              onPressToggleLike={onPressToggleLike}
               onCopyPostText={onCopyPostText}
               onOpenTranslate={onOpenTranslate}
               onDeletePost={onDeletePost}
diff --git a/src/view/com/profile/FollowButton.tsx b/src/view/com/profile/FollowButton.tsx
index 5204f5a40..f22eb9b4a 100644
--- a/src/view/com/profile/FollowButton.tsx
+++ b/src/view/com/profile/FollowButton.tsx
@@ -2,19 +2,16 @@ import React from 'react'
 import {observer} from 'mobx-react-lite'
 import {Button, ButtonType} from '../util/forms/Button'
 import {useStores} from 'state/index'
-import * as apilib from 'lib/api/index'
 import * as Toast from '../util/Toast'
 
 const FollowButton = observer(
   ({
     type = 'inverted',
     did,
-    declarationCid,
     onToggleFollow,
   }: {
     type?: ButtonType
     did: string
-    declarationCid: string
     onToggleFollow?: (v: boolean) => void
   }) => {
     const store = useStores()
@@ -23,7 +20,7 @@ const FollowButton = observer(
     const onToggleFollowInner = async () => {
       if (store.me.follows.isFollowing(did)) {
         try {
-          await apilib.unfollow(store, store.me.follows.getFollowUri(did))
+          await store.agent.deleteFollow(store.me.follows.getFollowUri(did))
           store.me.follows.removeFollow(did)
           onToggleFollow?.(false)
         } catch (e: any) {
@@ -32,7 +29,7 @@ const FollowButton = observer(
         }
       } else {
         try {
-          const res = await apilib.follow(store, did, declarationCid)
+          const res = await store.agent.follow(did)
           store.me.follows.addFollow(did, res.uri)
           onToggleFollow?.(true)
         } catch (e: any) {
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index 748648742..0beac8a7f 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, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
-import {AppBskyActorProfile} from '@atproto/api'
+import {AppBskyActorDefs} from '@atproto/api'
 import {Link} from '../util/Link'
 import {Text} from '../util/text/Text'
 import {UserAvatar} from '../util/UserAvatar'
@@ -11,6 +11,7 @@ import {useStores} from 'state/index'
 import FollowButton from './FollowButton'
 
 export function ProfileCard({
+  testID,
   handle,
   displayName,
   avatar,
@@ -21,6 +22,7 @@ export function ProfileCard({
   followers,
   renderButton,
 }: {
+  testID?: string
   handle: string
   displayName?: string
   avatar?: string
@@ -28,12 +30,13 @@ export function ProfileCard({
   isFollowedBy?: boolean
   noBg?: boolean
   noBorder?: boolean
-  followers?: AppBskyActorProfile.View[] | undefined
+  followers?: AppBskyActorDefs.ProfileView[] | undefined
   renderButton?: () => JSX.Element
 }) {
   const pal = usePalette('default')
   return (
     <Link
+      testID={testID}
       style={[
         styles.outer,
         pal.border,
@@ -106,7 +109,6 @@ export function ProfileCard({
 export const ProfileCardWithFollowBtn = observer(
   ({
     did,
-    declarationCid,
     handle,
     displayName,
     avatar,
@@ -117,7 +119,6 @@ export const ProfileCardWithFollowBtn = observer(
     followers,
   }: {
     did: string
-    declarationCid: string
     handle: string
     displayName?: string
     avatar?: string
@@ -125,7 +126,7 @@ export const ProfileCardWithFollowBtn = observer(
     isFollowedBy?: boolean
     noBg?: boolean
     noBorder?: boolean
-    followers?: AppBskyActorProfile.View[] | undefined
+    followers?: AppBskyActorDefs.ProfileView[] | undefined
   }) => {
     const store = useStores()
     const isMe = store.me.handle === handle
@@ -140,11 +141,7 @@ export const ProfileCardWithFollowBtn = observer(
         noBg={noBg}
         noBorder={noBorder}
         followers={followers}
-        renderButton={
-          isMe
-            ? undefined
-            : () => <FollowButton did={did} declarationCid={declarationCid} />
-        }
+        renderButton={isMe ? undefined : () => <FollowButton did={did} />}
       />
     )
   },
diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx
index d1488403a..8d489ad0a 100644
--- a/src/view/com/profile/ProfileFollowers.tsx
+++ b/src/view/com/profile/ProfileFollowers.tsx
@@ -19,7 +19,7 @@ export const ProfileFollowers = observer(function ProfileFollowers({
   const pal = usePalette('default')
   const store = useStores()
   const view = React.useMemo(
-    () => new UserFollowersViewModel(store, {user: name}),
+    () => new UserFollowersViewModel(store, {actor: name}),
     [store, name],
   )
 
@@ -64,7 +64,6 @@ export const ProfileFollowers = observer(function ProfileFollowers({
     <ProfileCardWithFollowBtn
       key={item.did}
       did={item.did}
-      declarationCid={item.declaration.cid}
       handle={item.handle}
       displayName={item.displayName}
       avatar={item.avatar}
diff --git a/src/view/com/profile/ProfileFollows.tsx b/src/view/com/profile/ProfileFollows.tsx
index ddb64787a..849b33441 100644
--- a/src/view/com/profile/ProfileFollows.tsx
+++ b/src/view/com/profile/ProfileFollows.tsx
@@ -16,7 +16,7 @@ export const ProfileFollows = observer(function ProfileFollows({
   const pal = usePalette('default')
   const store = useStores()
   const view = React.useMemo(
-    () => new UserFollowsViewModel(store, {user: name}),
+    () => new UserFollowsViewModel(store, {actor: name}),
     [store, name],
   )
 
@@ -61,7 +61,6 @@ export const ProfileFollows = observer(function ProfileFollows({
     <ProfileCardWithFollowBtn
       key={item.did}
       did={item.did}
-      declarationCid={item.declaration.cid}
       handle={item.handle}
       displayName={item.displayName}
       avatar={item.avatar}
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index 06dd20989..6294c627b 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -33,7 +33,61 @@ import {isDesktopWeb} from 'platform/detection'
 
 const BACK_HITSLOP = {left: 30, top: 30, right: 30, bottom: 30}
 
-export const ProfileHeader = observer(function ProfileHeader({
+export const ProfileHeader = observer(
+  ({
+    view,
+    onRefreshAll,
+  }: {
+    view: ProfileViewModel
+    onRefreshAll: () => void
+  }) => {
+    const pal = usePalette('default')
+
+    // loading
+    // =
+    if (!view || !view.hasLoaded) {
+      return (
+        <View style={pal.view}>
+          <LoadingPlaceholder width="100%" height={120} />
+          <View
+            style={[
+              pal.view,
+              {borderColor: pal.colors.background},
+              styles.avi,
+            ]}>
+            <LoadingPlaceholder width={80} height={80} style={styles.br40} />
+          </View>
+          <View style={styles.content}>
+            <View style={[styles.buttonsLine]}>
+              <LoadingPlaceholder width={100} height={31} style={styles.br50} />
+            </View>
+            <View style={styles.displayNameLine}>
+              <Text type="title-2xl" style={[pal.text, styles.title]}>
+                {view.displayName || view.handle}
+              </Text>
+            </View>
+          </View>
+        </View>
+      )
+    }
+
+    // error
+    // =
+    if (view.hasError) {
+      return (
+        <View testID="profileHeaderHasError">
+          <Text>{view.error}</Text>
+        </View>
+      )
+    }
+
+    // loaded
+    // =
+    return <ProfileHeaderLoaded view={view} onRefreshAll={onRefreshAll} />
+  },
+)
+
+const ProfileHeaderLoaded = observer(function ProfileHeaderLoaded({
   view,
   onRefreshAll,
 }: {
@@ -44,14 +98,17 @@ export const ProfileHeader = observer(function ProfileHeader({
   const store = useStores()
   const navigation = useNavigation<NavigationProp>()
   const {track} = useAnalytics()
+
   const onPressBack = React.useCallback(() => {
     navigation.goBack()
   }, [navigation])
+
   const onPressAvi = React.useCallback(() => {
     if (view.avatar) {
       store.shell.openLightbox(new ProfileImageLightbox(view))
     }
   }, [store, view])
+
   const onPressToggleFollow = React.useCallback(() => {
     view?.toggleFollowing().then(
       () => {
@@ -64,6 +121,7 @@ export const ProfileHeader = observer(function ProfileHeader({
       err => store.log.error('Failed to toggle follow', err),
     )
   }, [view, store])
+
   const onPressEditProfile = React.useCallback(() => {
     track('ProfileHeader:EditProfileButtonClicked')
     store.shell.openModal({
@@ -72,18 +130,22 @@ export const ProfileHeader = observer(function ProfileHeader({
       onUpdate: onRefreshAll,
     })
   }, [track, store, view, onRefreshAll])
+
   const onPressFollowers = React.useCallback(() => {
     track('ProfileHeader:FollowersButtonClicked')
     navigation.push('ProfileFollowers', {name: view.handle})
   }, [track, navigation, view])
+
   const onPressFollows = React.useCallback(() => {
     track('ProfileHeader:FollowsButtonClicked')
     navigation.push('ProfileFollows', {name: view.handle})
   }, [track, navigation, view])
+
   const onPressShare = React.useCallback(() => {
     track('ProfileHeader:ShareButtonClicked')
     Share.share({url: toShareUrl(`/profile/${view.handle}`)})
   }, [track, view])
+
   const onPressMuteAccount = React.useCallback(async () => {
     track('ProfileHeader:MuteAccountButtonClicked')
     try {
@@ -94,6 +156,7 @@ export const ProfileHeader = observer(function ProfileHeader({
       Toast.show(`There was an issue! ${e.toString()}`)
     }
   }, [track, view, store])
+
   const onPressUnmuteAccount = React.useCallback(async () => {
     track('ProfileHeader:UnmuteAccountButtonClicked')
     try {
@@ -104,6 +167,7 @@ export const ProfileHeader = observer(function ProfileHeader({
       Toast.show(`There was an issue! ${e.toString()}`)
     }
   }, [track, view, store])
+
   const onPressReportAccount = React.useCallback(() => {
     track('ProfileHeader:ReportAccountButtonClicked')
     store.shell.openModal({
@@ -112,54 +176,39 @@ export const ProfileHeader = observer(function ProfileHeader({
     })
   }, [track, store, view])
 
-  // loading
-  // =
-  if (!view || !view.hasLoaded) {
-    return (
-      <View style={pal.view}>
-        <LoadingPlaceholder width="100%" height={120} />
-        <View
-          style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}>
-          <LoadingPlaceholder width={80} height={80} style={styles.br40} />
-        </View>
-        <View style={styles.content}>
-          <View style={[styles.buttonsLine]}>
-            <LoadingPlaceholder width={100} height={31} style={styles.br50} />
-          </View>
-          <View style={styles.displayNameLine}>
-            <Text type="title-2xl" style={[pal.text, styles.title]}>
-              {view.displayName || view.handle}
-            </Text>
-          </View>
-        </View>
-      </View>
-    )
-  }
-
-  // error
-  // =
-  if (view.hasError) {
-    return (
-      <View testID="profileHeaderHasError">
-        <Text>{view.error}</Text>
-      </View>
-    )
-  }
-
-  // loaded
-  // =
-  const isMe = store.me.did === view.did
-  let dropdownItems: DropdownItem[] = [{label: 'Share', onPress: onPressShare}]
-  if (!isMe) {
-    dropdownItems.push({
-      label: view.viewer.muted ? 'Unmute Account' : 'Mute Account',
-      onPress: view.viewer.muted ? onPressUnmuteAccount : onPressMuteAccount,
-    })
-    dropdownItems.push({
-      label: 'Report Account',
-      onPress: onPressReportAccount,
-    })
-  }
+  const isMe = React.useMemo(
+    () => store.me.did === view.did,
+    [store.me.did, view.did],
+  )
+  const dropdownItems: DropdownItem[] = React.useMemo(() => {
+    let items: DropdownItem[] = [
+      {
+        testID: 'profileHeaderDropdownSahreBtn',
+        label: 'Share',
+        onPress: onPressShare,
+      },
+    ]
+    if (!isMe) {
+      items.push({
+        testID: 'profileHeaderDropdownMuteBtn',
+        label: view.viewer.muted ? 'Unmute Account' : 'Mute Account',
+        onPress: view.viewer.muted ? onPressUnmuteAccount : onPressMuteAccount,
+      })
+      items.push({
+        testID: 'profileHeaderDropdownReportBtn',
+        label: 'Report Account',
+        onPress: onPressReportAccount,
+      })
+    }
+    return items
+  }, [
+    isMe,
+    view.viewer.muted,
+    onPressShare,
+    onPressUnmuteAccount,
+    onPressMuteAccount,
+    onPressReportAccount,
+  ])
   return (
     <View style={pal.view}>
       <UserBanner banner={view.banner} />
@@ -178,6 +227,7 @@ export const ProfileHeader = observer(function ProfileHeader({
             <>
               {store.me.follows.isFollowing(view.did) ? (
                 <TouchableOpacity
+                  testID="unfollowBtn"
                   onPress={onPressToggleFollow}
                   style={[styles.btn, styles.mainBtn, pal.btn]}>
                   <FontAwesomeIcon
@@ -191,7 +241,7 @@ export const ProfileHeader = observer(function ProfileHeader({
                 </TouchableOpacity>
               ) : (
                 <TouchableOpacity
-                  testID="profileHeaderToggleFollowButton"
+                  testID="followBtn"
                   onPress={onPressToggleFollow}
                   style={[styles.btn, styles.primaryBtn]}>
                   <FontAwesomeIcon
@@ -207,6 +257,7 @@ export const ProfileHeader = observer(function ProfileHeader({
           )}
           {dropdownItems?.length ? (
             <DropdownButton
+              testID="profileHeaderDropdownBtn"
               type="bare"
               items={dropdownItems}
               style={[styles.btn, styles.secondaryBtn, pal.btn]}>
@@ -215,7 +266,10 @@ export const ProfileHeader = observer(function ProfileHeader({
           ) : undefined}
         </View>
         <View style={styles.displayNameLine}>
-          <Text type="title-2xl" style={[pal.text, styles.title]}>
+          <Text
+            testID="profileHeaderDisplayName"
+            type="title-2xl"
+            style={[pal.text, styles.title]}>
             {view.displayName || view.handle}
           </Text>
         </View>
@@ -241,19 +295,17 @@ export const ProfileHeader = observer(function ProfileHeader({
               {pluralize(view.followersCount, 'follower')}
             </Text>
           </TouchableOpacity>
-          {view.isUser ? (
-            <TouchableOpacity
-              testID="profileHeaderFollowsButton"
-              style={[s.flexRow, s.mr10]}
-              onPress={onPressFollows}>
-              <Text type="md" style={[s.bold, s.mr2, pal.text]}>
-                {view.followsCount}
-              </Text>
-              <Text type="md" style={[pal.textLight]}>
-                following
-              </Text>
-            </TouchableOpacity>
-          ) : undefined}
+          <TouchableOpacity
+            testID="profileHeaderFollowsButton"
+            style={[s.flexRow, s.mr10]}
+            onPress={onPressFollows}>
+            <Text type="md" style={[s.bold, s.mr2, pal.text]}>
+              {view.followsCount}
+            </Text>
+            <Text type="md" style={[pal.textLight]}>
+              following
+            </Text>
+          </TouchableOpacity>
           <View style={[s.flexRow, s.mr10]}>
             <Text type="md" style={[s.bold, s.mr2, pal.text]}>
               {view.postsCount}
@@ -265,13 +317,16 @@ export const ProfileHeader = observer(function ProfileHeader({
         </View>
         {view.descriptionRichText ? (
           <RichText
+            testID="profileHeaderDescription"
             style={[styles.description, pal.text]}
             numberOfLines={15}
             richText={view.descriptionRichText}
           />
         ) : undefined}
         {view.viewer.muted ? (
-          <View style={[styles.detailLine, pal.btn, s.p5]}>
+          <View
+            testID="profileHeaderMutedNotice"
+            style={[styles.detailLine, pal.btn, s.p5]}>
             <FontAwesomeIcon
               icon={['far', 'eye-slash']}
               style={[pal.text, s.mr5]}
diff --git a/src/view/com/search/SearchResults.tsx b/src/view/com/search/SearchResults.tsx
index 4bf46515c..b53965f44 100644
--- a/src/view/com/search/SearchResults.tsx
+++ b/src/view/com/search/SearchResults.tsx
@@ -97,7 +97,6 @@ const Profiles = observer(({model}: {model: SearchUIModel}) => {
         <ProfileCardWithFollowBtn
           key={item.did}
           did={item.did}
-          declarationCid={item.declaration.cid}
           handle={item.handle}
           displayName={item.displayName}
           avatar={item.avatar}
diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx
index f356f0b09..703869be1 100644
--- a/src/view/com/util/Link.tsx
+++ b/src/view/com/util/Link.tsx
@@ -29,6 +29,7 @@ type Event =
   | GestureResponderEvent
 
 export const Link = observer(function Link({
+  testID,
   style,
   href,
   title,
@@ -36,6 +37,7 @@ export const Link = observer(function Link({
   noFeedback,
   asAnchor,
 }: {
+  testID?: string
   style?: StyleProp<ViewStyle>
   href?: string
   title?: string
@@ -58,6 +60,7 @@ export const Link = observer(function Link({
   if (noFeedback) {
     return (
       <TouchableWithoutFeedback
+        testID={testID}
         onPress={onPress}
         // @ts-ignore web only -prf
         href={asAnchor ? href : undefined}>
@@ -69,6 +72,7 @@ export const Link = observer(function Link({
   }
   return (
     <TouchableOpacity
+      testID={testID}
       style={style}
       onPress={onPress}
       // @ts-ignore web only -prf
@@ -79,6 +83,7 @@ export const Link = observer(function Link({
 })
 
 export const TextLink = observer(function TextLink({
+  testID,
   type = 'md',
   style,
   href,
@@ -86,6 +91,7 @@ export const TextLink = observer(function TextLink({
   numberOfLines,
   lineHeight,
 }: {
+  testID?: string
   type?: TypographyVariant
   style?: StyleProp<TextStyle>
   href: string
@@ -106,6 +112,7 @@ export const TextLink = observer(function TextLink({
 
   return (
     <Text
+      testID={testID}
       type={type}
       style={style}
       numberOfLines={numberOfLines}
@@ -120,6 +127,7 @@ export const TextLink = observer(function TextLink({
  * Only acts as a link on desktop web
  */
 export const DesktopWebTextLink = observer(function DesktopWebTextLink({
+  testID,
   type = 'md',
   style,
   href,
@@ -127,6 +135,7 @@ export const DesktopWebTextLink = observer(function DesktopWebTextLink({
   numberOfLines,
   lineHeight,
 }: {
+  testID?: string
   type?: TypographyVariant
   style?: StyleProp<TextStyle>
   href: string
@@ -137,6 +146,7 @@ export const DesktopWebTextLink = observer(function DesktopWebTextLink({
   if (isDesktopWeb) {
     return (
       <TextLink
+        testID={testID}
         type={type}
         style={style}
         href={href}
@@ -148,6 +158,7 @@ export const DesktopWebTextLink = observer(function DesktopWebTextLink({
   }
   return (
     <Text
+      testID={testID}
       type={type}
       style={style}
       numberOfLines={numberOfLines}
diff --git a/src/view/com/util/PostCtrls.tsx b/src/view/com/util/PostCtrls.tsx
index 00e35eef7..6904928f4 100644
--- a/src/view/com/util/PostCtrls.tsx
+++ b/src/view/com/util/PostCtrls.tsx
@@ -45,12 +45,12 @@ interface PostCtrlsOpts {
   style?: StyleProp<ViewStyle>
   replyCount?: number
   repostCount?: number
-  upvoteCount?: number
+  likeCount?: number
   isReposted: boolean
-  isUpvoted: boolean
+  isLiked: boolean
   onPressReply: () => void
   onPressToggleRepost: () => Promise<void>
-  onPressToggleUpvote: () => Promise<void>
+  onPressToggleLike: () => Promise<void>
   onCopyPostText: () => void
   onOpenTranslate: () => void
   onDeletePost: () => void
@@ -157,26 +157,26 @@ export function PostCtrls(opts: PostCtrlsOpts) {
     })
   }
 
-  const onPressToggleUpvoteWrapper = () => {
-    if (!opts.isUpvoted) {
+  const onPressToggleLikeWrapper = () => {
+    if (!opts.isLiked) {
       ReactNativeHapticFeedback.trigger('impactMedium')
       setLikeMod(1)
       opts
-        .onPressToggleUpvote()
+        .onPressToggleLike()
         .catch(_e => undefined)
         .then(() => setLikeMod(0))
       // DISABLED see #135
       // likeRef.current?.trigger(
       //   {start: ctrlAnimStart, style: ctrlAnimStyle},
       //   async () => {
-      //     await opts.onPressToggleUpvote().catch(_e => undefined)
+      //     await opts.onPressToggleLike().catch(_e => undefined)
       //     setLikeMod(0)
       //   },
       // )
     } else {
       setLikeMod(-1)
       opts
-        .onPressToggleUpvote()
+        .onPressToggleLike()
         .catch(_e => undefined)
         .then(() => setLikeMod(0))
     }
@@ -186,6 +186,7 @@ export function PostCtrls(opts: PostCtrlsOpts) {
     <View style={[styles.ctrls, opts.style]}>
       <View style={s.flex1}>
         <TouchableOpacity
+          testID="replyBtn"
           style={styles.ctrl}
           hitSlop={HITSLOP}
           onPress={opts.onPressReply}>
@@ -203,6 +204,7 @@ export function PostCtrls(opts: PostCtrlsOpts) {
       </View>
       <View style={s.flex1}>
         <TouchableOpacity
+          testID="repostBtn"
           hitSlop={HITSLOP}
           onPress={onPressToggleRepostWrapper}
           style={styles.ctrl}>
@@ -230,6 +232,7 @@ export function PostCtrls(opts: PostCtrlsOpts) {
           }
           {typeof opts.repostCount !== 'undefined' ? (
             <Text
+              testID="repostCount"
               style={
                 opts.isReposted || repostMod > 0
                   ? [s.bold, s.green3, s.f15, s.ml5]
@@ -242,12 +245,13 @@ export function PostCtrls(opts: PostCtrlsOpts) {
       </View>
       <View style={s.flex1}>
         <TouchableOpacity
+          testID="likeBtn"
           style={styles.ctrl}
           hitSlop={HITSLOP}
-          onPress={onPressToggleUpvoteWrapper}>
-          {opts.isUpvoted || likeMod > 0 ? (
+          onPress={onPressToggleLikeWrapper}>
+          {opts.isLiked || likeMod > 0 ? (
             <HeartIconSolid
-              style={styles.ctrlIconUpvoted as StyleProp<ViewStyle>}
+              style={styles.ctrlIconLiked as StyleProp<ViewStyle>}
               size={opts.big ? 22 : 16}
             />
           ) : (
@@ -259,9 +263,9 @@ export function PostCtrls(opts: PostCtrlsOpts) {
           )}
           {
             undefined /*DISABLED see #135 <TriggerableAnimated ref={likeRef}>
-            {opts.isUpvoted || likeMod > 0 ? (
+            {opts.isLiked || likeMod > 0 ? (
               <HeartIconSolid
-                style={styles.ctrlIconUpvoted as ViewStyle}
+                style={styles.ctrlIconLiked as ViewStyle}
                 size={opts.big ? 22 : 16}
               />
             ) : (
@@ -276,14 +280,15 @@ export function PostCtrls(opts: PostCtrlsOpts) {
             )}
             </TriggerableAnimated>*/
           }
-          {typeof opts.upvoteCount !== 'undefined' ? (
+          {typeof opts.likeCount !== 'undefined' ? (
             <Text
+              testID="likeCount"
               style={
-                opts.isUpvoted || likeMod > 0
+                opts.isLiked || likeMod > 0
                   ? [s.bold, s.red3, s.f15, s.ml5]
                   : [defaultCtrlColor, s.f15, s.ml5]
               }>
-              {opts.upvoteCount + likeMod}
+              {opts.likeCount + likeMod}
             </Text>
           ) : undefined}
         </TouchableOpacity>
@@ -291,6 +296,7 @@ export function PostCtrls(opts: PostCtrlsOpts) {
       <View style={s.flex1}>
         {opts.big ? undefined : (
           <PostDropdownBtn
+            testID="postDropdownBtn"
             style={styles.ctrl}
             itemUri={opts.itemUri}
             itemCid={opts.itemCid}
@@ -330,7 +336,7 @@ const styles = StyleSheet.create({
   ctrlIconReposted: {
     color: colors.green3,
   },
-  ctrlIconUpvoted: {
+  ctrlIconLiked: {
     color: colors.red3,
   },
   mt1: {
diff --git a/src/view/com/util/PostEmbeds/YoutubeEmbed.tsx b/src/view/com/util/PostEmbeds/YoutubeEmbed.tsx
deleted file mode 100644
index d9425fe4e..000000000
--- a/src/view/com/util/PostEmbeds/YoutubeEmbed.tsx
+++ /dev/null
@@ -1,119 +0,0 @@
-import React, {useEffect} from 'react'
-import {useState} from 'react'
-import {
-  View,
-  StyleSheet,
-  Pressable,
-  TouchableWithoutFeedback,
-  EmitterSubscription,
-} from 'react-native'
-import YoutubePlayer from 'react-native-youtube-iframe'
-import {usePalette} from 'lib/hooks/usePalette'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import ExternalLinkEmbed from './ExternalLinkEmbed'
-import {PresentedExternal} from '@atproto/api/dist/client/types/app/bsky/embed/external'
-import {useStores} from 'state/index'
-
-const YoutubeEmbed = ({
-  link,
-  videoId,
-}: {
-  videoId: string
-  link: PresentedExternal
-}) => {
-  const store = useStores()
-  const [displayVideoPlayer, setDisplayVideoPlayer] = useState(false)
-  const [playerDimensions, setPlayerDimensions] = useState({
-    width: 0,
-    height: 0,
-  })
-  const pal = usePalette('default')
-  const handlePlayButtonPressed = () => {
-    setDisplayVideoPlayer(true)
-  }
-  const handleOnLayout = (event: {
-    nativeEvent: {layout: {width: any; height: any}}
-  }) => {
-    setPlayerDimensions({
-      width: event.nativeEvent.layout.width,
-      height: event.nativeEvent.layout.height,
-    })
-  }
-  useEffect(() => {
-    let sub: EmitterSubscription
-    if (displayVideoPlayer) {
-      sub = store.onNavigation(() => {
-        setDisplayVideoPlayer(false)
-      })
-    }
-    return () => sub && sub.remove()
-  }, [displayVideoPlayer, store])
-
-  const imageChild = (
-    <Pressable onPress={handlePlayButtonPressed} style={styles.playButton}>
-      <FontAwesomeIcon icon="play" size={24} color="white" />
-    </Pressable>
-  )
-
-  if (!displayVideoPlayer) {
-    return (
-      <View
-        style={[styles.extOuter, pal.view, pal.border]}
-        onLayout={handleOnLayout}>
-        <ExternalLinkEmbed
-          link={link}
-          onImagePress={handlePlayButtonPressed}
-          imageChild={imageChild}
-        />
-      </View>
-    )
-  }
-
-  const height = (playerDimensions.width / 16) * 9
-  const noop = () => {}
-
-  return (
-    <TouchableWithoutFeedback onPress={noop}>
-      <View>
-        {/* Removing the outter View will make tap events propagate to parents */}
-        <YoutubePlayer
-          initialPlayerParams={{
-            modestbranding: true,
-          }}
-          webViewProps={{
-            startInLoadingState: true,
-          }}
-          height={height}
-          videoId={videoId}
-          webViewStyle={styles.webView}
-        />
-      </View>
-    </TouchableWithoutFeedback>
-  )
-}
-
-const styles = StyleSheet.create({
-  extOuter: {
-    borderWidth: 1,
-    borderRadius: 8,
-    marginTop: 4,
-  },
-  playButton: {
-    position: 'absolute',
-    alignSelf: 'center',
-    alignItems: 'center',
-    top: '44%',
-    justifyContent: 'center',
-    backgroundColor: 'black',
-    padding: 10,
-    borderRadius: 50,
-    opacity: 0.8,
-  },
-  webView: {
-    alignItems: 'center',
-    alignContent: 'center',
-    justifyContent: 'center',
-  },
-})
-
-export default YoutubeEmbed
diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx
index c53de5c1f..a675283b8 100644
--- a/src/view/com/util/PostMeta.tsx
+++ b/src/view/com/util/PostMeta.tsx
@@ -16,7 +16,6 @@ interface PostMetaOpts {
   postHref: string
   timestamp: string
   did?: string
-  declarationCid?: string
   showFollowBtn?: boolean
 }
 
@@ -34,13 +33,7 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
     setDidFollow(true)
   }, [setDidFollow])
 
-  if (
-    opts.showFollowBtn &&
-    !isMe &&
-    (!isFollowing || didFollow) &&
-    opts.did &&
-    opts.declarationCid
-  ) {
+  if (opts.showFollowBtn && !isMe && (!isFollowing || didFollow) && opts.did) {
     // two-liner with follow button
     return (
       <View style={styles.metaTwoLine}>
@@ -79,7 +72,6 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
           <FollowButton
             type="default"
             did={opts.did}
-            declarationCid={opts.declarationCid}
             onToggleFollow={onToggleFollow}
           />
         </View>
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index 2e0632521..ff741cd34 100644
--- a/src/view/com/util/UserAvatar.tsx
+++ b/src/view/com/util/UserAvatar.tsx
@@ -23,6 +23,7 @@ import {isWeb} from 'platform/detection'
 function DefaultAvatar({size}: {size: number}) {
   return (
     <Svg
+      testID="userAvatarFallback"
       width={size}
       height={size}
       viewBox="0 0 24 24"
@@ -56,6 +57,7 @@ export function UserAvatar({
 
   const dropdownItems = [
     !isWeb && {
+      testID: 'changeAvatarCameraBtn',
       label: 'Camera',
       icon: 'camera' as IconProp,
       onPress: async () => {
@@ -73,6 +75,7 @@ export function UserAvatar({
       },
     },
     {
+      testID: 'changeAvatarLibraryBtn',
       label: 'Library',
       icon: 'image' as IconProp,
       onPress: async () => {
@@ -94,6 +97,7 @@ export function UserAvatar({
       },
     },
     {
+      testID: 'changeAvatarRemoveBtn',
       label: 'Remove',
       icon: ['far', 'trash-can'] as IconProp,
       onPress: async () => {
@@ -104,6 +108,7 @@ export function UserAvatar({
   // onSelectNewAvatar is only passed as prop on the EditProfile component
   return onSelectNewAvatar ? (
     <DropdownButton
+      testID="changeAvatarBtn"
       type="bare"
       items={dropdownItems}
       openToRight
@@ -112,6 +117,7 @@ export function UserAvatar({
       menuWidth={170}>
       {avatar ? (
         <HighPriorityImage
+          testID="userAvatarImage"
           style={{
             width: size,
             height: size,
@@ -132,6 +138,7 @@ export function UserAvatar({
     </DropdownButton>
   ) : avatar ? (
     <HighPriorityImage
+      testID="userAvatarImage"
       style={{width: size, height: size, borderRadius: Math.floor(size / 2)}}
       resizeMode="stretch"
       source={{uri: avatar}}
diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx
index 8317f93ac..56d7e370a 100644
--- a/src/view/com/util/UserBanner.tsx
+++ b/src/view/com/util/UserBanner.tsx
@@ -33,6 +33,7 @@ export function UserBanner({
 
   const dropdownItems = [
     !isWeb && {
+      testID: 'changeBannerCameraBtn',
       label: 'Camera',
       icon: 'camera' as IconProp,
       onPress: async () => {
@@ -51,6 +52,7 @@ export function UserBanner({
       },
     },
     {
+      testID: 'changeBannerLibraryBtn',
       label: 'Library',
       icon: 'image' as IconProp,
       onPress: async () => {
@@ -73,6 +75,7 @@ export function UserBanner({
       },
     },
     {
+      testID: 'changeBannerRemoveBtn',
       label: 'Remove',
       icon: ['far', 'trash-can'] as IconProp,
       onPress: () => {
@@ -84,6 +87,7 @@ export function UserBanner({
   // setUserBanner is only passed as prop on the EditProfile component
   return onSelectNewBanner ? (
     <DropdownButton
+      testID="changeBannerBtn"
       type="bare"
       items={dropdownItems}
       openToRight
@@ -91,9 +95,16 @@ export function UserBanner({
       bottomOffset={-10}
       menuWidth={170}>
       {banner ? (
-        <Image style={styles.bannerImage} source={{uri: banner}} />
+        <Image
+          testID="userBannerImage"
+          style={styles.bannerImage}
+          source={{uri: banner}}
+        />
       ) : (
-        <View style={[styles.bannerImage, styles.defaultBanner]} />
+        <View
+          testID="userBannerFallback"
+          style={[styles.bannerImage, styles.defaultBanner]}
+        />
       )}
       <View style={[styles.editButtonContainer, pal.btn]}>
         <FontAwesomeIcon
@@ -106,12 +117,16 @@ export function UserBanner({
     </DropdownButton>
   ) : banner ? (
     <Image
+      testID="userBannerImage"
       style={styles.bannerImage}
       resizeMode="cover"
       source={{uri: banner}}
     />
   ) : (
-    <View style={[styles.bannerImage, styles.defaultBanner]} />
+    <View
+      testID="userBannerFallback"
+      style={[styles.bannerImage, styles.defaultBanner]}
+    />
   )
 }
 
diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx
index a99282512..ad0a5a1d2 100644
--- a/src/view/com/util/ViewHeader.tsx
+++ b/src/view/com/util/ViewHeader.tsx
@@ -51,7 +51,7 @@ export const ViewHeader = observer(function ({
     return (
       <Container hideOnScroll={hideOnScroll || false}>
         <TouchableOpacity
-          testID="viewHeaderBackOrMenuBtn"
+          testID="viewHeaderDrawerBtn"
           onPress={canGoBack ? onPressBack : onPressMenu}
           hitSlop={BACK_HITSLOP}
           style={canGoBack ? styles.backBtn : styles.backBtnWide}>
diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx
index e1280fd82..82351cf08 100644
--- a/src/view/com/util/ViewSelector.tsx
+++ b/src/view/com/util/ViewSelector.tsx
@@ -47,13 +47,18 @@ export function ViewSelector({
   // events
   // =
 
-  const onSwipeEnd = (dx: number) => {
-    if (dx !== 0) {
-      setSelectedIndex(clamp(selectedIndex + dx, 0, sections.length))
-    }
-  }
-  const onPressSelection = (index: number) =>
-    setSelectedIndex(clamp(index, 0, sections.length))
+  const onSwipeEnd = React.useCallback(
+    (dx: number) => {
+      if (dx !== 0) {
+        setSelectedIndex(clamp(selectedIndex + dx, 0, sections.length))
+      }
+    },
+    [setSelectedIndex, selectedIndex, sections],
+  )
+  const onPressSelection = React.useCallback(
+    (index: number) => setSelectedIndex(clamp(index, 0, sections.length)),
+    [setSelectedIndex, sections],
+  )
   useEffect(() => {
     onSelectView?.(selectedIndex)
   }, [selectedIndex, onSelectView])
@@ -61,27 +66,33 @@ export function ViewSelector({
   // rendering
   // =
 
-  const renderItemInternal = ({item}: {item: any}) => {
-    if (item === HEADER_ITEM) {
-      if (renderHeader) {
-        return renderHeader()
+  const renderItemInternal = React.useCallback(
+    ({item}: {item: any}) => {
+      if (item === HEADER_ITEM) {
+        if (renderHeader) {
+          return renderHeader()
+        }
+        return <View />
+      } else if (item === SELECTOR_ITEM) {
+        return (
+          <Selector
+            items={sections}
+            panX={panX}
+            selectedIndex={selectedIndex}
+            onSelect={onPressSelection}
+          />
+        )
+      } else {
+        return renderItem(item)
       }
-      return <View />
-    } else if (item === SELECTOR_ITEM) {
-      return (
-        <Selector
-          items={sections}
-          panX={panX}
-          selectedIndex={selectedIndex}
-          onSelect={onPressSelection}
-        />
-      )
-    } else {
-      return renderItem(item)
-    }
-  }
+    },
+    [sections, panX, selectedIndex, onPressSelection, renderHeader, renderItem],
+  )
 
-  const data = [HEADER_ITEM, SELECTOR_ITEM, ...items]
+  const data = React.useMemo(
+    () => [HEADER_ITEM, SELECTOR_ITEM, ...items],
+    [items],
+  )
   return (
     <HorzSwipe
       hasPriority
diff --git a/src/view/com/util/forms/Button.tsx b/src/view/com/util/forms/Button.tsx
index f3f4d1c79..b7c058d2d 100644
--- a/src/view/com/util/forms/Button.tsx
+++ b/src/view/com/util/forms/Button.tsx
@@ -27,11 +27,13 @@ export function Button({
   style,
   onPress,
   children,
+  testID,
 }: React.PropsWithChildren<{
   type?: ButtonType
   label?: string
   style?: StyleProp<ViewStyle>
   onPress?: () => void
+  testID?: string
 }>) {
   const theme = useTheme()
   const outerStyle = choose<ViewStyle, Record<ButtonType, ViewStyle>>(type, {
@@ -107,7 +109,8 @@ export function Button({
   return (
     <TouchableOpacity
       style={[outerStyle, styles.outer, style]}
-      onPress={onPress}>
+      onPress={onPress}
+      testID={testID}>
       {label ? (
         <Text type="button" style={[labelStyle]}>
           {label}
diff --git a/src/view/com/util/forms/DropdownButton.tsx b/src/view/com/util/forms/DropdownButton.tsx
index d6ae800c6..938c346cd 100644
--- a/src/view/com/util/forms/DropdownButton.tsx
+++ b/src/view/com/util/forms/DropdownButton.tsx
@@ -24,6 +24,7 @@ const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10}
 const ESTIMATED_MENU_ITEM_HEIGHT = 52
 
 export interface DropdownItem {
+  testID?: string
   icon?: IconProp
   label: string
   onPress: () => void
@@ -33,6 +34,7 @@ type MaybeDropdownItem = DropdownItem | false | undefined
 export type DropdownButtonType = ButtonType | 'bare'
 
 export function DropdownButton({
+  testID,
   type = 'bare',
   style,
   items,
@@ -43,6 +45,7 @@ export function DropdownButton({
   rightOffset = 0,
   bottomOffset = 0,
 }: {
+  testID?: string
   type?: DropdownButtonType
   style?: StyleProp<ViewStyle>
   items: MaybeDropdownItem[]
@@ -90,22 +93,18 @@ export function DropdownButton({
   if (type === 'bare') {
     return (
       <TouchableOpacity
+        testID={testID}
         style={style}
         onPress={onPress}
         hitSlop={HITSLOP}
-        // Fix an issue where specific references cause runtime error in jest environment
-        ref={
-          typeof process !== 'undefined' && process.env.JEST_WORKER_ID != null
-            ? null
-            : ref
-        }>
+        ref={ref}>
         {children}
       </TouchableOpacity>
     )
   }
   return (
     <View ref={ref}>
-      <Button onPress={onPress} style={style} label={label}>
+      <Button testID={testID} onPress={onPress} style={style} label={label}>
         {children}
       </Button>
     </View>
@@ -113,6 +112,7 @@ export function DropdownButton({
 }
 
 export function PostDropdownBtn({
+  testID,
   style,
   children,
   itemUri,
@@ -123,6 +123,7 @@ export function PostDropdownBtn({
   onOpenTranslate,
   onDeletePost,
 }: {
+  testID?: string
   style?: StyleProp<ViewStyle>
   children?: React.ReactNode
   itemUri: string
@@ -138,6 +139,7 @@ export function PostDropdownBtn({
 
   const dropdownItems: DropdownItem[] = [
     {
+      testID: 'postDropdownTranslateBtn',
       icon: 'language',
       label: 'Translate...',
       onPress() {
@@ -145,6 +147,7 @@ export function PostDropdownBtn({
       },
     },
     {
+      testID: 'postDropdownCopyTextBtn',
       icon: ['far', 'paste'],
       label: 'Copy post text',
       onPress() {
@@ -152,6 +155,7 @@ export function PostDropdownBtn({
       },
     },
     {
+      testID: 'postDropdownShareBtn',
       icon: 'share',
       label: 'Share...',
       onPress() {
@@ -159,6 +163,7 @@ export function PostDropdownBtn({
       },
     },
     {
+      testID: 'postDropdownReportBtn',
       icon: 'circle-exclamation',
       label: 'Report post',
       onPress() {
@@ -171,6 +176,7 @@ export function PostDropdownBtn({
     },
     isAuthor
       ? {
+          testID: 'postDropdownDeleteBtn',
           icon: ['far', 'trash-can'],
           label: 'Delete post',
           onPress() {
@@ -186,7 +192,11 @@ export function PostDropdownBtn({
   ].filter(Boolean) as DropdownItem[]
 
   return (
-    <DropdownButton style={style} items={dropdownItems} menuWidth={200}>
+    <DropdownButton
+      testID={testID}
+      style={style}
+      items={dropdownItems}
+      menuWidth={200}>
       {children}
     </DropdownButton>
   )
@@ -291,6 +301,7 @@ const DropdownItems = ({
         ]}>
         {items.map((item, index) => (
           <TouchableOpacity
+            testID={item.testID}
             key={index}
             style={[styles.menuItem]}
             onPress={() => onPressItem(index)}>
diff --git a/src/view/com/util/forms/RadioButton.tsx b/src/view/com/util/forms/RadioButton.tsx
index d6b2bb119..f5696a76d 100644
--- a/src/view/com/util/forms/RadioButton.tsx
+++ b/src/view/com/util/forms/RadioButton.tsx
@@ -6,12 +6,14 @@ import {useTheme} from 'lib/ThemeContext'
 import {choose} from 'lib/functions'
 
 export function RadioButton({
+  testID,
   type = 'default-light',
   label,
   isSelected,
   style,
   onPress,
 }: {
+  testID?: string
   type?: ButtonType
   label: string
   isSelected: boolean
@@ -119,7 +121,7 @@ export function RadioButton({
     },
   })
   return (
-    <Button type={type} onPress={onPress} style={style}>
+    <Button testID={testID} type={type} onPress={onPress} style={style}>
       <View style={styles.outer}>
         <View style={[circleStyle, styles.circle]}>
           {isSelected ? (
diff --git a/src/view/com/util/forms/RadioGroup.tsx b/src/view/com/util/forms/RadioGroup.tsx
index 901b0cdd8..071540b73 100644
--- a/src/view/com/util/forms/RadioGroup.tsx
+++ b/src/view/com/util/forms/RadioGroup.tsx
@@ -10,11 +10,13 @@ export interface RadioGroupItem {
 }
 
 export function RadioGroup({
+  testID,
   type,
   items,
   initialSelection = '',
   onSelect,
 }: {
+  testID?: string
   type?: ButtonType
   items: RadioGroupItem[]
   initialSelection?: string
@@ -30,6 +32,7 @@ export function RadioGroup({
       {items.map((item, i) => (
         <RadioButton
           key={item.key}
+          testID={testID ? `${testID}-${item.key}` : undefined}
           style={i !== 0 ? s.mt2 : undefined}
           type={type}
           label={item.label}
diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx
index 24dbe6a52..ddb09ce39 100644
--- a/src/view/com/util/images/AutoSizedImage.tsx
+++ b/src/view/com/util/images/AutoSizedImage.tsx
@@ -4,9 +4,9 @@ import {
   StyleProp,
   StyleSheet,
   TouchableOpacity,
+  View,
   ViewStyle,
 } from 'react-native'
-// import Image from 'view/com/util/images/Image'
 import {clamp} from 'lib/numbers'
 import {useStores} from 'state/index'
 import {Dim} from 'lib/media/manip'
@@ -51,16 +51,24 @@ export function AutoSizedImage({
     })
   }, [dim, setDim, setAspectRatio, store, uri])
 
+  if (onPress || onLongPress || onPressIn) {
+    return (
+      <TouchableOpacity
+        onPress={onPress}
+        onLongPress={onLongPress}
+        onPressIn={onPressIn}
+        delayPressIn={DELAY_PRESS_IN}
+        style={[styles.container, style]}>
+        <Image style={[styles.image, {aspectRatio}]} source={{uri}} />
+        {children}
+      </TouchableOpacity>
+    )
+  }
   return (
-    <TouchableOpacity
-      onPress={onPress}
-      onLongPress={onLongPress}
-      onPressIn={onPressIn}
-      delayPressIn={DELAY_PRESS_IN}
-      style={[styles.container, style]}>
+    <View style={[styles.container, style]}>
       <Image style={[styles.image, {aspectRatio}]} source={{uri}} />
       {children}
-    </TouchableOpacity>
+    </View>
   )
 }
 
diff --git a/src/view/com/util/PostEmbeds/ExternalLinkEmbed.tsx b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx
index e8c63bdb7..a4cbb3e29 100644
--- a/src/view/com/util/PostEmbeds/ExternalLinkEmbed.tsx
+++ b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx
@@ -3,25 +3,20 @@ import {Text} from '../text/Text'
 import {AutoSizedImage} from '../images/AutoSizedImage'
 import {StyleSheet, View} from 'react-native'
 import {usePalette} from 'lib/hooks/usePalette'
-import {PresentedExternal} from '@atproto/api/dist/client/types/app/bsky/embed/external'
+import {AppBskyEmbedExternal} from '@atproto/api'
 
-const ExternalLinkEmbed = ({
+export const ExternalLinkEmbed = ({
   link,
-  onImagePress,
   imageChild,
 }: {
-  link: PresentedExternal
-  onImagePress?: () => void
+  link: AppBskyEmbedExternal.ViewExternal
   imageChild?: React.ReactNode
 }) => {
   const pal = usePalette('default')
   return (
     <>
       {link.thumb ? (
-        <AutoSizedImage
-          uri={link.thumb}
-          style={styles.extImage}
-          onPress={onImagePress}>
+        <AutoSizedImage uri={link.thumb} style={styles.extImage}>
           {imageChild}
         </AutoSizedImage>
       ) : undefined}
@@ -65,5 +60,3 @@ const styles = StyleSheet.create({
     marginTop: 4,
   },
 })
-
-export default ExternalLinkEmbed
diff --git a/src/view/com/util/PostEmbeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx
index fee67c9bc..9dc5739a0 100644
--- a/src/view/com/util/PostEmbeds/QuoteEmbed.tsx
+++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx
@@ -1,13 +1,21 @@
-import {StyleSheet} from 'react-native'
 import React from 'react'
+import {StyleProp, StyleSheet, ViewStyle} from 'react-native'
+import {AppBskyEmbedImages, AppBskyEmbedRecordWithMedia} from '@atproto/api'
 import {AtUri} from '../../../../third-party/uri'
 import {PostMeta} from '../PostMeta'
 import {Link} from '../Link'
 import {Text} from '../text/Text'
 import {usePalette} from 'lib/hooks/usePalette'
 import {ComposerOptsQuote} from 'state/models/ui/shell'
+import {PostEmbeds} from '.'
 
-const QuoteEmbed = ({quote}: {quote: ComposerOptsQuote}) => {
+export function QuoteEmbed({
+  quote,
+  style,
+}: {
+  quote: ComposerOptsQuote
+  style?: StyleProp<ViewStyle>
+}) {
   const pal = usePalette('default')
   const itemUrip = new AtUri(quote.uri)
   const itemHref = `/profile/${quote.author.handle}/post/${itemUrip.rkey}`
@@ -16,9 +24,18 @@ const QuoteEmbed = ({quote}: {quote: ComposerOptsQuote}) => {
     () => quote.text.trim().length === 0,
     [quote.text],
   )
+  const imagesEmbed = React.useMemo(
+    () =>
+      quote.embeds?.find(
+        embed =>
+          AppBskyEmbedImages.isView(embed) ||
+          AppBskyEmbedRecordWithMedia.isView(embed),
+      ),
+    [quote.embeds],
+  )
   return (
     <Link
-      style={[styles.container, pal.border]}
+      style={[styles.container, pal.border, style]}
       href={itemHref}
       title={itemTitle}>
       <PostMeta
@@ -37,6 +54,12 @@ const QuoteEmbed = ({quote}: {quote: ComposerOptsQuote}) => {
           quote.text
         )}
       </Text>
+      {AppBskyEmbedImages.isView(imagesEmbed) && (
+        <PostEmbeds embed={imagesEmbed} />
+      )}
+      {AppBskyEmbedRecordWithMedia.isView(imagesEmbed) && (
+        <PostEmbeds embed={imagesEmbed.media} />
+      )}
     </Link>
   )
 }
@@ -48,7 +71,6 @@ const styles = StyleSheet.create({
     borderRadius: 8,
     paddingVertical: 8,
     paddingHorizontal: 12,
-    marginVertical: 8,
     borderWidth: 1,
   },
   quotePost: {
diff --git a/src/view/com/util/post-embeds/YoutubeEmbed.tsx b/src/view/com/util/post-embeds/YoutubeEmbed.tsx
new file mode 100644
index 000000000..2ca0750a3
--- /dev/null
+++ b/src/view/com/util/post-embeds/YoutubeEmbed.tsx
@@ -0,0 +1,55 @@
+import React from 'react'
+import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
+import {usePalette} from 'lib/hooks/usePalette'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {ExternalLinkEmbed} from './ExternalLinkEmbed'
+import {AppBskyEmbedExternal} from '@atproto/api'
+import {Link} from '../Link'
+
+export const YoutubeEmbed = ({
+  link,
+  style,
+}: {
+  link: AppBskyEmbedExternal.ViewExternal
+  style?: StyleProp<ViewStyle>
+}) => {
+  const pal = usePalette('default')
+
+  const imageChild = (
+    <View style={styles.playButton}>
+      <FontAwesomeIcon icon="play" size={24} color="white" />
+    </View>
+  )
+
+  return (
+    <Link
+      style={[styles.extOuter, pal.view, pal.border, style]}
+      href={link.uri}
+      noFeedback>
+      <ExternalLinkEmbed link={link} imageChild={imageChild} />
+    </Link>
+  )
+}
+
+const styles = StyleSheet.create({
+  extOuter: {
+    borderWidth: 1,
+    borderRadius: 8,
+  },
+  playButton: {
+    position: 'absolute',
+    alignSelf: 'center',
+    alignItems: 'center',
+    top: '44%',
+    justifyContent: 'center',
+    backgroundColor: 'black',
+    padding: 10,
+    borderRadius: 50,
+    opacity: 0.8,
+  },
+  webView: {
+    alignItems: 'center',
+    alignContent: 'center',
+    justifyContent: 'center',
+  },
+})
diff --git a/src/view/com/util/PostEmbeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index 02a8aa90e..726bea6e7 100644
--- a/src/view/com/util/PostEmbeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -10,6 +10,7 @@ import {
   AppBskyEmbedImages,
   AppBskyEmbedExternal,
   AppBskyEmbedRecord,
+  AppBskyEmbedRecordWithMedia,
   AppBskyFeedPost,
 } from '@atproto/api'
 import {Link} from '../Link'
@@ -19,15 +20,16 @@ import {ImagesLightbox} from 'state/models/ui/shell'
 import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
 import {saveImageModal} from 'lib/media/manip'
-import YoutubeEmbed from './YoutubeEmbed'
-import ExternalLinkEmbed from './ExternalLinkEmbed'
+import {YoutubeEmbed} from './YoutubeEmbed'
+import {ExternalLinkEmbed} from './ExternalLinkEmbed'
 import {getYoutubeVideoId} from 'lib/strings/url-helpers'
 import QuoteEmbed from './QuoteEmbed'
 
 type Embed =
-  | AppBskyEmbedRecord.Presented
-  | AppBskyEmbedImages.Presented
-  | AppBskyEmbedExternal.Presented
+  | AppBskyEmbedRecord.View
+  | AppBskyEmbedImages.View
+  | AppBskyEmbedExternal.View
+  | AppBskyEmbedRecordWithMedia.View
   | {$type: string; [k: string]: unknown}
 
 export function PostEmbeds({
@@ -39,11 +41,35 @@ export function PostEmbeds({
 }) {
   const pal = usePalette('default')
   const store = useStores()
-  if (AppBskyEmbedRecord.isPresented(embed)) {
+
+  if (
+    AppBskyEmbedRecordWithMedia.isView(embed) &&
+    AppBskyEmbedRecord.isViewRecord(embed.record.record) &&
+    AppBskyFeedPost.isRecord(embed.record.record.value) &&
+    AppBskyFeedPost.validateRecord(embed.record.record.value).success
+  ) {
+    return (
+      <View style={[styles.stackContainer, style]}>
+        <PostEmbeds embed={embed.media} />
+        <QuoteEmbed
+          quote={{
+            author: embed.record.record.author,
+            cid: embed.record.record.cid,
+            uri: embed.record.record.uri,
+            indexedAt: embed.record.record.indexedAt,
+            text: embed.record.record.value.text,
+            embeds: embed.record.record.embeds,
+          }}
+        />
+      </View>
+    )
+  }
+
+  if (AppBskyEmbedRecord.isView(embed)) {
     if (
-      AppBskyEmbedRecord.isPresentedRecord(embed.record) &&
-      AppBskyFeedPost.isRecord(embed.record.record) &&
-      AppBskyFeedPost.validateRecord(embed.record.record).success
+      AppBskyEmbedRecord.isViewRecord(embed.record) &&
+      AppBskyFeedPost.isRecord(embed.record.value) &&
+      AppBskyFeedPost.validateRecord(embed.record.value).success
     ) {
       return (
         <QuoteEmbed
@@ -51,14 +77,17 @@ export function PostEmbeds({
             author: embed.record.author,
             cid: embed.record.cid,
             uri: embed.record.uri,
-            indexedAt: embed.record.record.createdAt, // TODO
-            text: embed.record.record.text,
+            indexedAt: embed.record.indexedAt,
+            text: embed.record.value.text,
+            embeds: embed.record.embeds,
           }}
+          style={style}
         />
       )
     }
   }
-  if (AppBskyEmbedImages.isPresented(embed)) {
+
+  if (AppBskyEmbedImages.isView(embed)) {
     if (embed.images.length > 0) {
       const uris = embed.images.map(img => img.fullsize)
       const openLightbox = (index: number) => {
@@ -129,12 +158,13 @@ export function PostEmbeds({
       }
     }
   }
-  if (AppBskyEmbedExternal.isPresented(embed)) {
+
+  if (AppBskyEmbedExternal.isView(embed)) {
     const link = embed.external
     const youtubeVideoId = getYoutubeVideoId(link.uri)
 
     if (youtubeVideoId) {
-      return <YoutubeEmbed videoId={youtubeVideoId} link={link} />
+      return <YoutubeEmbed link={link} style={style} />
     }
 
     return (
@@ -150,6 +180,9 @@ export function PostEmbeds({
 }
 
 const styles = StyleSheet.create({
+  stackContainer: {
+    gap: 6,
+  },
   imagesContainer: {
     marginTop: 4,
   },
diff --git a/src/view/com/util/text/RichText.tsx b/src/view/com/util/text/RichText.tsx
index d4cf19172..804db002a 100644
--- a/src/view/com/util/text/RichText.tsx
+++ b/src/view/com/util/text/RichText.tsx
@@ -1,20 +1,22 @@
 import React from 'react'
 import {TextStyle, StyleProp} from 'react-native'
+import {RichText as RichTextObj, AppBskyRichtextFacet} from '@atproto/api'
 import {TextLink} from '../Link'
 import {Text} from './Text'
 import {lh} from 'lib/styles'
 import {toShortUrl} from 'lib/strings/url-helpers'
-import {RichText as RichTextObj, Entity} from 'lib/strings/rich-text'
 import {useTheme, TypographyVariant} from 'lib/ThemeContext'
 import {usePalette} from 'lib/hooks/usePalette'
 
 export function RichText({
+  testID,
   type = 'md',
   richText,
   lineHeight = 1.2,
   style,
   numberOfLines,
 }: {
+  testID?: string
   type?: TypographyVariant
   richText?: RichTextObj
   lineHeight?: number
@@ -29,17 +31,24 @@ export function RichText({
     return null
   }
 
-  const {text, entities} = richText
-  if (!entities?.length) {
+  const {text, facets} = richText
+  if (!facets?.length) {
     if (/^\p{Extended_Pictographic}+$/u.test(text) && text.length <= 5) {
       style = {
         fontSize: 26,
         lineHeight: 30,
       }
-      return <Text style={[style, pal.text]}>{text}</Text>
+      return (
+        <Text testID={testID} style={[style, pal.text]}>
+          {text}
+        </Text>
+      )
     }
     return (
-      <Text type={type} style={[style, pal.text, lineHeightStyle]}>
+      <Text
+        testID={testID}
+        type={type}
+        style={[style, pal.text, lineHeightStyle]}>
         {text}
       </Text>
     )
@@ -49,40 +58,40 @@ export function RichText({
   } else if (!Array.isArray(style)) {
     style = [style]
   }
-  entities.sort(sortByIndex)
-  const segments = Array.from(toSegments(text, entities))
+
   const els = []
   let key = 0
-  for (const segment of segments) {
-    if (typeof segment === 'string') {
-      els.push(segment)
+  for (const segment of richText.segments()) {
+    const link = segment.link
+    const mention = segment.mention
+    if (mention && AppBskyRichtextFacet.validateMention(mention).success) {
+      els.push(
+        <TextLink
+          key={key}
+          type={type}
+          text={segment.text}
+          href={`/profile/${mention.did}`}
+          style={[style, lineHeightStyle, pal.link]}
+        />,
+      )
+    } else if (link && AppBskyRichtextFacet.validateLink(link).success) {
+      els.push(
+        <TextLink
+          key={key}
+          type={type}
+          text={toShortUrl(segment.text)}
+          href={link.uri}
+          style={[style, lineHeightStyle, pal.link]}
+        />,
+      )
     } else {
-      if (segment.entity.type === 'mention') {
-        els.push(
-          <TextLink
-            key={key}
-            type={type}
-            text={segment.text}
-            href={`/profile/${segment.entity.value}`}
-            style={[style, lineHeightStyle, 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, lineHeightStyle, pal.link]}
-          />,
-        )
-      }
+      els.push(segment.text)
     }
     key++
   }
   return (
     <Text
+      testID={testID}
       type={type}
       style={[style, pal.text, lineHeightStyle]}
       numberOfLines={numberOfLines}>
@@ -90,38 +99,3 @@ export function RichText({
     </Text>
   )
 }
-
-function sortByIndex(a: Entity, b: Entity) {
-  return a.index.start - b.index.start
-}
-
-function* toSegments(text: string, entities: Entity[]) {
-  let cursor = 0
-  let i = 0
-  do {
-    let currEnt = entities[i]
-    if (cursor < currEnt.index.start) {
-      yield text.slice(cursor, currEnt.index.start)
-    } else if (cursor > currEnt.index.start) {
-      i++
-      continue
-    }
-    if (currEnt.index.start < currEnt.index.end) {
-      let subtext = text.slice(currEnt.index.start, currEnt.index.end)
-      if (!subtext.trim()) {
-        // dont yield links to empty strings
-        yield subtext
-      } else {
-        yield {
-          entity: currEnt,
-          text: subtext,
-        }
-      }
-    }
-    cursor = currEnt.index.end
-    i++
-  } while (i < entities.length)
-  if (cursor < text.length) {
-    yield text.slice(cursor, text.length)
-  }
-}
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 4f2bc4c15..871aae9c7 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -33,6 +33,7 @@ export const HomeScreen = withAuthRequired((_opts: Props) => {
 
   useFocusEffect(
     React.useCallback(() => {
+      store.shell.setMinimalShellMode(false)
       store.shell.setIsDrawerSwipeDisabled(selectedPage > 0)
       return () => {
         store.shell.setIsDrawerSwipeDisabled(false)
@@ -42,6 +43,7 @@ export const HomeScreen = withAuthRequired((_opts: Props) => {
 
   const onPageSelected = React.useCallback(
     (index: number) => {
+      store.shell.setMinimalShellMode(false)
       setSelectedPage(index)
       store.shell.setIsDrawerSwipeDisabled(index > 0)
     },
@@ -54,7 +56,13 @@ export const HomeScreen = withAuthRequired((_opts: Props) => {
 
   const renderTabBar = React.useCallback(
     (props: RenderTabBarFnProps) => {
-      return <FeedsTabBar {...props} onPressSelected={onPressSelected} />
+      return (
+        <FeedsTabBar
+          {...props}
+          testID="homeScreenFeedTabs"
+          onPressSelected={onPressSelected}
+        />
+      )
     },
     [onPressSelected],
   )
@@ -66,27 +74,36 @@ export const HomeScreen = withAuthRequired((_opts: Props) => {
   const initialPage = store.me.follows.isEmpty ? 1 : 0
   return (
     <Pager
+      testID="homeScreen"
       onPageSelected={onPageSelected}
       renderTabBar={renderTabBar}
       tabBarPosition="top"
       initialPage={initialPage}>
       <FeedPage
         key="1"
+        testID="followingFeedPage"
         isPageFocused={selectedPage === 0}
         feed={store.me.mainFeed}
         renderEmptyState={renderFollowingEmptyState}
       />
-      <FeedPage key="2" isPageFocused={selectedPage === 1} feed={algoFeed} />
+      <FeedPage
+        key="2"
+        testID="whatshotFeedPage"
+        isPageFocused={selectedPage === 1}
+        feed={algoFeed}
+      />
     </Pager>
   )
 })
 
 const FeedPage = observer(
   ({
+    testID,
     isPageFocused,
     feed,
     renderEmptyState,
   }: {
+    testID?: string
     feed: FeedModel
     isPageFocused: boolean
     renderEmptyState?: () => JSX.Element
@@ -163,9 +180,9 @@ const FeedPage = observer(
     }, [feed, scrollToTop])
 
     return (
-      <View style={s.h100pct}>
+      <View testID={testID} style={s.h100pct}>
         <Feed
-          testID="homeFeed"
+          testID={testID ? `${testID}-feed` : undefined}
           key="default"
           feed={feed}
           scrollElRef={scrollElRef}
diff --git a/src/view/screens/NotFound.tsx b/src/view/screens/NotFound.tsx
index 6ab37f117..cb52da58b 100644
--- a/src/view/screens/NotFound.tsx
+++ b/src/view/screens/NotFound.tsx
@@ -1,16 +1,28 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
-import {useNavigation, StackActions} from '@react-navigation/native'
+import {
+  useNavigation,
+  StackActions,
+  useFocusEffect,
+} from '@react-navigation/native'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {Text} from '../com/util/text/Text'
 import {Button} from 'view/com/util/forms/Button'
 import {NavigationProp} from 'lib/routes/types'
 import {usePalette} from 'lib/hooks/usePalette'
+import {useStores} from 'state/index'
 import {s} from 'lib/styles'
 
 export const NotFoundScreen = () => {
   const pal = usePalette('default')
   const navigation = useNavigation<NavigationProp>()
+  const store = useStores()
+
+  useFocusEffect(
+    React.useCallback(() => {
+      store.shell.setMinimalShellMode(false)
+    }, [store]),
+  )
 
   const canGoBack = navigation.canGoBack()
   const onPressHome = React.useCallback(() => {
diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx
index 7da563843..e5521c7ac 100644
--- a/src/view/screens/Notifications.tsx
+++ b/src/view/screens/Notifications.tsx
@@ -72,6 +72,7 @@ export const NotificationsScreen = withAuthRequired(
     // =
     useFocusEffect(
       React.useCallback(() => {
+        store.shell.setMinimalShellMode(false)
         store.log.debug('NotificationsScreen: Updating feed')
         const softResetSub = store.onScreenSoftReset(scrollToTop)
         store.me.notifications.loadUnreadCount()
@@ -86,7 +87,7 @@ export const NotificationsScreen = withAuthRequired(
     )
 
     return (
-      <View style={s.hContentRegion}>
+      <View testID="notificationsScreen" style={s.hContentRegion}>
         <ViewHeader title="Notifications" canGoBack={false} />
         <Feed
           view={store.me.notifications}
diff --git a/src/view/screens/PostUpvotedBy.tsx b/src/view/screens/PostLikedBy.tsx
index 35b55f3c4..fb44f1f9b 100644
--- a/src/view/screens/PostUpvotedBy.tsx
+++ b/src/view/screens/PostLikedBy.tsx
@@ -4,12 +4,12 @@ import {useFocusEffect} from '@react-navigation/native'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
 import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {ViewHeader} from '../com/util/ViewHeader'
-import {PostVotedBy as PostLikedByComponent} from '../com/post-thread/PostVotedBy'
+import {PostLikedBy as PostLikedByComponent} from '../com/post-thread/PostLikedBy'
 import {useStores} from 'state/index'
 import {makeRecordUri} from 'lib/strings/url-helpers'
 
-type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostUpvotedBy'>
-export const PostUpvotedByScreen = withAuthRequired(({route}: Props) => {
+type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostLikedBy'>
+export const PostLikedByScreen = withAuthRequired(({route}: Props) => {
   const store = useStores()
   const {name, rkey} = route.params
   const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
@@ -23,7 +23,7 @@ export const PostUpvotedByScreen = withAuthRequired(({route}: Props) => {
   return (
     <View>
       <ViewHeader title="Liked by" />
-      <PostLikedByComponent uri={uri} direction="up" />
+      <PostLikedByComponent uri={uri} />
     </View>
   )
 })
diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx
index ad54126b6..9bfdcc95a 100644
--- a/src/view/screens/PostThread.tsx
+++ b/src/view/screens/PostThread.tsx
@@ -29,8 +29,8 @@ export const PostThreadScreen = withAuthRequired(({route}: Props) => {
 
   useFocusEffect(
     React.useCallback(() => {
-      const threadCleanup = view.registerListeners()
       store.shell.setMinimalShellMode(false)
+      const threadCleanup = view.registerListeners()
       if (!view.hasLoaded && !view.isLoading) {
         view.setup().catch(err => {
           store.log.error('Failed to fetch thread', err)
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 65f1fef26..556578e77 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -42,6 +42,7 @@ export const ProfileScreen = withAuthRequired(
     useFocusEffect(
       React.useCallback(() => {
         let aborted = false
+        store.shell.setMinimalShellMode(false)
         const feedCleanup = uiState.feed.registerListeners()
         if (hasSetup) {
           uiState.update()
@@ -57,7 +58,7 @@ export const ProfileScreen = withAuthRequired(
           aborted = true
           feedCleanup()
         }
-      }, [hasSetup, uiState]),
+      }, [hasSetup, uiState, store]),
     )
 
     // events
diff --git a/src/view/screens/Search.tsx b/src/view/screens/Search.tsx
index 641d144ae..e6947013e 100644
--- a/src/view/screens/Search.tsx
+++ b/src/view/screens/Search.tsx
@@ -152,6 +152,7 @@ export const SearchScreen = withAuthRequired(
                   {autocompleteView.searchRes.map(item => (
                     <ProfileCard
                       key={item.did}
+                      testID={`searchAutoCompleteResult-${item.handle}`}
                       handle={item.handle}
                       displayName={item.displayName}
                       avatar={item.avatar}
diff --git a/src/view/shell/BottomBar.tsx b/src/view/shell/BottomBar.tsx
index bfbd7f0a2..e46eeb991 100644
--- a/src/view/shell/BottomBar.tsx
+++ b/src/view/shell/BottomBar.tsx
@@ -112,6 +112,7 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
         footerMinimalShellTransform,
       ]}>
       <Btn
+        testID="bottomBarHomeBtn"
         icon={
           isAtHome ? (
             <HomeIconSolid
@@ -130,6 +131,7 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
         onPress={onPressHome}
       />
       <Btn
+        testID="bottomBarSearchBtn"
         icon={
           isAtSearch ? (
             <MagnifyingGlassIcon2Solid
@@ -148,6 +150,7 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
         onPress={onPressSearch}
       />
       <Btn
+        testID="bottomBarNotificationsBtn"
         icon={
           isAtNotifications ? (
             <BellIconSolid
@@ -167,6 +170,7 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
         notificationCount={store.me.notifications.unreadCount}
       />
       <Btn
+        testID="bottomBarProfileBtn"
         icon={
           <View style={styles.ctrlIconSizingWrapper}>
             <UserIcon
@@ -183,11 +187,13 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
 })
 
 function Btn({
+  testID,
   icon,
   notificationCount,
   onPress,
   onLongPress,
 }: {
+  testID?: string
   icon: JSX.Element
   notificationCount?: number
   onPress?: (event: GestureResponderEvent) => void
@@ -195,6 +201,7 @@ function Btn({
 }) {
   return (
     <TouchableOpacity
+      testID={testID}
       style={styles.ctrl}
       onPress={onLongPress ? onPress : undefined}
       onPressIn={onLongPress ? undefined : onPress}
diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx
index a33cf8c4e..ccf64c0e6 100644
--- a/src/view/shell/Drawer.tsx
+++ b/src/view/shell/Drawer.tsx
@@ -162,7 +162,7 @@ export const DrawerContent = observer(() => {
 
   return (
     <View
-      testID="menuView"
+      testID="drawer"
       style={[
         styles.view,
         theme.colorScheme === 'light' ? pal.view : styles.viewDarkMode,
diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx
index eec0f8ed4..84242c283 100644
--- a/src/view/shell/index.tsx
+++ b/src/view/shell/index.tsx
@@ -7,11 +7,9 @@ import {useNavigationState} from '@react-navigation/native'
 import {useStores} from 'state/index'
 import {ModalsContainer} from 'view/com/modals/Modal'
 import {Lightbox} from 'view/com/lightbox/Lightbox'
-import {Text} from 'view/com/util/text/Text'
 import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
 import {DrawerContent} from './Drawer'
 import {Composer} from './Composer'
-import {s} from 'lib/styles'
 import {useTheme} from 'lib/ThemeContext'
 import {usePalette} from 'lib/hooks/usePalette'
 import {RoutesContainer, TabsNavigator} from '../../Navigation'
@@ -72,41 +70,6 @@ const ShellInner = observer(() => {
 export const Shell: React.FC = observer(() => {
   const theme = useTheme()
   const pal = usePalette('default')
-  const store = useStores()
-
-  if (store.hackUpgradeNeeded) {
-    return (
-      <View style={styles.outerContainer}>
-        <View style={[s.flexCol, s.p20, s.h100pct]}>
-          <View style={s.flex1} />
-          <View>
-            <Text type="title-2xl" style={s.pb10}>
-              Update required
-            </Text>
-            <Text style={[s.pb20, s.bold]}>
-              Please update your app to the latest version. If no update is
-              available yet, please check the App Store in a day or so.
-            </Text>
-            <Text type="title" style={s.pb10}>
-              What's happening?
-            </Text>
-            <Text style={s.pb10}>
-              We're in the final stages of the AT Protocol's v1 development. To
-              make sure everything works as well as possible, we're making final
-              breaking changes to the APIs.
-            </Text>
-            <Text>
-              If we didn't botch this process, a new version of the app should
-              be available now.
-            </Text>
-          </View>
-          <View style={s.flex1} />
-          <View style={s.footerSpacer} />
-        </View>
-      </View>
-    )
-  }
-
   return (
     <View testID="mobileShellView" style={[styles.outerContainer, pal.view]}>
       <StatusBar