about summary refs log tree commit diff
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2023-03-31 13:17:26 -0500
committerGitHub <noreply@github.com>2023-03-31 13:17:26 -0500
commita3334a01a221877d3e06e02f960fda441f3460bd (patch)
tree64cdbb1232d1a3c00750c346b6e3ae529b51d1b0
parent19f3a2fa92a61ddb785fc4e42d73792c1d0e772c (diff)
downloadvoidsky-a3334a01a221877d3e06e02f960fda441f3460bd.tar.zst
Lex refactor (#362)
* Remove the hackcheck for upgrades

* Rename the PostEmbeds folder to match the codebase style

* Updates to latest lex refactor

* Update to use new bsky agent

* Update to use api package's richtext library

* Switch to upsertProfile

* Add TextEncoder/TextDecoder polyfill

* Add Intl.Segmenter polyfill

* Update composer to calculate lengths by grapheme

* Fix detox

* Fix login in e2e

* Create account e2e passing

* Implement an e2e mocking framework

* Don't use private methods on mobx models as mobx can't track them

* Add tooling for e2e-specific builds and add e2e media-picker mock

* Add some tests and fix some bugs around profile editing

* Add shell tests

* Add home screen tests

* Add thread screen tests

* Add tests for other user profile screens

* Add search screen tests

* Implement profile imagery change tools and tests

* Update to new embed behaviors

* Add post tests

* Fix to profile-screen test

* Fix session resumption

* Update web composer to new api

* 1.11.0

* Fix pagination cursor parameters

* Add quote posts to notifications

* Fix embed layouts

* Remove youtube inline player and improve tap handling on link cards

* Reset minimal shell mode on all screen loads and feed swipes (close #299)

* Update podfile.lock

* Improve post notfound UI (close #366)

* Bump atproto packages
-rw-r--r--.detoxrc.js11
-rw-r--r--README.md6
-rw-r--r--__e2e__/jest.config.js (renamed from e2e/jest.config.js)2
-rw-r--r--__e2e__/mock-server.ts75
-rw-r--r--__e2e__/tests/composer.test.ts108
-rw-r--r--__e2e__/tests/create-account.test.ts31
-rw-r--r--__e2e__/tests/home-screen.test.ts92
-rw-r--r--__e2e__/tests/login.test.ts19
-rw-r--r--__e2e__/tests/profile-screen.test.ts173
-rw-r--r--__e2e__/tests/search-screen.test.ts24
-rw-r--r--__e2e__/tests/shell.test.ts34
-rw-r--r--__e2e__/tests/thread-screen.test.ts123
-rw-r--r--__e2e__/util.ts96
-rw-r--r--__tests__/lib/link-meta.test.ts4
-rw-r--r--__tests__/lib/string.test.ts164
-rw-r--r--__tests__/lib/strings/rich-text-sanitize.ts123
-rw-r--r--__tests__/lib/strings/rich-text.ts123
-rw-r--r--app.json2
-rw-r--r--e2e/tests/happyPath.test.js70
-rw-r--r--ios/Podfile.lock466
-rw-r--r--ios/bluesky/Info.plist2
-rw-r--r--jest/test-pds.ts259
-rw-r--r--metro.config.js6
-rw-r--r--package.json26
-rw-r--r--src/App.native.tsx1
-rw-r--r--src/Navigation.tsx4
-rw-r--r--src/lib/api/api-polyfill.ts8
-rw-r--r--src/lib/api/api-polyfill.web.ts3
-rw-r--r--src/lib/api/build-suggested-posts.ts22
-rw-r--r--src/lib/api/feed-manip.ts8
-rw-r--r--src/lib/api/index.ts176
-rw-r--r--src/lib/media/picker.e2e.tsx116
-rw-r--r--src/lib/notifee.ts4
-rw-r--r--src/lib/routes/types.ts2
-rw-r--r--src/lib/strings/rich-text-detection.ts59
-rw-r--r--src/lib/strings/rich-text-sanitize.ts32
-rw-r--r--src/lib/strings/rich-text.ts216
-rw-r--r--src/lib/styles.ts1
-rw-r--r--src/platform/polyfills.ts17
-rw-r--r--src/platform/polyfills.web.ts8
-rw-r--r--src/routes.ts2
-rw-r--r--src/state/index.ts4
-rw-r--r--src/state/models/cache/image-sizes.ts2
-rw-r--r--src/state/models/cache/my-follows.ts21
-rw-r--r--src/state/models/discovery/foafs.ts14
-rw-r--r--src/state/models/discovery/suggested-actors.ts20
-rw-r--r--src/state/models/feed-view.ts153
-rw-r--r--src/state/models/likes-view.ts (renamed from src/state/models/votes-view.ts)30
-rw-r--r--src/state/models/log.ts8
-rw-r--r--src/state/models/me.ts2
-rw-r--r--src/state/models/notifications-view.ts76
-rw-r--r--src/state/models/post-thread-view.ts135
-rw-r--r--src/state/models/post.ts12
-rw-r--r--src/state/models/profile-view.ts116
-rw-r--r--src/state/models/profiles-view.ts2
-rw-r--r--src/state/models/reposted-by-view.ts18
-rw-r--r--src/state/models/root-store.ts41
-rw-r--r--src/state/models/session.ts35
-rw-r--r--src/state/models/suggested-posts-view.ts4
-rw-r--r--src/state/models/ui/create-account.ts4
-rw-r--r--src/state/models/ui/profile.ts12
-rw-r--r--src/state/models/ui/search.ts8
-rw-r--r--src/state/models/ui/shell.ts2
-rw-r--r--src/state/models/user-autocomplete-view.ts18
-rw-r--r--src/state/models/user-followers-view.ts19
-rw-r--r--src/state/models/user-follows-view.ts19
-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
-rw-r--r--tsconfig.json2
-rw-r--r--web/static/js/intl-segmenter-polyfill.min.js2
-rw-r--r--yarn.lock712
133 files changed, 3068 insertions, 2804 deletions
diff --git a/.detoxrc.js b/.detoxrc.js
index fc9cf042b..1fe578e76 100644
--- a/.detoxrc.js
+++ b/.detoxrc.js
@@ -3,7 +3,7 @@ module.exports = {
   testRunner: {
     args: {
       $0: 'jest',
-      config: 'e2e/jest.config.js',
+      config: '__e2e__/jest.config.js',
     },
     jest: {
       setupTimeout: 120000,
@@ -12,15 +12,16 @@ module.exports = {
   apps: {
     'ios.debug': {
       type: 'ios.app',
-      binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/app.app',
+      binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/bluesky.app',
       build:
-        'xcodebuild -workspace ios/app.xcworkspace -scheme app -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build',
+        'xcodebuild -workspace ios/bluesky.xcworkspace -scheme bluesky -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build',
     },
     'ios.release': {
       type: 'ios.app',
-      binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/app.app',
+      binaryPath:
+        'ios/build/Build/Products/Release-iphonesimulator/bluesky.app',
       build:
-        'xcodebuild -workspace ios/app.xcworkspace -scheme app -configuration Release -sdk iphonesimulator -derivedDataPath ios/build',
+        'xcodebuild -workspace ios/bluesky.xcworkspace -scheme bluesky -configuration Release -sdk iphonesimulator -derivedDataPath ios/build',
     },
     'android.debug': {
       type: 'android.apk',
diff --git a/README.md b/README.md
index 8ae03e70c..c6c72c03d 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,12 @@
   - iOS: `yarn ios`
   - Android: `yarn android`
   - Web: `yarn web`
+- Run e2e tests
+  - Start in various console tabs:
+    - `yarn e2e:server`
+    - `yarn e2e:metro`
+  - Run once: `yarn e2e:build`
+  - Each test run: `yarn e2e:run`
 - Tips
   - `npx react-native info` Checks what has been installed.
   - On M1 macs, [you need to exclude "arm64" from the target architectures](https://stackoverflow.com/a/65399525)
diff --git a/e2e/jest.config.js b/__e2e__/jest.config.js
index 3472f7161..80c2ad5b3 100644
--- a/e2e/jest.config.js
+++ b/__e2e__/jest.config.js
@@ -1,7 +1,7 @@
 /** @type {import('@jest/types').Config.InitialOptions} */
 module.exports = {
   rootDir: '..',
-  testMatch: ['<rootDir>/e2e/**/*.test.js'],
+  testMatch: ['<rootDir>/__e2e__/**/*.test.ts'],
   testTimeout: 120000,
   maxWorkers: 1,
   globalSetup: 'detox/runners/jest/globalSetup',
diff --git a/__e2e__/mock-server.ts b/__e2e__/mock-server.ts
new file mode 100644
index 000000000..7a2be6060
--- /dev/null
+++ b/__e2e__/mock-server.ts
@@ -0,0 +1,75 @@
+import {createServer as createHTTPServer} from 'node:http'
+import {parse} from 'node:url'
+import {createServer, TestPDS} from '../jest/test-pds'
+
+async function main() {
+  let server: TestPDS
+  createHTTPServer(async (req, res) => {
+    const url = parse(req.url || '/', true)
+    if (req.method !== 'POST') {
+      return res.writeHead(200).end()
+    }
+    try {
+      console.log('Closing old server')
+      await server?.close()
+      console.log('Starting new server')
+      server = await createServer()
+      console.log('Listening at', server.pdsUrl)
+      if (url?.query) {
+        if ('users' in url.query) {
+          console.log('Generating mock users')
+          await server.mocker.createUser('alice')
+          await server.mocker.createUser('bob')
+          await server.mocker.createUser('carla')
+          await server.mocker.users.alice.agent.upsertProfile(() => ({
+            displayName: 'Alice',
+            description: 'Test user 1',
+          }))
+          await server.mocker.users.bob.agent.upsertProfile(() => ({
+            displayName: 'Bob',
+            description: 'Test user 2',
+          }))
+          await server.mocker.users.carla.agent.upsertProfile(() => ({
+            displayName: 'Carla',
+            description: 'Test user 3',
+          }))
+        }
+        if ('follows' in url.query) {
+          console.log('Generating mock follows')
+          await server.mocker.follow('alice', 'bob')
+          await server.mocker.follow('alice', 'carla')
+          await server.mocker.follow('bob', 'alice')
+          await server.mocker.follow('bob', 'carla')
+          await server.mocker.follow('carla', 'alice')
+          await server.mocker.follow('carla', 'bob')
+        }
+        if ('posts' in url.query) {
+          console.log('Generating mock posts')
+          for (let user in server.mocker.users) {
+            await server.mocker.users[user].agent.post({text: 'Post'})
+          }
+        }
+        if ('thread' in url.query) {
+          console.log('Generating mock posts')
+          const res = await server.mocker.users.bob.agent.post({
+            text: 'Thread root',
+          })
+          await server.mocker.users.carla.agent.post({
+            text: 'Thread reply',
+            reply: {
+              parent: {cid: res.cid, uri: res.uri},
+              root: {cid: res.cid, uri: res.uri},
+            },
+          })
+        }
+      }
+      console.log('Ready')
+      return res.writeHead(200).end(server.pdsUrl)
+    } catch (e) {
+      console.error('Error!', e)
+      return res.writeHead(500).end()
+    }
+  }).listen(1986)
+  console.log('Mock server manager listening on 1986')
+}
+main()
diff --git a/__e2e__/tests/composer.test.ts b/__e2e__/tests/composer.test.ts
new file mode 100644
index 000000000..afc23cc13
--- /dev/null
+++ b/__e2e__/tests/composer.test.ts
@@ -0,0 +1,108 @@
+/* eslint-env detox/detox */
+
+import {openApp, login, createServer, sleep} from '../util'
+
+describe('Composer', () => {
+  let service: string
+  beforeAll(async () => {
+    service = await createServer('?users')
+    await openApp({
+      permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
+    })
+  })
+
+  it('Login', async () => {
+    await login(service, 'alice', 'hunter2')
+    await element(by.id('homeScreenFeedTabs-Following')).tap()
+  })
+
+  it('Post text only', async () => {
+    await element(by.id('composeFAB')).tap()
+    await device.takeScreenshot('1- opened composer')
+    await element(by.id('composerTextInput')).typeText('Post text only')
+    await device.takeScreenshot('2- entered text')
+    await element(by.id('composerPublishBtn')).tap()
+    await device.takeScreenshot('3- opened general section')
+    await expect(element(by.id('composeFAB'))).toBeVisible()
+  })
+
+  it('Post with an image', async () => {
+    await element(by.id('composeFAB')).tap()
+    await element(by.id('composerTextInput')).typeText('Post with an image')
+    await element(by.id('openGalleryBtn')).tap()
+    await sleep(1e3)
+    await element(by.id('composerPublishBtn')).tap()
+    await expect(element(by.id('composeFAB'))).toBeVisible()
+  })
+
+  it('Post with a link card', async () => {
+    await element(by.id('composeFAB')).tap()
+    await element(by.id('composerTextInput')).typeText(
+      'Post with a https://example.com link card',
+    )
+    await element(by.id('addLinkCardBtn')).tap()
+    await element(by.id('composerPublishBtn')).tap()
+    await expect(element(by.id('composeFAB'))).toBeVisible()
+  })
+
+  it('Reply text only', async () => {
+    const post = by.id('feedItem-by-alice.test')
+    await element(by.id('replyBtn').withAncestor(post)).atIndex(0).tap()
+    await element(by.id('composerTextInput')).typeText('Reply text only')
+    await element(by.id('composerPublishBtn')).tap()
+    await expect(element(by.id('composeFAB'))).toBeVisible()
+  })
+
+  it('Reply with an image', async () => {
+    const post = by.id('feedItem-by-alice.test')
+    await element(by.id('replyBtn').withAncestor(post)).atIndex(0).tap()
+    await element(by.id('composerTextInput')).typeText('Reply with an image')
+    await element(by.id('openGalleryBtn')).tap()
+    await sleep(1e3)
+    await element(by.id('composerPublishBtn')).tap()
+    await expect(element(by.id('composeFAB'))).toBeVisible()
+  })
+
+  it('Reply with a link card', async () => {
+    const post = by.id('feedItem-by-alice.test')
+    await element(by.id('replyBtn').withAncestor(post)).atIndex(0).tap()
+    await element(by.id('composerTextInput')).typeText(
+      'Reply with a https://example.com link card',
+    )
+    await element(by.id('addLinkCardBtn')).tap()
+    await element(by.id('composerPublishBtn')).tap()
+    await expect(element(by.id('composeFAB'))).toBeVisible()
+  })
+
+  it('QP text only', async () => {
+    const post = by.id('feedItem-by-alice.test')
+    await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap()
+    await element(by.id('quoteBtn').withAncestor(by.id('repostModal'))).tap()
+    await element(by.id('composerTextInput')).typeText('QP text only')
+    await element(by.id('composerPublishBtn')).tap()
+    await expect(element(by.id('composeFAB'))).toBeVisible()
+  })
+
+  it('QP with an image', async () => {
+    const post = by.id('feedItem-by-alice.test')
+    await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap()
+    await element(by.id('quoteBtn').withAncestor(by.id('repostModal'))).tap()
+    await element(by.id('composerTextInput')).typeText('QP with an image')
+    await element(by.id('openGalleryBtn')).tap()
+    await sleep(1e3)
+    await element(by.id('composerPublishBtn')).tap()
+    await expect(element(by.id('composeFAB'))).toBeVisible()
+  })
+
+  it('QP with a link card', async () => {
+    const post = by.id('feedItem-by-alice.test')
+    await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap()
+    await element(by.id('quoteBtn').withAncestor(by.id('repostModal'))).tap()
+    await element(by.id('composerTextInput')).typeText(
+      'QP with a https://example.com link card',
+    )
+    await element(by.id('addLinkCardBtn')).tap()
+    await element(by.id('composerPublishBtn')).tap()
+    await expect(element(by.id('composeFAB'))).toBeVisible()
+  })
+})
diff --git a/__e2e__/tests/create-account.test.ts b/__e2e__/tests/create-account.test.ts
new file mode 100644
index 000000000..7b2e00fb5
--- /dev/null
+++ b/__e2e__/tests/create-account.test.ts
@@ -0,0 +1,31 @@
+/* eslint-env detox/detox */
+
+import {openApp, createServer} from '../util'
+
+describe('Create account', () => {
+  let service: string
+  beforeAll(async () => {
+    service = await createServer('mock0')
+    await openApp({permissions: {notifications: 'YES'}})
+  })
+
+  it('I can create a new account', async () => {
+    await element(by.id('createAccountButton')).tap()
+    await device.takeScreenshot('1- opened create account screen')
+    await element(by.id('otherServerBtn')).tap()
+    await device.takeScreenshot('2- selected other server')
+    await element(by.id('customServerInput')).clearText()
+    await element(by.id('customServerInput')).typeText(service)
+    await device.takeScreenshot('3- input test server URL')
+    await element(by.id('nextBtn')).tap()
+    await element(by.id('emailInput')).typeText('example@test.com')
+    await element(by.id('passwordInput')).typeText('hunter2')
+    await element(by.id('is13Input')).tap()
+    await device.takeScreenshot('4- entered account details')
+    await element(by.id('nextBtn')).tap()
+    await element(by.id('handleInput')).typeText('e2e-test')
+    await device.takeScreenshot('4- entered handle')
+    await element(by.id('nextBtn')).tap()
+    await expect(element(by.id('homeScreen'))).toBeVisible()
+  })
+})
diff --git a/__e2e__/tests/home-screen.test.ts b/__e2e__/tests/home-screen.test.ts
new file mode 100644
index 000000000..1ec1774f3
--- /dev/null
+++ b/__e2e__/tests/home-screen.test.ts
@@ -0,0 +1,92 @@
+/* eslint-env detox/detox */
+
+import {openApp, login, createServer} from '../util'
+
+describe('Home screen', () => {
+  let service: string
+  beforeAll(async () => {
+    service = await createServer('?users&follows&posts')
+    await openApp({permissions: {notifications: 'YES'}})
+  })
+
+  it('Login', async () => {
+    await login(service, 'alice', 'hunter2')
+    await element(by.id('homeScreenFeedTabs-Following')).tap()
+  })
+
+  it('Can like posts', async () => {
+    const carlaPosts = by.id('feedItem-by-carla.test')
+    await expect(
+      element(by.id('likeCount').withAncestor(carlaPosts)).atIndex(0),
+    ).toHaveText('0')
+    await element(by.id('likeBtn').withAncestor(carlaPosts)).atIndex(0).tap()
+    await expect(
+      element(by.id('likeCount').withAncestor(carlaPosts)).atIndex(0),
+    ).toHaveText('1')
+    await element(by.id('likeBtn').withAncestor(carlaPosts)).atIndex(0).tap()
+    await expect(
+      element(by.id('likeCount').withAncestor(carlaPosts)).atIndex(0),
+    ).toHaveText('0')
+  })
+
+  it('Can repost posts', async () => {
+    const carlaPosts = by.id('feedItem-by-carla.test')
+    await expect(
+      element(by.id('repostCount').withAncestor(carlaPosts)).atIndex(0),
+    ).toHaveText('0')
+    await element(by.id('repostBtn').withAncestor(carlaPosts)).atIndex(0).tap()
+    await expect(element(by.id('repostModal'))).toBeVisible()
+    await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
+    await expect(element(by.id('repostModal'))).not.toBeVisible()
+    await expect(
+      element(by.id('repostCount').withAncestor(carlaPosts)).atIndex(0),
+    ).toHaveText('1')
+    await element(by.id('repostBtn').withAncestor(carlaPosts)).atIndex(0).tap()
+    await expect(element(by.id('repostModal'))).toBeVisible()
+    await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
+    await expect(element(by.id('repostModal'))).not.toBeVisible()
+    await expect(
+      element(by.id('repostCount').withAncestor(carlaPosts)).atIndex(0),
+    ).toHaveText('0')
+  })
+
+  it('Can report posts', async () => {
+    const carlaPosts = by.id('feedItem-by-carla.test')
+    await element(by.id('postDropdownBtn').withAncestor(carlaPosts))
+      .atIndex(0)
+      .tap()
+    await element(by.id('postDropdownReportBtn')).tap()
+    await expect(element(by.id('reportPostModal'))).toBeVisible()
+    await element(by.id('reportPostRadios-spam')).tap()
+    await element(by.id('sendReportBtn')).tap()
+    await expect(element(by.id('reportPostModal'))).not.toBeVisible()
+  })
+
+  it('Can swipe between feeds', async () => {
+    await element(by.id('homeScreen')).swipe('left', 'fast', 0.75)
+    await expect(element(by.id('whatshotFeedPage'))).toBeVisible()
+    await element(by.id('homeScreen')).swipe('right', 'fast', 0.75)
+    await expect(element(by.id('followingFeedPage'))).toBeVisible()
+  })
+
+  it('Can tap between feeds', async () => {
+    await element(by.id("homeScreenFeedTabs-What's hot")).tap()
+    await expect(element(by.id('whatshotFeedPage'))).toBeVisible()
+    await element(by.id('homeScreenFeedTabs-Following')).tap()
+    await expect(element(by.id('followingFeedPage'))).toBeVisible()
+  })
+
+  it('Can delete posts', async () => {
+    const alicePosts = by.id('feedItem-by-alice.test')
+    await expect(element(alicePosts.withDescendant(by.text('Post')))).toExist()
+    await element(by.id('postDropdownBtn').withAncestor(alicePosts))
+      .atIndex(0)
+      .tap()
+    await element(by.id('postDropdownDeleteBtn')).tap()
+    await expect(element(by.id('confirmModal'))).toBeVisible()
+    await element(by.id('confirmBtn')).tap()
+    await expect(
+      element(alicePosts.withDescendant(by.text('Post'))),
+    ).not.toExist()
+  })
+})
diff --git a/__e2e__/tests/login.test.ts b/__e2e__/tests/login.test.ts
new file mode 100644
index 000000000..788016db6
--- /dev/null
+++ b/__e2e__/tests/login.test.ts
@@ -0,0 +1,19 @@
+/* eslint-env detox/detox */
+
+import {openApp, login, createServer} from '../util'
+
+describe('Login', () => {
+  let service: string
+  beforeAll(async () => {
+    service = await createServer('?users')
+    await openApp({permissions: {notifications: 'YES'}})
+  })
+
+  it('As Alice, I can login', async () => {
+    await expect(element(by.id('signInButton'))).toBeVisible()
+    await login(service, 'alice', 'hunter2', {
+      takeScreenshots: true,
+    })
+    await device.takeScreenshot('5- opened home screen')
+  })
+})
diff --git a/__e2e__/tests/profile-screen.test.ts b/__e2e__/tests/profile-screen.test.ts
new file mode 100644
index 000000000..e1b6dcaf4
--- /dev/null
+++ b/__e2e__/tests/profile-screen.test.ts
@@ -0,0 +1,173 @@
+/* eslint-env detox/detox */
+
+import {openApp, login, createServer, sleep} from '../util'
+
+describe('Profile screen', () => {
+  let service: string
+  beforeAll(async () => {
+    service = await createServer('?users&posts')
+    await openApp({
+      permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
+    })
+  })
+
+  it('Login and navigate to my profile', async () => {
+    await expect(element(by.id('signInButton'))).toBeVisible()
+    await login(service, 'alice', 'hunter2')
+    await element(by.id('bottomBarProfileBtn')).tap()
+  })
+
+  it('Open and close edit profile modal', async () => {
+    await element(by.id('profileHeaderEditProfileButton')).tap()
+    await expect(element(by.id('editProfileModal'))).toBeVisible()
+    await element(by.id('editProfileCancelBtn')).tap()
+    await expect(element(by.id('editProfileModal'))).not.toBeVisible()
+  })
+
+  it('Edit display name and description via the edit profile modal', async () => {
+    await element(by.id('profileHeaderEditProfileButton')).tap()
+    await expect(element(by.id('editProfileModal'))).toBeVisible()
+    await element(by.id('editProfileDisplayNameInput')).clearText()
+    await element(by.id('editProfileDisplayNameInput')).typeText('Alicia')
+    await element(by.id('editProfileDescriptionInput')).clearText()
+    await element(by.id('editProfileDescriptionInput')).typeText(
+      'One cool hacker',
+    )
+    await element(by.id('editProfileSaveBtn')).tap()
+    await expect(element(by.id('editProfileModal'))).not.toBeVisible()
+    await expect(element(by.id('profileHeaderDisplayName'))).toHaveText(
+      'Alicia',
+    )
+    await expect(element(by.id('profileHeaderDescription'))).toHaveText(
+      'One cool hacker',
+    )
+  })
+
+  it('Remove display name and description via the edit profile modal', async () => {
+    await element(by.id('profileHeaderEditProfileButton')).tap()
+    await expect(element(by.id('editProfileModal'))).toBeVisible()
+    await element(by.id('editProfileDisplayNameInput')).clearText()
+    await element(by.id('editProfileDescriptionInput')).clearText()
+    await element(by.id('editProfileSaveBtn')).tap()
+    await expect(element(by.id('editProfileModal'))).not.toBeVisible()
+    await expect(element(by.id('profileHeaderDisplayName'))).toHaveText(
+      'alice.test',
+    )
+    await expect(element(by.id('profileHeaderDescription'))).toHaveText('')
+  })
+
+  it('Set avi and banner via the edit profile modal', async () => {
+    await expect(element(by.id('userBannerFallback'))).toExist()
+    await expect(element(by.id('userAvatarFallback'))).toExist()
+    await element(by.id('profileHeaderEditProfileButton')).tap()
+    await expect(element(by.id('editProfileModal'))).toBeVisible()
+    await element(by.id('changeBannerBtn')).tap()
+    await element(by.id('changeBannerLibraryBtn')).tap()
+    await sleep(3e3)
+    await element(by.id('changeAvatarBtn')).tap()
+    await element(by.id('changeAvatarLibraryBtn')).tap()
+    await sleep(3e3)
+    await element(by.id('editProfileSaveBtn')).tap()
+    await expect(element(by.id('editProfileModal'))).not.toBeVisible()
+    await expect(element(by.id('userBannerImage'))).toExist()
+    await expect(element(by.id('userAvatarImage'))).toExist()
+  })
+
+  it('Remove avi and banner via the edit profile modal', async () => {
+    await expect(element(by.id('userBannerImage'))).toExist()
+    await expect(element(by.id('userAvatarImage'))).toExist()
+    await element(by.id('profileHeaderEditProfileButton')).tap()
+    await expect(element(by.id('editProfileModal'))).toBeVisible()
+    await element(by.id('changeBannerBtn')).tap()
+    await element(by.id('changeBannerRemoveBtn')).tap()
+    await element(by.id('changeAvatarBtn')).tap()
+    await element(by.id('changeAvatarRemoveBtn')).tap()
+    await element(by.id('editProfileSaveBtn')).tap()
+    await expect(element(by.id('editProfileModal'))).not.toBeVisible()
+    await expect(element(by.id('userBannerFallback'))).toExist()
+    await expect(element(by.id('userAvatarFallback'))).toExist()
+  })
+
+  it('Navigate to another user profile', async () => {
+    await element(by.id('bottomBarSearchBtn')).tap()
+    // have to wait for the toast to clear
+    await waitFor(element(by.id('searchTextInput')))
+      .toBeVisible()
+      .withTimeout(2000)
+    await element(by.id('searchTextInput')).typeText('bob')
+    await element(by.id('searchAutoCompleteResult-bob.test')).tap()
+    await expect(element(by.id('profileView'))).toBeVisible()
+  })
+
+  it('Can follow/unfollow another user', async () => {
+    await element(by.id('followBtn')).tap()
+    await expect(element(by.id('unfollowBtn'))).toBeVisible()
+    await element(by.id('unfollowBtn')).tap()
+    await expect(element(by.id('followBtn'))).toBeVisible()
+  })
+
+  it('Can mute/unmute another user', async () => {
+    await expect(element(by.id('profileHeaderMutedNotice'))).not.toExist()
+    await element(by.id('profileHeaderDropdownBtn')).tap()
+    await element(by.id('profileHeaderDropdownMuteBtn')).tap()
+    await expect(element(by.id('profileHeaderMutedNotice'))).toBeVisible()
+    await element(by.id('profileHeaderDropdownBtn')).tap()
+    await element(by.id('profileHeaderDropdownMuteBtn')).tap()
+    await expect(element(by.id('profileHeaderMutedNotice'))).not.toExist()
+  })
+
+  it('Can report another user', async () => {
+    await element(by.id('profileHeaderDropdownBtn')).tap()
+    await element(by.id('profileHeaderDropdownReportBtn')).tap()
+    await expect(element(by.id('reportAccountModal'))).toBeVisible()
+    await element(by.id('reportAccountRadios-spam')).tap()
+    await element(by.id('sendReportBtn')).tap()
+    await expect(element(by.id('reportAccountModal'))).not.toBeVisible()
+  })
+
+  it('Can like posts', async () => {
+    const posts = by.id('feedItem-by-bob.test')
+    await expect(
+      element(by.id('likeCount').withAncestor(posts)).atIndex(0),
+    ).toHaveText('0')
+    await element(by.id('likeBtn').withAncestor(posts)).atIndex(0).tap()
+    await expect(
+      element(by.id('likeCount').withAncestor(posts)).atIndex(0),
+    ).toHaveText('1')
+    await element(by.id('likeBtn').withAncestor(posts)).atIndex(0).tap()
+    await expect(
+      element(by.id('likeCount').withAncestor(posts)).atIndex(0),
+    ).toHaveText('0')
+  })
+
+  it('Can repost posts', async () => {
+    const posts = by.id('feedItem-by-bob.test')
+    await expect(
+      element(by.id('repostCount').withAncestor(posts)).atIndex(0),
+    ).toHaveText('0')
+    await element(by.id('repostBtn').withAncestor(posts)).atIndex(0).tap()
+    await expect(element(by.id('repostModal'))).toBeVisible()
+    await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
+    await expect(element(by.id('repostModal'))).not.toBeVisible()
+    await expect(
+      element(by.id('repostCount').withAncestor(posts)).atIndex(0),
+    ).toHaveText('1')
+    await element(by.id('repostBtn').withAncestor(posts)).atIndex(0).tap()
+    await expect(element(by.id('repostModal'))).toBeVisible()
+    await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
+    await expect(element(by.id('repostModal'))).not.toBeVisible()
+    await expect(
+      element(by.id('repostCount').withAncestor(posts)).atIndex(0),
+    ).toHaveText('0')
+  })
+
+  it('Can report posts', async () => {
+    const posts = by.id('feedItem-by-bob.test')
+    await element(by.id('postDropdownBtn').withAncestor(posts)).atIndex(0).tap()
+    await element(by.id('postDropdownReportBtn')).tap()
+    await expect(element(by.id('reportPostModal'))).toBeVisible()
+    await element(by.id('reportPostRadios-spam')).tap()
+    await element(by.id('sendReportBtn')).tap()
+    await expect(element(by.id('reportPostModal'))).not.toBeVisible()
+  })
+})
diff --git a/__e2e__/tests/search-screen.test.ts b/__e2e__/tests/search-screen.test.ts
new file mode 100644
index 000000000..093d97c89
--- /dev/null
+++ b/__e2e__/tests/search-screen.test.ts
@@ -0,0 +1,24 @@
+/* eslint-env detox/detox */
+
+import {openApp, login, createServer} from '../util'
+
+describe('Search screen', () => {
+  let service: string
+  beforeAll(async () => {
+    service = await createServer('?users')
+    await openApp({
+      permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
+    })
+  })
+
+  it('Login', async () => {
+    await login(service, 'alice', 'hunter2')
+  })
+
+  it('Navigate to another user profile via autocomplete', async () => {
+    await element(by.id('bottomBarSearchBtn')).tap()
+    await element(by.id('searchTextInput')).typeText('bob')
+    await element(by.id('searchAutoCompleteResult-bob.test')).tap()
+    await expect(element(by.id('profileView'))).toBeVisible()
+  })
+})
diff --git a/__e2e__/tests/shell.test.ts b/__e2e__/tests/shell.test.ts
new file mode 100644
index 000000000..5cfd4277f
--- /dev/null
+++ b/__e2e__/tests/shell.test.ts
@@ -0,0 +1,34 @@
+/* eslint-env detox/detox */
+
+import {openApp, login, createServer} from '../util'
+
+describe('Shell', () => {
+  let service: string
+  beforeAll(async () => {
+    service = await createServer('?users')
+    await openApp({permissions: {notifications: 'YES'}})
+  })
+
+  it('Login', async () => {
+    await login(service, 'alice', 'hunter2')
+    await element(by.id('homeScreenFeedTabs-Following')).tap()
+  })
+
+  it('Can swipe the shelf open', async () => {
+    await element(by.id('homeScreen')).swipe('right', 'fast', 0.75)
+    await expect(element(by.id('drawer'))).toBeVisible()
+    await element(by.id('drawer')).swipe('left', 'fast', 0.75)
+    await expect(element(by.id('drawer'))).not.toBeVisible()
+  })
+
+  it('Can open the shelf by pressing the header avi', async () => {
+    await element(by.id('viewHeaderDrawerBtn')).tap()
+    await expect(element(by.id('drawer'))).toBeVisible()
+  })
+
+  it('Can navigate using the shelf', async () => {
+    await element(by.id('menuItemButton-Notifications')).tap()
+    await expect(element(by.id('drawer'))).not.toBeVisible()
+    await expect(element(by.id('notificationsScreen'))).toBeVisible()
+  })
+})
diff --git a/__e2e__/tests/thread-screen.test.ts b/__e2e__/tests/thread-screen.test.ts
new file mode 100644
index 000000000..f84c339ce
--- /dev/null
+++ b/__e2e__/tests/thread-screen.test.ts
@@ -0,0 +1,123 @@
+/* eslint-env detox/detox */
+
+import {openApp, login, createServer} from '../util'
+
+describe('Thread screen', () => {
+  let service: string
+  beforeAll(async () => {
+    service = await createServer('?users&follows&thread')
+    await openApp({permissions: {notifications: 'YES'}})
+  })
+
+  it('Login & navigate to thread', async () => {
+    await login(service, 'alice', 'hunter2')
+    await element(by.id('homeScreenFeedTabs-Following')).tap()
+    await element(by.id('feedItem-by-bob.test')).atIndex(0).tap()
+    await expect(
+      element(
+        by
+          .id('postThreadItem-by-bob.test')
+          .withDescendant(by.text('Thread root')),
+      ),
+    ).toBeVisible()
+    await expect(
+      element(
+        by
+          .id('postThreadItem-by-carla.test')
+          .withDescendant(by.text('Thread reply')),
+      ),
+    ).toBeVisible()
+  })
+
+  it('Can like the root post', async () => {
+    const post = by.id('postThreadItem-by-bob.test')
+    await expect(
+      element(by.id('likeCount').withAncestor(post)).atIndex(0),
+    ).not.toExist()
+    await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap()
+    await expect(
+      element(by.id('likeCount').withAncestor(post)).atIndex(0),
+    ).toHaveText('1 like')
+    await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap()
+    await expect(
+      element(by.id('likeCount').withAncestor(post)).atIndex(0),
+    ).not.toExist()
+  })
+
+  it('Can like a reply post', async () => {
+    const post = by.id('postThreadItem-by-carla.test')
+    await expect(
+      element(by.id('likeCount').withAncestor(post)).atIndex(0),
+    ).toHaveText('0')
+    await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap()
+    await expect(
+      element(by.id('likeCount').withAncestor(post)).atIndex(0),
+    ).toHaveText('1')
+    await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap()
+    await expect(
+      element(by.id('likeCount').withAncestor(post)).atIndex(0),
+    ).toHaveText('0')
+  })
+
+  it('Can repost the root post', async () => {
+    const post = by.id('postThreadItem-by-bob.test')
+    await expect(
+      element(by.id('repostCount').withAncestor(post)).atIndex(0),
+    ).not.toExist()
+    await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap()
+    await expect(element(by.id('repostModal'))).toBeVisible()
+    await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
+    await expect(element(by.id('repostModal'))).not.toBeVisible()
+    await expect(
+      element(by.id('repostCount').withAncestor(post)).atIndex(0),
+    ).toHaveText('1 repost')
+    await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap()
+    await expect(element(by.id('repostModal'))).toBeVisible()
+    await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
+    await expect(element(by.id('repostModal'))).not.toBeVisible()
+    await expect(
+      element(by.id('repostCount').withAncestor(post)).atIndex(0),
+    ).not.toExist()
+  })
+
+  it('Can repost a reply post', async () => {
+    const post = by.id('postThreadItem-by-carla.test')
+    await expect(
+      element(by.id('repostCount').withAncestor(post)).atIndex(0),
+    ).toHaveText('0')
+    await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap()
+    await expect(element(by.id('repostModal'))).toBeVisible()
+    await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
+    await expect(element(by.id('repostModal'))).not.toBeVisible()
+    await expect(
+      element(by.id('repostCount').withAncestor(post)).atIndex(0),
+    ).toHaveText('1')
+    await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap()
+    await expect(element(by.id('repostModal'))).toBeVisible()
+    await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
+    await expect(element(by.id('repostModal'))).not.toBeVisible()
+    await expect(
+      element(by.id('repostCount').withAncestor(post)).atIndex(0),
+    ).toHaveText('0')
+  })
+
+  it('Can report the root post', async () => {
+    const post = by.id('postThreadItem-by-bob.test')
+    await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap()
+    await element(by.id('postDropdownReportBtn')).tap()
+    await expect(element(by.id('reportPostModal'))).toBeVisible()
+    await element(by.id('reportPostRadios-spam')).tap()
+    await element(by.id('sendReportBtn')).tap()
+    await expect(element(by.id('reportPostModal'))).not.toBeVisible()
+  })
+
+  it('Can report a reply post', async () => {
+    const post = by.id('postThreadItem-by-carla.test')
+    await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap()
+    await element(by.id('postDropdownReportBtn')).tap()
+    await expect(element(by.id('reportPostModal'))).toBeVisible()
+    await element(by.id('reportPostRadios-spam')).tap()
+    await element(by.id('sendReportBtn')).tap()
+    await expect(element(by.id('reportPostModal'))).not.toBeVisible()
+  })
+})
diff --git a/__e2e__/util.ts b/__e2e__/util.ts
new file mode 100644
index 000000000..78d9f9f5d
--- /dev/null
+++ b/__e2e__/util.ts
@@ -0,0 +1,96 @@
+import {resolveConfig} from 'detox/internals'
+
+const platform = device.getPlatform()
+
+export async function openApp(opts: any) {
+  opts = opts || {}
+  const config = await resolveConfig()
+  if (config.configurationName.split('.').includes('debug')) {
+    return await openAppForDebugBuild(platform, opts)
+  } else {
+    return await device.launchApp({
+      ...opts,
+      newInstance: true,
+    })
+  }
+}
+
+export async function isVisible(id: string) {
+  try {
+    await expect(element(by.id(id))).toBeVisible()
+    return true
+  } catch (e) {
+    return false
+  }
+}
+
+export async function login(
+  service: string,
+  username: string,
+  password: string,
+  {takeScreenshots} = {takeScreenshots: false},
+) {
+  await element(by.id('signInButton')).tap()
+  if (takeScreenshots) {
+    await device.takeScreenshot('1- opened sign-in screen')
+  }
+  if (await isVisible('chooseAccountForm')) {
+    await element(by.id('chooseNewAccountBtn')).tap()
+  }
+  await element(by.id('loginSelectServiceButton')).tap()
+  if (takeScreenshots) {
+    await device.takeScreenshot('2- opened service selector')
+  }
+  await element(by.id('customServerTextInput')).typeText(service)
+  await element(by.id('customServerSelectBtn')).tap()
+  if (takeScreenshots) {
+    await device.takeScreenshot('3- input custom service')
+  }
+  await element(by.id('loginUsernameInput')).typeText(username)
+  await element(by.id('loginPasswordInput')).typeText(password)
+  if (takeScreenshots) {
+    await device.takeScreenshot('4- entered username and password')
+  }
+  await element(by.id('loginNextButton')).tap()
+}
+
+async function openAppForDebugBuild(platform: string, opts: any) {
+  const deepLinkUrl = // Local testing with packager
+    /*process.env.EXPO_USE_UPDATES
+    ? // Testing latest published EAS update for the test_debug channel
+      getDeepLinkUrl(getLatestUpdateUrl())
+    : */ getDeepLinkUrl(getDevLauncherPackagerUrl(platform))
+
+  if (platform === 'ios') {
+    await device.launchApp({
+      ...opts,
+      newInstance: true,
+    })
+    sleep(3000)
+    await device.openURL({
+      url: deepLinkUrl,
+    })
+  } else {
+    await device.launchApp({
+      ...opts,
+      newInstance: true,
+      url: deepLinkUrl,
+    })
+  }
+
+  await sleep(3000)
+}
+
+export async function createServer(path = '') {
+  const res = await fetch(`http://localhost:1986/${path}`, {method: 'POST'})
+  const resBody = await res.text()
+  return resBody
+}
+
+const getDeepLinkUrl = (url: string) =>
+  `expo+bluesky://expo-development-client/?url=${encodeURIComponent(url)}`
+
+const getDevLauncherPackagerUrl = (platform: string) =>
+  `http://localhost:8081/index.bundle?platform=${platform}&dev=true&minify=false&disableOnboarding=1`
+
+export const sleep = (t: number) => new Promise(res => setTimeout(res, t))
diff --git a/__tests__/lib/link-meta.test.ts b/__tests__/lib/link-meta.test.ts
index ce7da4152..8af14628e 100644
--- a/__tests__/lib/link-meta.test.ts
+++ b/__tests__/lib/link-meta.test.ts
@@ -4,14 +4,14 @@ import {
   getLikelyType,
 } from '../../src/lib/link-meta/link-meta'
 import {exampleComHtml} from './__mocks__/exampleComHtml'
-import AtpAgent from '@atproto/api'
+import {BskyAgent} from '@atproto/api'
 import {DEFAULT_SERVICE, RootStoreModel} from '../../src/state'
 
 describe('getLinkMeta', () => {
   let rootStore: RootStoreModel
 
   beforeEach(() => {
-    rootStore = new RootStoreModel(new AtpAgent({service: DEFAULT_SERVICE}))
+    rootStore = new RootStoreModel(new BskyAgent({service: DEFAULT_SERVICE}))
   })
 
   const inputs = [
diff --git a/__tests__/lib/string.test.ts b/__tests__/lib/string.test.ts
index 4f6fd62d6..f25bd02a7 100644
--- a/__tests__/lib/string.test.ts
+++ b/__tests__/lib/string.test.ts
@@ -7,172 +7,10 @@ import {
 } from '../../src/lib/strings/url-helpers'
 import {pluralize, enforceLen} from '../../src/lib/strings/helpers'
 import {ago} from '../../src/lib/strings/time'
-import {
-  extractEntities,
-  detectLinkables,
-} from '../../src/lib/strings/rich-text-detection'
+import {detectLinkables} from '../../src/lib/strings/rich-text-detection'
 import {makeValidHandle, createFullHandle} from '../../src/lib/strings/handles'
 import {cleanError} from '../../src/lib/strings/errors'
 
-describe('extractEntities', () => {
-  const knownHandles = new Set(['handle.com', 'full123.test-of-chars'])
-  const inputs = [
-    'no mention',
-    '@handle.com middle end',
-    'start @handle.com end',
-    'start middle @handle.com',
-    '@handle.com @handle.com @handle.com',
-    '@full123.test-of-chars',
-    'not@right',
-    '@handle.com!@#$chars',
-    '@handle.com\n@handle.com',
-    'parenthetical (@handle.com)',
-    'start https://middle.com end',
-    'start https://middle.com/foo/bar end',
-    'start https://middle.com/foo/bar?baz=bux end',
-    'start https://middle.com/foo/bar?baz=bux#hash end',
-    'https://start.com/foo/bar?baz=bux#hash middle end',
-    'start middle https://end.com/foo/bar?baz=bux#hash',
-    'https://newline1.com\nhttps://newline2.com',
-    'start middle.com end',
-    'start middle.com/foo/bar end',
-    'start middle.com/foo/bar?baz=bux end',
-    'start middle.com/foo/bar?baz=bux#hash end',
-    'start.com/foo/bar?baz=bux#hash middle end',
-    'start middle end.com/foo/bar?baz=bux#hash',
-    'newline1.com\nnewline2.com',
-    'not.. a..url ..here',
-    'e.g.',
-    'something-cool.jpg',
-    'website.com.jpg',
-    'e.g./foo',
-    'website.com.jpg/foo',
-    'Classic article https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/',
-    'Classic article https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/ ',
-    'https://foo.com https://bar.com/whatever https://baz.com',
-    'punctuation https://foo.com, https://bar.com/whatever; https://baz.com.',
-    'parenthentical (https://foo.com)',
-    'except for https://foo.com/thing_(cool)',
-  ]
-  interface Output {
-    type: string
-    value: string
-    noScheme?: boolean
-  }
-  const outputs: Output[][] = [
-    [],
-    [{type: 'mention', value: 'handle.com'}],
-    [{type: 'mention', value: 'handle.com'}],
-    [{type: 'mention', value: 'handle.com'}],
-    [
-      {type: 'mention', value: 'handle.com'},
-      {type: 'mention', value: 'handle.com'},
-      {type: 'mention', value: 'handle.com'},
-    ],
-    [
-      {
-        type: 'mention',
-        value: 'full123.test-of-chars',
-      },
-    ],
-    [],
-    [{type: 'mention', value: 'handle.com'}],
-    [
-      {type: 'mention', value: 'handle.com'},
-      {type: 'mention', value: 'handle.com'},
-    ],
-    [{type: 'mention', value: 'handle.com'}],
-    [{type: 'link', value: 'https://middle.com'}],
-    [{type: 'link', value: 'https://middle.com/foo/bar'}],
-    [{type: 'link', value: 'https://middle.com/foo/bar?baz=bux'}],
-    [{type: 'link', value: 'https://middle.com/foo/bar?baz=bux#hash'}],
-    [{type: 'link', value: 'https://start.com/foo/bar?baz=bux#hash'}],
-    [{type: 'link', value: 'https://end.com/foo/bar?baz=bux#hash'}],
-    [
-      {type: 'link', value: 'https://newline1.com'},
-      {type: 'link', value: 'https://newline2.com'},
-    ],
-    [{type: 'link', value: 'middle.com', noScheme: true}],
-    [{type: 'link', value: 'middle.com/foo/bar', noScheme: true}],
-    [{type: 'link', value: 'middle.com/foo/bar?baz=bux', noScheme: true}],
-    [{type: 'link', value: 'middle.com/foo/bar?baz=bux#hash', noScheme: true}],
-    [{type: 'link', value: 'start.com/foo/bar?baz=bux#hash', noScheme: true}],
-    [{type: 'link', value: 'end.com/foo/bar?baz=bux#hash', noScheme: true}],
-    [
-      {type: 'link', value: 'newline1.com', noScheme: true},
-      {type: 'link', value: 'newline2.com', noScheme: true},
-    ],
-    [],
-    [],
-    [],
-    [],
-    [],
-    [],
-    [
-      {
-        type: 'link',
-        value:
-          'https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/',
-      },
-    ],
-    [
-      {
-        type: 'link',
-        value:
-          'https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/',
-      },
-    ],
-    [
-      {type: 'link', value: 'https://foo.com'},
-      {type: 'link', value: 'https://bar.com/whatever'},
-      {type: 'link', value: 'https://baz.com'},
-    ],
-    [
-      {type: 'link', value: 'https://foo.com'},
-      {type: 'link', value: 'https://bar.com/whatever'},
-      {type: 'link', value: 'https://baz.com'},
-    ],
-    [{type: 'link', value: 'https://foo.com'}],
-    [{type: 'link', value: 'https://foo.com/thing_(cool)'}],
-  ]
-  it('correctly handles a set of text inputs', () => {
-    for (let i = 0; i < inputs.length; i++) {
-      const input = inputs[i]
-      const result = extractEntities(input, knownHandles)
-      if (!outputs[i].length) {
-        expect(result).toBeFalsy()
-      } else if (outputs[i].length && !result) {
-        expect(result).toBeTruthy()
-      } else if (result) {
-        expect(result.length).toBe(outputs[i].length)
-        for (let j = 0; j < outputs[i].length; j++) {
-          expect(result[j].type).toEqual(outputs[i][j].type)
-          if (outputs[i][j].noScheme) {
-            expect(result[j].value).toEqual(`https://${outputs[i][j].value}`)
-          } else {
-            expect(result[j].value).toEqual(outputs[i][j].value)
-          }
-          if (outputs[i]?.[j].type === 'mention') {
-            expect(
-              input.slice(result[j].index.start, result[j].index.end),
-            ).toBe(`@${result[j].value}`)
-          } else {
-            if (!outputs[i]?.[j].noScheme) {
-              expect(
-                input.slice(result[j].index.start, result[j].index.end),
-              ).toBe(result[j].value)
-            } else {
-              expect(
-                input.slice(result[j].index.start, result[j].index.end),
-              ).toBe(result[j].value.slice('https://'.length))
-            }
-          }
-        }
-      }
-    }
-  })
-})
-
 describe('detectLinkables', () => {
   const inputs = [
     'no linkable',
diff --git a/__tests__/lib/strings/rich-text-sanitize.ts b/__tests__/lib/strings/rich-text-sanitize.ts
deleted file mode 100644
index d0bbae5e8..000000000
--- a/__tests__/lib/strings/rich-text-sanitize.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-import {AppBskyFeedPost} from '@atproto/api'
-type Entity = AppBskyFeedPost.Entity
-import {RichText} from '../../../src/lib/strings/rich-text'
-import {removeExcessNewlines} from '../../../src/lib/strings/rich-text-sanitize'
-
-describe('removeExcessNewlines', () => {
-  it('removes more than two consecutive new lines', () => {
-    const input = new RichText(
-      'test\n\n\n\n\ntest\n\n\n\n\n\n\ntest\n\n\n\n\n\n\ntest\n\n\n\n\n\n\ntest',
-    )
-    const output = removeExcessNewlines(input)
-    expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest')
-  })
-
-  it('removes more than two consecutive new lines with spaces', () => {
-    const input = new RichText(
-      'test\n\n\n\n\ntest\n \n \n \n \n\n\ntest\n\n\n\n\n\n\ntest\n\n\n\n\n  \n\ntest',
-    )
-    const output = removeExcessNewlines(input)
-    expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest')
-  })
-
-  it('returns original string if there are no consecutive new lines', () => {
-    const input = new RichText('test\n\ntest\n\ntest\n\ntest\n\ntest')
-    const output = removeExcessNewlines(input)
-    expect(output.text).toEqual(input.text)
-  })
-
-  it('returns original string if there are no new lines', () => {
-    const input = new RichText('test test          test test test')
-    const output = removeExcessNewlines(input)
-    expect(output.text).toEqual(input.text)
-  })
-
-  it('returns empty string if input is empty', () => {
-    const input = new RichText('')
-    const output = removeExcessNewlines(input)
-    expect(output.text).toEqual('')
-  })
-
-  it('works with different types of new line characters', () => {
-    const input = new RichText(
-      'test\r\ntest\n\rtest\rtest\n\n\n\ntest\n\r \n \n \n \n\n\ntest',
-    )
-    const output = removeExcessNewlines(input)
-    expect(output.text).toEqual('test\r\ntest\n\rtest\rtest\n\ntest\n\ntest')
-  })
-
-  it('removes more than two consecutive new lines with zero width space', () => {
-    const input = new RichText(
-      'test\n\n\n\n\ntest\n\u200B\u200B\n\n\n\ntest\n \u200B\u200B \n\n\n\ntest\n\n\n\n\n\n\ntest',
-    )
-    const output = removeExcessNewlines(input)
-    expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest')
-  })
-
-  it('removes more than two consecutive new lines with zero width non-joiner', () => {
-    const input = new RichText(
-      'test\n\n\n\n\ntest\n\u200C\u200C\n\n\n\ntest\n \u200C\u200C \n\n\n\ntest\n\n\n\n\n\n\ntest',
-    )
-    const output = removeExcessNewlines(input)
-    expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest')
-  })
-
-  it('removes more than two consecutive new lines with zero width joiner', () => {
-    const input = new RichText(
-      'test\n\n\n\n\ntest\n\u200D\u200D\n\n\n\ntest\n \u200D\u200D \n\n\n\ntest\n\n\n\n\n\n\ntest',
-    )
-    const output = removeExcessNewlines(input)
-    expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest')
-  })
-
-  it('removes more than two consecutive new lines with soft hyphen', () => {
-    const input = new RichText(
-      'test\n\n\n\n\ntest\n\u00AD\u00AD\n\n\n\ntest\n \u00AD\u00AD \n\n\n\ntest\n\n\n\n\n\n\ntest',
-    )
-    const output = removeExcessNewlines(input)
-    expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest')
-  })
-
-  it('removes more than two consecutive new lines with word joiner', () => {
-    const input = new RichText(
-      'test\n\n\n\n\ntest\n\u2060\u2060\n\n\n\ntest\n \u2060\u2060 \n\n\n\ntest\n\n\n\n\n\n\ntest',
-    )
-    const output = removeExcessNewlines(input)
-    expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest')
-  })
-})
-
-describe('removeExcessNewlines w/entities', () => {
-  it('preserves entities as expected', () => {
-    const input = new RichText(
-      'test\n\n\n\n\ntest\n\n\n\n\n\n\ntest\n\n\n\n\n\n\ntest\n\n\n\n\n\n\ntest',
-      [
-        {index: {start: 0, end: 13}, type: '', value: ''},
-        {index: {start: 13, end: 24}, type: '', value: ''},
-        {index: {start: 9, end: 15}, type: '', value: ''},
-        {index: {start: 4, end: 9}, type: '', value: ''},
-      ],
-    )
-    const output = removeExcessNewlines(input)
-    expect(entToStr(input.text, input.entities?.[0])).toEqual(
-      'test\n\n\n\n\ntest',
-    )
-    expect(entToStr(input.text, input.entities?.[1])).toEqual(
-      '\n\n\n\n\n\n\ntest',
-    )
-    expect(entToStr(input.text, input.entities?.[2])).toEqual('test\n\n')
-    expect(entToStr(input.text, input.entities?.[3])).toEqual('\n\n\n\n\n')
-    expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest')
-    expect(entToStr(output.text, output.entities?.[0])).toEqual('test\n\ntest')
-    expect(entToStr(output.text, output.entities?.[1])).toEqual('test')
-    expect(entToStr(output.text, output.entities?.[2])).toEqual('test')
-    expect(output.entities?.[3]).toEqual(undefined)
-  })
-})
-
-function entToStr(str: string, ent?: Entity) {
-  if (!ent) {
-    return ''
-  }
-  return str.slice(ent.index.start, ent.index.end)
-}
diff --git a/__tests__/lib/strings/rich-text.ts b/__tests__/lib/strings/rich-text.ts
deleted file mode 100644
index e52ac6cec..000000000
--- a/__tests__/lib/strings/rich-text.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-import {RichText} from '../../../src/lib/strings/rich-text'
-
-describe('richText.insert', () => {
-  const input = new RichText('hello world', [
-    {index: {start: 2, end: 7}, type: '', value: ''},
-  ])
-
-  it('correctly adjusts entities (scenario A - before)', () => {
-    const output = input.clone().insert(0, 'test')
-    expect(output.text).toEqual('testhello world')
-    expect(output.entities?.[0].index.start).toEqual(6)
-    expect(output.entities?.[0].index.end).toEqual(11)
-    expect(
-      output.text.slice(
-        output.entities?.[0].index.start,
-        output.entities?.[0].index.end,
-      ),
-    ).toEqual('llo w')
-  })
-
-  it('correctly adjusts entities (scenario B - inner)', () => {
-    const output = input.clone().insert(4, 'test')
-    expect(output.text).toEqual('helltesto world')
-    expect(output.entities?.[0].index.start).toEqual(2)
-    expect(output.entities?.[0].index.end).toEqual(11)
-    expect(
-      output.text.slice(
-        output.entities?.[0].index.start,
-        output.entities?.[0].index.end,
-      ),
-    ).toEqual('lltesto w')
-  })
-
-  it('correctly adjusts entities (scenario C - after)', () => {
-    const output = input.clone().insert(8, 'test')
-    expect(output.text).toEqual('hello wotestrld')
-    expect(output.entities?.[0].index.start).toEqual(2)
-    expect(output.entities?.[0].index.end).toEqual(7)
-    expect(
-      output.text.slice(
-        output.entities?.[0].index.start,
-        output.entities?.[0].index.end,
-      ),
-    ).toEqual('llo w')
-  })
-})
-
-describe('richText.delete', () => {
-  const input = new RichText('hello world', [
-    {index: {start: 2, end: 7}, type: '', value: ''},
-  ])
-
-  it('correctly adjusts entities (scenario A - entirely outer)', () => {
-    const output = input.clone().delete(0, 9)
-    expect(output.text).toEqual('ld')
-    expect(output.entities?.length).toEqual(0)
-  })
-
-  it('correctly adjusts entities (scenario B - entirely after)', () => {
-    const output = input.clone().delete(7, 11)
-    expect(output.text).toEqual('hello w')
-    expect(output.entities?.[0].index.start).toEqual(2)
-    expect(output.entities?.[0].index.end).toEqual(7)
-    expect(
-      output.text.slice(
-        output.entities?.[0].index.start,
-        output.entities?.[0].index.end,
-      ),
-    ).toEqual('llo w')
-  })
-
-  it('correctly adjusts entities (scenario C - partially after)', () => {
-    const output = input.clone().delete(4, 11)
-    expect(output.text).toEqual('hell')
-    expect(output.entities?.[0].index.start).toEqual(2)
-    expect(output.entities?.[0].index.end).toEqual(4)
-    expect(
-      output.text.slice(
-        output.entities?.[0].index.start,
-        output.entities?.[0].index.end,
-      ),
-    ).toEqual('ll')
-  })
-
-  it('correctly adjusts entities (scenario D - entirely inner)', () => {
-    const output = input.clone().delete(3, 5)
-    expect(output.text).toEqual('hel world')
-    expect(output.entities?.[0].index.start).toEqual(2)
-    expect(output.entities?.[0].index.end).toEqual(5)
-    expect(
-      output.text.slice(
-        output.entities?.[0].index.start,
-        output.entities?.[0].index.end,
-      ),
-    ).toEqual('l w')
-  })
-
-  it('correctly adjusts entities (scenario E - partially before)', () => {
-    const output = input.clone().delete(1, 5)
-    expect(output.text).toEqual('h world')
-    expect(output.entities?.[0].index.start).toEqual(1)
-    expect(output.entities?.[0].index.end).toEqual(3)
-    expect(
-      output.text.slice(
-        output.entities?.[0].index.start,
-        output.entities?.[0].index.end,
-      ),
-    ).toEqual(' w')
-  })
-
-  it('correctly adjusts entities (scenario F - entirely before)', () => {
-    const output = input.clone().delete(0, 2)
-    expect(output.text).toEqual('llo world')
-    expect(output.entities?.[0].index.start).toEqual(0)
-    expect(output.entities?.[0].index.end).toEqual(5)
-    expect(
-      output.text.slice(
-        output.entities?.[0].index.start,
-        output.entities?.[0].index.end,
-      ),
-    ).toEqual('llo w')
-  })
-})
diff --git a/app.json b/app.json
index a4749144e..0e5bb2e03 100644
--- a/app.json
+++ b/app.json
@@ -2,7 +2,7 @@
   "expo": {
     "name": "bluesky",
     "slug": "bluesky",
-    "version": "1.10.0",
+    "version": "1.11.0",
     "orientation": "portrait",
     "icon": "./assets/icon.png",
     "userInterfaceStyle": "light",
diff --git a/e2e/tests/happyPath.test.js b/e2e/tests/happyPath.test.js
deleted file mode 100644
index 4176cecb9..000000000
--- a/e2e/tests/happyPath.test.js
+++ /dev/null
@@ -1,70 +0,0 @@
-/* eslint-env detox/detox */
-
-describe('Happy paths', () => {
-  async function grantAccessToUserWithValidCredentials(
-    username,
-    {takeScreenshots} = {takeScreenshots: false},
-  ) {
-    await element(by.id('signInButton')).tap()
-    if (takeScreenshots) {
-      await device.takeScreenshot('1- opened sign-in screen')
-    }
-    await element(by.id('loginSelectServiceButton')).tap()
-    if (takeScreenshots) {
-      await device.takeScreenshot('2- opened service selector')
-    }
-    await element(by.id('localDevServerButton')).tap()
-    if (takeScreenshots) {
-      await device.takeScreenshot('3- selected local dev server')
-    }
-    await element(by.id('loginUsernameInput')).typeText(username)
-    await element(by.id('loginPasswordInput')).typeText('hunter2')
-    if (takeScreenshots) {
-      await device.takeScreenshot('4- entered username and password')
-    }
-    await element(by.id('loginNextButton')).tap()
-  }
-
-  beforeEach(async () => {
-    await device.uninstallApp()
-    await device.installApp()
-    await device.launchApp({permissions: {notifications: 'YES'}})
-  })
-
-  it('As Alice, I can login', async () => {
-    await expect(element(by.id('signInButton'))).toBeVisible()
-    await grantAccessToUserWithValidCredentials('alice', {
-      takeScreenshots: true,
-    })
-    await device.takeScreenshot('5- opened home screen')
-  })
-
-  it('As Alice, I can login, and post a text', async () => {
-    await grantAccessToUserWithValidCredentials('alice')
-    await element(by.id('composeFAB')).tap()
-    await device.takeScreenshot('1- opened composer')
-    await element(by.id('composerTextInput')).typeText(
-      'Greetings earthlings, I come in peace... and to run some tests.',
-    )
-    await device.takeScreenshot('2- entered text')
-    await element(by.id('composerPublishButton')).tap()
-    await device.takeScreenshot('3- opened general section')
-    await expect(element(by.id('composeFAB'))).toBeVisible()
-  })
-
-  it('I can create a new account', async () => {
-    await element(by.id('createAccountButton')).tap()
-    await device.takeScreenshot('1- opened create account screen')
-    await element(by.id('registerSelectServiceButton')).tap()
-    await device.takeScreenshot('2- opened service selector')
-    await element(by.id('localDevServerButton')).tap()
-    await device.takeScreenshot('3- selected local dev server')
-    await element(by.id('registerEmailInput')).typeText('example@test.com')
-    await element(by.id('registerPasswordInput')).typeText('hunter2')
-    await element(by.id('registerHandleInput')).typeText('e2e-test')
-    await element(by.id('registerIs13Input')).tap()
-    await device.takeScreenshot('4- entered account details')
-    await element(by.id('createAccountButton')).tap()
-    await expect(element(by.id('welcomeBanner'))).toBeVisible()
-  })
-})
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 761ec7373..8f9e5f903 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -22,7 +22,7 @@ PODS:
   - EXMediaLibrary (15.2.3):
     - ExpoModulesCore
     - React-Core
-  - Expo (48.0.7):
+  - Expo (48.0.9):
     - ExpoModulesCore
   - expo-dev-client (2.1.5):
     - EXManifests
@@ -102,7 +102,7 @@ PODS:
     - ExpoModulesCore
   - ExpoLocalization (14.1.1):
     - ExpoModulesCore
-  - ExpoModulesCore (1.2.5):
+  - ExpoModulesCore (1.2.6):
     - React-Core
     - React-RCTAppDelegate
     - ReactCommon/turbomodule/core
@@ -110,19 +110,19 @@ PODS:
     - ExpoModulesCore
     - React-Core
   - EXUpdatesInterface (0.9.1)
-  - FBLazyVector (0.71.3)
-  - FBReactNativeSpec (0.71.3):
+  - FBLazyVector (0.71.4)
+  - FBReactNativeSpec (0.71.4):
     - RCT-Folly (= 2021.07.22.00)
-    - RCTRequired (= 0.71.3)
-    - RCTTypeSafety (= 0.71.3)
-    - React-Core (= 0.71.3)
-    - React-jsi (= 0.71.3)
-    - ReactCommon/turbomodule/core (= 0.71.3)
+    - RCTRequired (= 0.71.4)
+    - RCTTypeSafety (= 0.71.4)
+    - React-Core (= 0.71.4)
+    - React-jsi (= 0.71.4)
+    - ReactCommon/turbomodule/core (= 0.71.4)
   - fmt (6.2.1)
   - glog (0.3.5)
-  - hermes-engine (0.71.3):
-    - hermes-engine/Pre-built (= 0.71.3)
-  - hermes-engine/Pre-built (0.71.3)
+  - hermes-engine (0.71.4):
+    - hermes-engine/Pre-built (= 0.71.4)
+  - hermes-engine/Pre-built (0.71.4)
   - libevent (2.1.12)
   - libwebp (1.2.4):
     - libwebp/demux (= 1.2.4)
@@ -150,26 +150,26 @@ PODS:
     - fmt (~> 6.2.1)
     - glog
     - libevent
-  - RCTRequired (0.71.3)
-  - RCTTypeSafety (0.71.3):
-    - FBLazyVector (= 0.71.3)
-    - RCTRequired (= 0.71.3)
-    - React-Core (= 0.71.3)
-  - React (0.71.3):
-    - React-Core (= 0.71.3)
-    - React-Core/DevSupport (= 0.71.3)
-    - React-Core/RCTWebSocket (= 0.71.3)
-    - React-RCTActionSheet (= 0.71.3)
-    - React-RCTAnimation (= 0.71.3)
-    - React-RCTBlob (= 0.71.3)
-    - React-RCTImage (= 0.71.3)
-    - React-RCTLinking (= 0.71.3)
-    - React-RCTNetwork (= 0.71.3)
-    - React-RCTSettings (= 0.71.3)
-    - React-RCTText (= 0.71.3)
-    - React-RCTVibration (= 0.71.3)
-  - React-callinvoker (0.71.3)
-  - React-Codegen (0.71.3):
+  - RCTRequired (0.71.4)
+  - RCTTypeSafety (0.71.4):
+    - FBLazyVector (= 0.71.4)
+    - RCTRequired (= 0.71.4)
+    - React-Core (= 0.71.4)
+  - React (0.71.4):
+    - React-Core (= 0.71.4)
+    - React-Core/DevSupport (= 0.71.4)
+    - React-Core/RCTWebSocket (= 0.71.4)
+    - React-RCTActionSheet (= 0.71.4)
+    - React-RCTAnimation (= 0.71.4)
+    - React-RCTBlob (= 0.71.4)
+    - React-RCTImage (= 0.71.4)
+    - React-RCTLinking (= 0.71.4)
+    - React-RCTNetwork (= 0.71.4)
+    - React-RCTSettings (= 0.71.4)
+    - React-RCTText (= 0.71.4)
+    - React-RCTVibration (= 0.71.4)
+  - React-callinvoker (0.71.4)
+  - React-Codegen (0.71.4):
     - FBReactNativeSpec
     - hermes-engine
     - RCT-Folly
@@ -180,209 +180,209 @@ PODS:
     - React-jsiexecutor
     - ReactCommon/turbomodule/bridging
     - ReactCommon/turbomodule/core
-  - React-Core (0.71.3):
+  - React-Core (0.71.4):
     - glog
     - hermes-engine
     - RCT-Folly (= 2021.07.22.00)
-    - React-Core/Default (= 0.71.3)
-    - React-cxxreact (= 0.71.3)
+    - React-Core/Default (= 0.71.4)
+    - React-cxxreact (= 0.71.4)
     - React-hermes
-    - React-jsi (= 0.71.3)
-    - React-jsiexecutor (= 0.71.3)
-    - React-perflogger (= 0.71.3)
+    - React-jsi (= 0.71.4)
+    - React-jsiexecutor (= 0.71.4)
+    - React-perflogger (= 0.71.4)
     - Yoga
-  - React-Core/CoreModulesHeaders (0.71.3):
+  - React-Core/CoreModulesHeaders (0.71.4):
     - glog
     - hermes-engine
     - RCT-Folly (= 2021.07.22.00)
     - React-Core/Default
-    - React-cxxreact (= 0.71.3)
+    - React-cxxreact (= 0.71.4)
     - React-hermes
-    - React-jsi (= 0.71.3)
-    - React-jsiexecutor (= 0.71.3)
-    - React-perflogger (= 0.71.3)
+    - React-jsi (= 0.71.4)
+    - React-jsiexecutor (= 0.71.4)
+    - React-perflogger (= 0.71.4)
     - Yoga
-  - React-Core/Default (0.71.3):
+  - React-Core/Default (0.71.4):
     - glog
     - hermes-engine
     - RCT-Folly (= 2021.07.22.00)
-    - React-cxxreact (= 0.71.3)
+    - React-cxxreact (= 0.71.4)
     - React-hermes
-    - React-jsi (= 0.71.3)
-    - React-jsiexecutor (= 0.71.3)
-    - React-perflogger (= 0.71.3)
+    - React-jsi (= 0.71.4)
+    - React-jsiexecutor (= 0.71.4)
+    - React-perflogger (= 0.71.4)
     - Yoga
-  - React-Core/DevSupport (0.71.3):
+  - React-Core/DevSupport (0.71.4):
     - glog
     - hermes-engine
     - RCT-Folly (= 2021.07.22.00)
-    - React-Core/Default (= 0.71.3)
-    - React-Core/RCTWebSocket (= 0.71.3)
-    - React-cxxreact (= 0.71.3)
+    - React-Core/Default (= 0.71.4)
+    - React-Core/RCTWebSocket (= 0.71.4)
+    - React-cxxreact (= 0.71.4)
     - React-hermes
-    - React-jsi (= 0.71.3)
-    - React-jsiexecutor (= 0.71.3)
-    - React-jsinspector (= 0.71.3)
-    - React-perflogger (= 0.71.3)
+    - React-jsi (= 0.71.4)
+    - React-jsiexecutor (= 0.71.4)
+    - React-jsinspector (= 0.71.4)
+    - React-perflogger (= 0.71.4)
     - Yoga
-  - React-Core/RCTActionSheetHeaders (0.71.3):
+  - React-Core/RCTActionSheetHeaders (0.71.4):
     - glog
     - hermes-engine
     - RCT-Folly (= 2021.07.22.00)
     - React-Core/Default
-    - React-cxxreact (= 0.71.3)
+    - React-cxxreact (= 0.71.4)
     - React-hermes
-    - React-jsi (= 0.71.3)
-    - React-jsiexecutor (= 0.71.3)
-    - React-perflogger (= 0.71.3)
+    - React-jsi (= 0.71.4)
+    - React-jsiexecutor (= 0.71.4)
+    - React-perflogger (= 0.71.4)
     - Yoga
-  - React-Core/RCTAnimationHeaders (0.71.3):
+  - React-Core/RCTAnimationHeaders (0.71.4):
     - glog
     - hermes-engine
     - RCT-Folly (= 2021.07.22.00)
     - React-Core/Default
-    - React-cxxreact (= 0.71.3)
+    - React-cxxreact (= 0.71.4)
     - React-hermes
-    - React-jsi (= 0.71.3)
-    - React-jsiexecutor (= 0.71.3)
-    - React-perflogger (= 0.71.3)
+    - React-jsi (= 0.71.4)
+    - React-jsiexecutor (= 0.71.4)
+    - React-perflogger (= 0.71.4)
     - Yoga
-  - React-Core/RCTBlobHeaders (0.71.3):
+  - React-Core/RCTBlobHeaders (0.71.4):
     - glog
     - hermes-engine
     - RCT-Folly (= 2021.07.22.00)
     - React-Core/Default
-    - React-cxxreact (= 0.71.3)
+    - React-cxxreact (= 0.71.4)
     - React-hermes
-    - React-jsi (= 0.71.3)
-    - React-jsiexecutor (= 0.71.3)
-    - React-perflogger (= 0.71.3)
+    - React-jsi (= 0.71.4)
+    - React-jsiexecutor (= 0.71.4)
+    - React-perflogger (= 0.71.4)
     - Yoga
-  - React-Core/RCTImageHeaders (0.71.3):
+  - React-Core/RCTImageHeaders (0.71.4):
     - glog
     - hermes-engine
     - RCT-Folly (= 2021.07.22.00)
     - React-Core/Default
-    - React-cxxreact (= 0.71.3)
+    - React-cxxreact (= 0.71.4)
     - React-hermes
-    - React-jsi (= 0.71.3)
-    - React-jsiexecutor (= 0.71.3)
-    - React-perflogger (= 0.71.3)
+    - React-jsi (= 0.71.4)
+    - React-jsiexecutor (= 0.71.4)
+    - React-perflogger (= 0.71.4)
     - Yoga
-  - React-Core/RCTLinkingHeaders (0.71.3):
+  - React-Core/RCTLinkingHeaders (0.71.4):
     - glog
     - hermes-engine
     - RCT-Folly (= 2021.07.22.00)
     - React-Core/Default
-    - React-cxxreact (= 0.71.3)
+    - React-cxxreact (= 0.71.4)
     - React-hermes
-    - React-jsi (= 0.71.3)
-    - React-jsiexecutor (= 0.71.3)
-    - React-perflogger (= 0.71.3)
+    - React-jsi (= 0.71.4)
+    - React-jsiexecutor (= 0.71.4)
+    - React-perflogger (= 0.71.4)
     - Yoga
-  - React-Core/RCTNetworkHeaders (0.71.3):
+  - React-Core/RCTNetworkHeaders (0.71.4):
     - glog
     - hermes-engine
     - RCT-Folly (= 2021.07.22.00)
     - React-Core/Default
-    - React-cxxreact (= 0.71.3)
+    - React-cxxreact (= 0.71.4)
     - React-hermes
-    - React-jsi (= 0.71.3)
-    - React-jsiexecutor (= 0.71.3)
-    - React-perflogger (= 0.71.3)
+    - React-jsi (= 0.71.4)
+    - React-jsiexecutor (= 0.71.4)
+    - React-perflogger (= 0.71.4)
     - Yoga
-  - React-Core/RCTSettingsHeaders (0.71.3):
+  - React-Core/RCTSettingsHeaders (0.71.4):
     - glog
     - hermes-engine
     - RCT-Folly (= 2021.07.22.00)
     - React-Core/Default
-    - React-cxxreact (= 0.71.3)
+    - React-cxxreact (= 0.71.4)
     - React-hermes
-    - React-jsi (= 0.71.3)
-    - React-jsiexecutor (= 0.71.3)
-    - React-perflogger (= 0.71.3)
+    - React-jsi (= 0.71.4)
+    - React-jsiexecutor (= 0.71.4)
+    - React-perflogger (= 0.71.4)
     - Yoga
-  - React-Core/RCTTextHeaders (0.71.3):
+  - React-Core/RCTTextHeaders (0.71.4):
     - glog
     - hermes-engine
     - RCT-Folly (= 2021.07.22.00)
     - React-Core/Default
-    - React-cxxreact (= 0.71.3)
+    - React-cxxreact (= 0.71.4)
     - React-hermes
-    - React-jsi (= 0.71.3)
-    - React-jsiexecutor (= 0.71.3)
-    - React-perflogger (= 0.71.3)
+    - React-jsi (= 0.71.4)
+    - React-jsiexecutor (= 0.71.4)
+    - React-perflogger (= 0.71.4)
     - Yoga
-  - React-Core/RCTVibrationHeaders (0.71.3):
+  - React-Core/RCTVibrationHeaders (0.71.4):
     - glog
     - hermes-engine
     - RCT-Folly (= 2021.07.22.00)
     - React-Core/Default
-    - React-cxxreact (= 0.71.3)
+    - React-cxxreact (= 0.71.4)
     - React-hermes
-    - React-jsi (= 0.71.3)
-    - React-jsiexecutor (= 0.71.3)
-    - React-perflogger (= 0.71.3)
+    - React-jsi (= 0.71.4)
+    - React-jsiexecutor (= 0.71.4)
+    - React-perflogger (= 0.71.4)
     - Yoga
-  - React-Core/RCTWebSocket (0.71.3):
+  - React-Core/RCTWebSocket (0.71.4):
     - glog
     - hermes-engine
     - RCT-Folly (= 2021.07.22.00)
-    - React-Core/Default (= 0.71.3)
-    - React-cxxreact (= 0.71.3)
+    - React-Core/Default (= 0.71.4)
+    - React-cxxreact (= 0.71.4)
     - React-hermes
-    - React-jsi (= 0.71.3)
-    - React-jsiexecutor (= 0.71.3)
-    - React-perflogger (= 0.71.3)
+    - React-jsi (= 0.71.4)
+    - React-jsiexecutor (= 0.71.4)
+    - React-perflogger (= 0.71.4)
     - Yoga
-  - React-CoreModules (0.71.3):
+  - React-CoreModules (0.71.4):
     - RCT-Folly (= 2021.07.22.00)
-    - RCTTypeSafety (= 0.71.3)
-    - React-Codegen (= 0.71.3)
-    - React-Core/CoreModulesHeaders (= 0.71.3)
-    - React-jsi (= 0.71.3)
+    - RCTTypeSafety (= 0.71.4)
+    - React-Codegen (= 0.71.4)
+    - React-Core/CoreModulesHeaders (= 0.71.4)
+    - React-jsi (= 0.71.4)
     - React-RCTBlob
-    - React-RCTImage (= 0.71.3)
-    - ReactCommon/turbomodule/core (= 0.71.3)
-  - React-cxxreact (0.71.3):
+    - React-RCTImage (= 0.71.4)
+    - ReactCommon/turbomodule/core (= 0.71.4)
+  - React-cxxreact (0.71.4):
     - boost (= 1.76.0)
     - DoubleConversion
     - glog
     - hermes-engine
     - RCT-Folly (= 2021.07.22.00)
-    - React-callinvoker (= 0.71.3)
-    - React-jsi (= 0.71.3)
-    - React-jsinspector (= 0.71.3)
-    - React-logger (= 0.71.3)
-    - React-perflogger (= 0.71.3)
-    - React-runtimeexecutor (= 0.71.3)
-  - React-hermes (0.71.3):
+    - React-callinvoker (= 0.71.4)
+    - React-jsi (= 0.71.4)
+    - React-jsinspector (= 0.71.4)
+    - React-logger (= 0.71.4)
+    - React-perflogger (= 0.71.4)
+    - React-runtimeexecutor (= 0.71.4)
+  - React-hermes (0.71.4):
     - DoubleConversion
     - glog
     - hermes-engine
     - RCT-Folly (= 2021.07.22.00)
     - RCT-Folly/Futures (= 2021.07.22.00)
-    - React-cxxreact (= 0.71.3)
+    - React-cxxreact (= 0.71.4)
     - React-jsi
-    - React-jsiexecutor (= 0.71.3)
-    - React-jsinspector (= 0.71.3)
-    - React-perflogger (= 0.71.3)
-  - React-jsi (0.71.3):
+    - React-jsiexecutor (= 0.71.4)
+    - React-jsinspector (= 0.71.4)
+    - React-perflogger (= 0.71.4)
+  - React-jsi (0.71.4):
     - boost (= 1.76.0)
     - DoubleConversion
     - glog
     - hermes-engine
     - RCT-Folly (= 2021.07.22.00)
-  - React-jsiexecutor (0.71.3):
+  - React-jsiexecutor (0.71.4):
     - DoubleConversion
     - glog
     - hermes-engine
     - RCT-Folly (= 2021.07.22.00)
-    - React-cxxreact (= 0.71.3)
-    - React-jsi (= 0.71.3)
-    - React-perflogger (= 0.71.3)
-  - React-jsinspector (0.71.3)
-  - React-logger (0.71.3):
+    - React-cxxreact (= 0.71.4)
+    - React-jsi (= 0.71.4)
+    - React-perflogger (= 0.71.4)
+  - React-jsinspector (0.71.4)
+  - React-logger (0.71.4):
     - glog
   - react-native-blur (4.3.0):
     - React-Core
@@ -407,92 +407,90 @@ PODS:
     - React-Core
   - react-native-version-number (0.3.6):
     - React
-  - react-native-webview (11.26.0):
-    - React-Core
-  - React-perflogger (0.71.3)
-  - React-RCTActionSheet (0.71.3):
-    - React-Core/RCTActionSheetHeaders (= 0.71.3)
-  - React-RCTAnimation (0.71.3):
+  - React-perflogger (0.71.4)
+  - React-RCTActionSheet (0.71.4):
+    - React-Core/RCTActionSheetHeaders (= 0.71.4)
+  - React-RCTAnimation (0.71.4):
     - RCT-Folly (= 2021.07.22.00)
-    - RCTTypeSafety (= 0.71.3)
-    - React-Codegen (= 0.71.3)
-    - React-Core/RCTAnimationHeaders (= 0.71.3)
-    - React-jsi (= 0.71.3)
-    - ReactCommon/turbomodule/core (= 0.71.3)
-  - React-RCTAppDelegate (0.71.3):
+    - RCTTypeSafety (= 0.71.4)
+    - React-Codegen (= 0.71.4)
+    - React-Core/RCTAnimationHeaders (= 0.71.4)
+    - React-jsi (= 0.71.4)
+    - ReactCommon/turbomodule/core (= 0.71.4)
+  - React-RCTAppDelegate (0.71.4):
     - RCT-Folly
     - RCTRequired
     - RCTTypeSafety
     - React-Core
     - ReactCommon/turbomodule/core
-  - React-RCTBlob (0.71.3):
+  - React-RCTBlob (0.71.4):
     - hermes-engine
     - RCT-Folly (= 2021.07.22.00)
-    - React-Codegen (= 0.71.3)
-    - React-Core/RCTBlobHeaders (= 0.71.3)
-    - React-Core/RCTWebSocket (= 0.71.3)
-    - React-jsi (= 0.71.3)
-    - React-RCTNetwork (= 0.71.3)
-    - ReactCommon/turbomodule/core (= 0.71.3)
-  - React-RCTImage (0.71.3):
+    - React-Codegen (= 0.71.4)
+    - React-Core/RCTBlobHeaders (= 0.71.4)
+    - React-Core/RCTWebSocket (= 0.71.4)
+    - React-jsi (= 0.71.4)
+    - React-RCTNetwork (= 0.71.4)
+    - ReactCommon/turbomodule/core (= 0.71.4)
+  - React-RCTImage (0.71.4):
     - RCT-Folly (= 2021.07.22.00)
-    - RCTTypeSafety (= 0.71.3)
-    - React-Codegen (= 0.71.3)
-    - React-Core/RCTImageHeaders (= 0.71.3)
-    - React-jsi (= 0.71.3)
-    - React-RCTNetwork (= 0.71.3)
-    - ReactCommon/turbomodule/core (= 0.71.3)
-  - React-RCTLinking (0.71.3):
-    - React-Codegen (= 0.71.3)
-    - React-Core/RCTLinkingHeaders (= 0.71.3)
-    - React-jsi (= 0.71.3)
-    - ReactCommon/turbomodule/core (= 0.71.3)
-  - React-RCTNetwork (0.71.3):
+    - RCTTypeSafety (= 0.71.4)
+    - React-Codegen (= 0.71.4)
+    - React-Core/RCTImageHeaders (= 0.71.4)
+    - React-jsi (= 0.71.4)
+    - React-RCTNetwork (= 0.71.4)
+    - ReactCommon/turbomodule/core (= 0.71.4)
+  - React-RCTLinking (0.71.4):
+    - React-Codegen (= 0.71.4)
+    - React-Core/RCTLinkingHeaders (= 0.71.4)
+    - React-jsi (= 0.71.4)
+    - ReactCommon/turbomodule/core (= 0.71.4)
+  - React-RCTNetwork (0.71.4):
     - RCT-Folly (= 2021.07.22.00)
-    - RCTTypeSafety (= 0.71.3)
-    - React-Codegen (= 0.71.3)
-    - React-Core/RCTNetworkHeaders (= 0.71.3)
-    - React-jsi (= 0.71.3)
-    - ReactCommon/turbomodule/core (= 0.71.3)
-  - React-RCTSettings (0.71.3):
+    - RCTTypeSafety (= 0.71.4)
+    - React-Codegen (= 0.71.4)
+    - React-Core/RCTNetworkHeaders (= 0.71.4)
+    - React-jsi (= 0.71.4)
+    - ReactCommon/turbomodule/core (= 0.71.4)
+  - React-RCTSettings (0.71.4):
     - RCT-Folly (= 2021.07.22.00)
-    - RCTTypeSafety (= 0.71.3)
-    - React-Codegen (= 0.71.3)
-    - React-Core/RCTSettingsHeaders (= 0.71.3)
-    - React-jsi (= 0.71.3)
-    - ReactCommon/turbomodule/core (= 0.71.3)
-  - React-RCTText (0.71.3):
-    - React-Core/RCTTextHeaders (= 0.71.3)
-  - React-RCTVibration (0.71.3):
+    - RCTTypeSafety (= 0.71.4)
+    - React-Codegen (= 0.71.4)
+    - React-Core/RCTSettingsHeaders (= 0.71.4)
+    - React-jsi (= 0.71.4)
+    - ReactCommon/turbomodule/core (= 0.71.4)
+  - React-RCTText (0.71.4):
+    - React-Core/RCTTextHeaders (= 0.71.4)
+  - React-RCTVibration (0.71.4):
     - RCT-Folly (= 2021.07.22.00)
-    - React-Codegen (= 0.71.3)
-    - React-Core/RCTVibrationHeaders (= 0.71.3)
-    - React-jsi (= 0.71.3)
-    - ReactCommon/turbomodule/core (= 0.71.3)
-  - React-runtimeexecutor (0.71.3):
-    - React-jsi (= 0.71.3)
-  - ReactCommon/turbomodule/bridging (0.71.3):
+    - React-Codegen (= 0.71.4)
+    - React-Core/RCTVibrationHeaders (= 0.71.4)
+    - React-jsi (= 0.71.4)
+    - ReactCommon/turbomodule/core (= 0.71.4)
+  - React-runtimeexecutor (0.71.4):
+    - React-jsi (= 0.71.4)
+  - ReactCommon/turbomodule/bridging (0.71.4):
     - DoubleConversion
     - glog
     - hermes-engine
     - RCT-Folly (= 2021.07.22.00)
-    - React-callinvoker (= 0.71.3)
-    - React-Core (= 0.71.3)
-    - React-cxxreact (= 0.71.3)
-    - React-jsi (= 0.71.3)
-    - React-logger (= 0.71.3)
-    - React-perflogger (= 0.71.3)
-  - ReactCommon/turbomodule/core (0.71.3):
+    - React-callinvoker (= 0.71.4)
+    - React-Core (= 0.71.4)
+    - React-cxxreact (= 0.71.4)
+    - React-jsi (= 0.71.4)
+    - React-logger (= 0.71.4)
+    - React-perflogger (= 0.71.4)
+  - ReactCommon/turbomodule/core (0.71.4):
     - DoubleConversion
     - glog
     - hermes-engine
     - RCT-Folly (= 2021.07.22.00)
-    - React-callinvoker (= 0.71.3)
-    - React-Core (= 0.71.3)
-    - React-cxxreact (= 0.71.3)
-    - React-jsi (= 0.71.3)
-    - React-logger (= 0.71.3)
-    - React-perflogger (= 0.71.3)
+    - React-callinvoker (= 0.71.4)
+    - React-Core (= 0.71.4)
+    - React-cxxreact (= 0.71.4)
+    - React-jsi (= 0.71.4)
+    - React-logger (= 0.71.4)
+    - React-perflogger (= 0.71.4)
   - rn-fetch-blob (0.12.0):
     - React-Core
   - RNBackgroundFetch (4.1.9):
@@ -627,7 +625,6 @@ DEPENDENCIES:
   - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
   - react-native-splash-screen (from `../node_modules/react-native-splash-screen`)
   - react-native-version-number (from `../node_modules/react-native-version-number`)
-  - react-native-webview (from `../node_modules/react-native-webview`)
   - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
   - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
   - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`)
@@ -770,8 +767,6 @@ EXTERNAL SOURCES:
     :path: "../node_modules/react-native-splash-screen"
   react-native-version-number:
     :path: "../node_modules/react-native-version-number"
-  react-native-webview:
-    :path: "../node_modules/react-native-webview"
   React-perflogger:
     :path: "../node_modules/react-native/ReactCommon/reactperflogger"
   React-RCTActionSheet:
@@ -846,38 +841,38 @@ SPEC CHECKSUMS:
   EXJSONUtils: 48b1e764ac35160e6f54d21ab60d7d9501f3e473
   EXManifests: 500666d48e8dd7ca5a482c9e729e4a7a6c34081b
   EXMediaLibrary: 587cd8aad27a6fc8d7c38b950bc75bc1845a7480
-  Expo: 707f9b0039eacc6a1dce90c08c9e37b9c417bba2
+  Expo: 863488a600a4565698a79577117c70b170054d08
   expo-dev-client: 7c1ef51516853465f4d448c14ddf365167d20361
   expo-dev-launcher: 90de99d9e5d1a883d81355ca10e87c2f3c81d46e
-  expo-dev-menu: d4369e74d8d21a0ccdee35f7c732e7118b0fee16
+  expo-dev-menu: 4f54ef98df59d9d625677cb18ad4582de92b4a7d
   expo-dev-menu-interface: 6c82ae323c4b8724dead4763ce3ff24a2108bdb1
   ExpoImagePicker: 270dea232b3a072d981dd564e2cafc63a864edb1
   ExpoKeepAwake: 69f5f627670d62318410392d03e0b5db0f85759a
   ExpoLocalization: f26cd431ad9ea3533c5b08c4fabd879176a794bb
-  ExpoModulesCore: 397fc99e9d6c9dcc010f36d5802097c17b90424c
+  ExpoModulesCore: 6e0259511f4c4341b6b8357db393624df2280828
   EXSplashScreen: cd7fb052dff5ba8311d5c2455ecbebffe1b7a8ca
   EXUpdatesInterface: dd699d1930e28639dcbd70a402caea98e86364ca
-  FBLazyVector: 60195509584153283780abdac5569feffb8f08cc
-  FBReactNativeSpec: 9c191fb58d06dc05ab5559a5505fc32139e9e4a2
+  FBLazyVector: 446e84642979fff0ba57f3c804c2228a473aeac2
+  FBReactNativeSpec: 241709e132e3bf1526c1c4f00bc5384dd39dfba9
   fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
   glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
-  hermes-engine: 38bfe887e456b33b697187570a08de33969f5db7
+  hermes-engine: a1f157c49ea579c28b0296bda8530e980c45bdb3
   libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
   libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
   RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
-  RCTRequired: bec48f07daf7bcdc2655a0cde84e07d24d2a9e2a
-  RCTTypeSafety: 171394eebacf71e1cfad79dbfae7ee8fc16ca80a
-  React: d7433ccb6a8c36e4cbed59a73c0700fc83c3e98a
-  React-callinvoker: 15f165009bd22ae829b2b600e50bcc98076ce4b8
-  React-Codegen: b5910000eaf1e0c2f47d29be6f82f5f1264420d7
-  React-Core: b6f2f78d580a90b83fd7b0d1c6911c799f6eac82
-  React-CoreModules: e0cbc1a4f4f3f60e23c476fef7ab37be363ea8c1
-  React-cxxreact: c87f3f124b2117d00d410b35f16c2257e25e50fa
-  React-hermes: c64ca6bdf16a7069773103c9bedaf30ec90ab38f
-  React-jsi: 39729361645568e238081b3b3180fbad803f25a4
-  React-jsiexecutor: 515b703d23ffadeac7687bc2d12fb08b90f0aaa1
-  React-jsinspector: 9f7c9137605e72ca0343db4cea88006cb94856dd
-  React-logger: 957e5dc96d9dbffc6e0f15e0ee4d2b42829ff207
+  RCTRequired: 5a024fdf458fa8c0d82fc262e76f982d4dcdecdd
+  RCTTypeSafety: b6c253064466411c6810b45f66bc1e43ce0c54ba
+  React: 715292db5bd46989419445a5547954b25d2090f0
+  React-callinvoker: 105392d1179058585b564d35b4592fe1c46d6fba
+  React-Codegen: b75333b93d835afce84b73472927cccaef2c9f8c
+  React-Core: 88838ed1724c64905fc6c0811d752828a92e395b
+  React-CoreModules: cd238b4bb8dc8529ccc8b34ceae7267b04ce1882
+  React-cxxreact: 291bfab79d8098dc5ebab98f62e6bdfe81b3955a
+  React-hermes: b1e67e9a81c71745704950516f40ee804349641c
+  React-jsi: c9d5b563a6af6bb57034a82c2b0d39d0a7483bdc
+  React-jsiexecutor: d6b7fa9260aa3cb40afee0507e3bc1d17ecaa6f2
+  React-jsinspector: 1f51e775819199d3fe9410e69ee8d4c4161c7b06
+  React-logger: 0d58569ec51d30d1792c5e86a8e3b78d24b582c6
   react-native-blur: 50c9feabacbc5f49b61337ebc32192c6be7ec3c3
   react-native-cameraroll: f3050460fe1708378698c16686bfaa5f34099be2
   react-native-get-random-values: a6ea6a8a65dc93e96e24a11105b1a9c8cfe1d72a
@@ -887,20 +882,19 @@ SPEC CHECKSUMS:
   react-native-safe-area-context: 39c2d8be3328df5d437ac1700f4f3a4f75716acc
   react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457
   react-native-version-number: b415bbec6a13f2df62bf978e85bc0d699462f37f
-  react-native-webview: 994b9f8fbb504d6314dc40d83f94f27c6831b3bf
-  React-perflogger: af8a3d31546077f42d729b949925cc4549f14def
-  React-RCTActionSheet: 57cc5adfefbaaf0aae2cf7e10bccd746f2903673
-  React-RCTAnimation: 11c61e94da700c4dc915cf134513764d87fc5e2b
-  React-RCTAppDelegate: c3980adeaadcfd6cb495532e928b36ac6db3c14a
-  React-RCTBlob: ccc5049d742b41971141415ca86b83b201495695
-  React-RCTImage: 7a9226b0944f1e76e8e01e35a9245c2477cdbabb
-  React-RCTLinking: bbe8cc582046a9c04f79c235b73c93700263e8b4
-  React-RCTNetwork: fc2ca322159dc54e06508d4f5c3e934da63dc013
-  React-RCTSettings: f1e9db2cdf946426d3f2b210e4ff4ce0f0d842ef
-  React-RCTText: 1c41dd57e5d742b1396b4eeb251851ce7ff0fca1
-  React-RCTVibration: 5199a180d04873366a83855de55ac33ce60fe4d5
-  React-runtimeexecutor: 7bf0dafc7b727d93c8cb94eb00a9d3753c446c3e
-  ReactCommon: 6f65ea5b7d84deb9e386f670dd11ce499ded7b40
+  React-perflogger: 0bb0522a12e058f6eb69d888bc16f40c16c4b907
+  React-RCTActionSheet: bfd675a10f06a18728ea15d82082d48f228a213a
+  React-RCTAnimation: 2fa220b2052ec75b733112aca39143d34546a941
+  React-RCTAppDelegate: 8564f93c1d9274e95e3b0c746d08a87ff5a621b2
+  React-RCTBlob: d0336111f46301ae8aba2e161817e451aad72dd6
+  React-RCTImage: fec592c46edb7c12a9cde08780bdb4a688416c62
+  React-RCTLinking: 14eccac5d2a3b34b89dbfa29e8ef6219a153fe2d
+  React-RCTNetwork: 1fbce92e772e39ca3687a2ebb854501ff6226dd7
+  React-RCTSettings: 1abea36c9bb16d9979df6c4b42e2ea281b4bbcc5
+  React-RCTText: 15355c41561a9f43dfd23616d0a0dd40ba05ed61
+  React-RCTVibration: ad17efcfb2fa8f6bfd8ac0cf48d96668b8b28e0b
+  React-runtimeexecutor: 8fa50b38df6b992c76537993a2b0553d3b088004
+  ReactCommon: b49a4b00ca6d181ff74b17c12b2d59ac4add0bde
   rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
   RNBackgroundFetch: 642777e4e76435773c149d565a043d66f1781237
   RNCAsyncStorage: 09fc8595e6d6f6d5abf16b23a56b257d9c6b7c5b
@@ -921,7 +915,7 @@ SPEC CHECKSUMS:
   sovran-react-native: fd3dc8f1a4b14acdc4ad25fc6b4ac4f52a2a2a15
   Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b
   TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863
-  Yoga: 5ed1699acbba8863755998a4245daa200ff3817b
+  Yoga: 79dd7410de6f8ad73a77c868d3d368843f0c93e0
 
 PODFILE CHECKSUM: 5570c7b7d6ce7895f95d9db8a3a99b136a3f42c4
 
diff --git a/ios/bluesky/Info.plist b/ios/bluesky/Info.plist
index 117d763e5..f58ed1b55 100644
--- a/ios/bluesky/Info.plist
+++ b/ios/bluesky/Info.plist
@@ -21,7 +21,7 @@
 	<key>CFBundlePackageType</key>
 	<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
 	<key>CFBundleShortVersionString</key>
-	<string>1.10</string>
+	<string>1.11</string>
 	<key>CFBundleSignature</key>
 	<string>????</string>
 	<key>CFBundleURLTypes</key>
diff --git a/jest/test-pds.ts b/jest/test-pds.ts
index 32f3bc9b0..1e87df811 100644
--- a/jest/test-pds.ts
+++ b/jest/test-pds.ts
@@ -1,86 +1,73 @@
 import {AddressInfo} from 'net'
 import os from 'os'
+import net from 'net'
 import path from 'path'
 import * as crypto from '@atproto/crypto'
-import PDSServer, {
-  Database as PDSDatabase,
-  MemoryBlobStore,
-  ServerConfig as PDSServerConfig,
-} from '@atproto/pds'
-import * as plc from '@atproto/plc'
-import AtpAgent from '@atproto/api'
+import {PDS, ServerConfig, Database, MemoryBlobStore} from '@atproto/pds'
+import * as plc from '@did-plc/lib'
+import {PlcServer, Database as PlcDatabase} from '@did-plc/server'
+import {BskyAgent} from '@atproto/api'
+
+const ADMIN_PASSWORD = 'admin-pass'
+const SECOND = 1000
+const MINUTE = SECOND * 60
+const HOUR = MINUTE * 60
 
 export interface TestUser {
   email: string
   did: string
-  declarationCid: string
   handle: string
   password: string
-  agent: AtpAgent
-}
-
-export interface TestUsers {
-  alice: TestUser
-  bob: TestUser
-  carla: TestUser
+  agent: BskyAgent
 }
 
 export interface TestPDS {
   pdsUrl: string
-  users: TestUsers
+  mocker: Mocker
   close: () => Promise<void>
 }
 
-// NOTE
-// deterministic date generator
-// we use this to ensure the mock dataset is always the same
-// which is very useful when testing
-function* dateGen() {
-  let start = 1657846031914
-  while (true) {
-    yield new Date(start).toISOString()
-    start += 1e3
-  }
-}
-
 export async function createServer(): Promise<TestPDS> {
-  const keypair = await crypto.EcdsaKeypair.create()
+  const repoSigningKey = await crypto.Secp256k1Keypair.create()
+  const plcRotationKey = await crypto.Secp256k1Keypair.create()
+  const port = await getPort()
+
+  const plcDb = PlcDatabase.mock()
 
-  // run plc server
-  const plcDb = plc.Database.memory()
-  await plcDb.migrateToLatestOrThrow()
-  const plcServer = plc.PlcServer.create({db: plcDb})
+  const plcServer = PlcServer.create({db: plcDb})
   const plcListener = await plcServer.start()
   const plcPort = (plcListener.address() as AddressInfo).port
   const plcUrl = `http://localhost:${plcPort}`
 
-  const recoveryKey = (await crypto.EcdsaKeypair.create()).did()
+  const recoveryKey = (await crypto.Secp256k1Keypair.create()).did()
 
-  const plcClient = new plc.PlcClient(plcUrl)
-  const serverDid = await plcClient.createDid(
-    keypair,
-    recoveryKey,
-    'localhost',
-    'https://pds.public.url',
-  )
+  const plcClient = new plc.Client(plcUrl)
+  const serverDid = await plcClient.createDid({
+    signingKey: repoSigningKey.did(),
+    rotationKeys: [recoveryKey, plcRotationKey.did()],
+    handle: 'localhost',
+    pds: `http://localhost:${port}`,
+    signer: plcRotationKey,
+  })
 
   const blobstoreLoc = path.join(os.tmpdir(), crypto.randomStr(5, 'base32'))
 
-  const cfg = new PDSServerConfig({
+  const cfg = new ServerConfig({
     debugMode: true,
     version: '0.0.0',
     scheme: 'http',
     hostname: 'localhost',
+    port,
     serverDid,
     recoveryKey,
-    adminPassword: 'admin-pass',
+    adminPassword: ADMIN_PASSWORD,
     inviteRequired: false,
     didPlcUrl: plcUrl,
     jwtSecret: 'jwt-secret',
     availableUserDomains: ['.test'],
     appUrlPasswordReset: 'app://forgot-password',
     emailNoReplyAddress: 'noreply@blueskyweb.xyz',
-    publicUrl: 'https://pds.public.url',
+    publicUrl: `http://localhost:${port}`,
     imgUriSalt: '9dd04221f5755bce5f55f47464c27e1e',
     imgUriKey:
       'f23ecd142835025f42c3db2cf25dd813956c178392760256211f9d315f8ab4d8',
@@ -88,22 +75,33 @@ export async function createServer(): Promise<TestPDS> {
     blobstoreLocation: `${blobstoreLoc}/blobs`,
     blobstoreTmp: `${blobstoreLoc}/tmp`,
     maxSubscriptionBuffer: 200,
-    repoBackfillLimitMs: 1e3 * 60 * 60,
+    repoBackfillLimitMs: HOUR,
   })
 
-  const db = PDSDatabase.memory()
+  const db =
+    cfg.dbPostgresUrl !== undefined
+      ? Database.postgres({
+          url: cfg.dbPostgresUrl,
+          schema: cfg.dbPostgresSchema,
+        })
+      : Database.memory()
   await db.migrateToLatestOrThrow()
+
   const blobstore = new MemoryBlobStore()
 
-  const pds = PDSServer.create({db, blobstore, keypair, config: cfg})
-  const pdsServer = await pds.start()
-  const pdsPort = (pdsServer.address() as AddressInfo).port
-  const pdsUrl = `http://localhost:${pdsPort}`
-  const testUsers = await genMockData(pdsUrl)
+  const pds = PDS.create({
+    db,
+    blobstore,
+    repoSigningKey,
+    plcRotationKey,
+    config: cfg,
+  })
+  await pds.start()
+  const pdsUrl = `http://localhost:${port}`
 
   return {
     pdsUrl,
-    users: testUsers,
+    mocker: new Mocker(pdsUrl),
     async close() {
       await pds.destroy()
       await plcServer.destroy()
@@ -111,90 +109,93 @@ export async function createServer(): Promise<TestPDS> {
   }
 }
 
-async function genMockData(pdsUrl: string): Promise<TestUsers> {
-  const date = dateGen()
+class Mocker {
+  agent: BskyAgent
+  users: Record<string, TestUser> = {}
 
-  const agents = {
-    loggedout: new AtpAgent({service: pdsUrl}),
-    alice: new AtpAgent({service: pdsUrl}),
-    bob: new AtpAgent({service: pdsUrl}),
-    carla: new AtpAgent({service: pdsUrl}),
+  constructor(public service: string) {
+    this.agent = new BskyAgent({service})
   }
-  const users: TestUser[] = [
-    {
-      email: 'alice@test.com',
-      did: '',
-      declarationCid: '',
-      handle: 'alice.test',
-      password: 'hunter2',
-      agent: agents.alice,
-    },
-    {
-      email: 'bob@test.com',
-      did: '',
-      declarationCid: '',
-      handle: 'bob.test',
-      password: 'hunter2',
-      agent: agents.bob,
-    },
-    {
-      email: 'carla@test.com',
-      did: '',
-      declarationCid: '',
-      handle: 'carla.test',
+
+  // NOTE
+  // deterministic date generator
+  // we use this to ensure the mock dataset is always the same
+  // which is very useful when testing
+  *dateGen() {
+    let start = 1657846031914
+    while (true) {
+      yield new Date(start).toISOString()
+      start += 1e3
+    }
+  }
+
+  async createUser(name: string) {
+    const agent = new BskyAgent({service: this.agent.service})
+    const email = `fake${Object.keys(this.users).length + 1}@fake.com`
+    const res = await agent.createAccount({
+      email,
+      handle: name + '.test',
       password: 'hunter2',
-      agent: agents.carla,
-    },
-  ]
-  const alice = users[0]
-  const bob = users[1]
-  const carla = users[2]
-
-  let _i = 1
-  for (const user of users) {
-    const res = await agents.loggedout.api.com.atproto.account.create({
-      email: user.email,
-      handle: user.handle,
-      password: user.password,
-    })
-    user.agent.api.setHeader('Authorization', `Bearer ${res.data.accessJwt}`)
-    const {data: profile} = await user.agent.api.app.bsky.actor.getProfile({
-      actor: user.handle,
     })
-    user.did = res.data.did
-    user.declarationCid = profile.declaration.cid
-    await user.agent.api.app.bsky.actor.profile.create(
-      {did: user.did},
-      {
-        displayName: ucfirst(user.handle).slice(0, -5),
-        description: `Test user ${_i++}`,
-      },
-    )
+    this.users[name] = {
+      did: res.data.did,
+      email,
+      handle: name + '.test',
+      password: 'hunter2',
+      agent: agent,
+    }
   }
 
-  // everybody follows everybody
-  const follow = async (author: TestUser, subject: TestUser) => {
-    await author.agent.api.app.bsky.graph.follow.create(
-      {did: author.did},
-      {
-        subject: {
-          did: subject.did,
-          declarationCid: subject.declarationCid,
-        },
-        createdAt: date.next().value || '',
-      },
-    )
+  async follow(a: string, b: string) {
+    await this.users[a].agent.follow(this.users[b].did)
+  }
+
+  async generateStandardGraph() {
+    await this.createUser('alice')
+    await this.createUser('bob')
+    await this.createUser('carla')
+
+    await this.users.alice.agent.upsertProfile(() => ({
+      displayName: 'Alice',
+      description: 'Test user 1',
+    }))
+
+    await this.users.bob.agent.upsertProfile(() => ({
+      displayName: 'Bob',
+      description: 'Test user 2',
+    }))
+
+    await this.users.carla.agent.upsertProfile(() => ({
+      displayName: 'Carla',
+      description: 'Test user 3',
+    }))
+
+    await this.follow('alice', 'bob')
+    await this.follow('alice', 'carla')
+    await this.follow('bob', 'alice')
+    await this.follow('bob', 'carla')
+    await this.follow('carla', 'alice')
+    await this.follow('carla', 'bob')
   }
-  await follow(alice, bob)
-  await follow(alice, carla)
-  await follow(bob, alice)
-  await follow(bob, carla)
-  await follow(carla, alice)
-  await follow(carla, bob)
-
-  return {alice, bob, carla}
 }
 
-function ucfirst(str: string): string {
-  return str.at(0)?.toUpperCase() + str.slice(1)
+const checkAvailablePort = (port: number) =>
+  new Promise(resolve => {
+    const server = net.createServer()
+    server.unref()
+    server.on('error', () => resolve(false))
+    server.listen({port}, () => {
+      server.close(() => {
+        resolve(true)
+      })
+    })
+  })
+
+async function getPort() {
+  for (let i = 3000; i < 65000; i++) {
+    if (await checkAvailablePort(i)) {
+      return i
+    }
+  }
+  throw new Error('Unable to find an available port')
 }
diff --git a/metro.config.js b/metro.config.js
index 9e8e8745a..b1714479f 100644
--- a/metro.config.js
+++ b/metro.config.js
@@ -1,3 +1,7 @@
 // Learn more https://docs.expo.io/guides/customizing-metro
 const {getDefaultConfig} = require('expo/metro-config')
-module.exports = getDefaultConfig(__dirname)
+const cfg = getDefaultConfig(__dirname)
+cfg.resolver.sourceExts = process.env.RN_SRC_EXT
+  ? process.env.RN_SRC_EXT.split(',').concat(cfg.resolver.sourceExts)
+  : cfg.resolver.sourceExts
+module.exports = cfg
diff --git a/package.json b/package.json
index 8f2f7e9db..2ac5367a4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "bsky.app",
-  "version": "1.10.0",
+  "version": "1.11.0",
   "private": true,
   "scripts": {
     "postinstall": "patch-package",
@@ -15,12 +15,13 @@
     "test-ci": "jest --ci --forceExit --reporters=default --reporters=jest-junit",
     "test-coverage": "jest --coverage",
     "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx",
-    "e2e": "detox test --configuration ios.sim.debug --take-screenshots all"
+    "e2e:mock-server": "ts-node __e2e__/mock-server.ts",
+    "e2e:metro": "RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
+    "e2e:build": "detox build -c ios.sim.debug",
+    "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all"
   },
   "dependencies": {
-    "@atproto/api": "0.1.3",
-    "@atproto/lexicon": "^0.0.4",
-    "@atproto/xrpc": "^0.0.4",
+    "@atproto/api": "0.2.0",
     "@bam.tech/react-native-image-resizer": "^3.0.4",
     "@expo/webpack-config": "^18.0.1",
     "@fortawesome/fontawesome-svg-core": "^6.1.1",
@@ -55,7 +56,7 @@
     "await-lock": "^2.2.2",
     "base64-js": "^1.5.1",
     "email-validator": "^2.0.4",
-    "expo": "~48.0.0-beta.2",
+    "expo": "~48.0.9",
     "expo-camera": "~13.2.1",
     "expo-dev-client": "~2.1.1",
     "expo-image-picker": "~14.1.1",
@@ -63,6 +64,8 @@
     "expo-media-library": "~15.2.3",
     "expo-splash-screen": "~0.18.1",
     "expo-status-bar": "~1.4.4",
+    "fast-text-encoding": "^1.0.6",
+    "graphemer": "^1.4.0",
     "he": "^1.2.0",
     "history": "^5.3.0",
     "js-sha256": "^0.9.0",
@@ -84,7 +87,7 @@
     "react-avatar-editor": "^13.0.0",
     "react-circular-progressbar": "^2.1.0",
     "react-dom": "^18.2.0",
-    "react-native": "0.71.3",
+    "react-native": "0.71.4",
     "react-native-appstate-hook": "^1.0.6",
     "react-native-background-fetch": "^4.1.8",
     "react-native-drawer-layout": "^3.2.0",
@@ -109,19 +112,17 @@
     "react-native-version-number": "^0.3.6",
     "react-native-web": "^0.18.11",
     "react-native-web-linear-gradient": "^1.1.2",
-    "react-native-web-webview": "^1.0.2",
-    "react-native-webview": "11.26.0",
-    "react-native-youtube-iframe": "^2.2.2",
     "rn-fetch-blob": "^0.12.0",
     "tippy.js": "^6.3.7",
     "tlds": "^1.234.0",
     "zod": "^3.20.2"
   },
   "devDependencies": {
-    "@atproto/pds": "^0.0.3",
+    "@atproto/pds": "^0.1.0",
     "@babel/core": "^7.20.0",
     "@babel/preset-env": "^7.20.0",
     "@babel/runtime": "^7.20.0",
+    "@did-plc/server": "^0.0.1",
     "@react-native-community/eslint-config": "^3.0.0",
     "@testing-library/jest-native": "^5.4.1",
     "@testing-library/react-native": "^11.5.2",
@@ -150,13 +151,14 @@
     "eslint-plugin-ft-flow": "^2.0.3",
     "html-webpack-plugin": "^5.5.0",
     "jest": "^29.4.3",
-    "jest-expo": "^48.0.0-beta.2",
+    "jest-expo": "^48.0.2",
     "jest-junit": "^15.0.0",
     "metro-react-native-babel-preset": "^0.73.7",
     "prettier": "^2.8.3",
     "react-native-dotenv": "^3.3.1",
     "react-scripts": "^5.0.1",
     "react-test-renderer": "18.2.0",
+    "ts-node": "^10.9.1",
     "typescript": "^4.4.4",
     "url-loader": "^4.1.1",
     "webpack": "^5.75.0",
diff --git a/src/App.native.tsx b/src/App.native.tsx
index ebe6a7cd6..0adbae606 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -29,7 +29,6 @@ const App = observer(() => {
       analytics.init(store)
       notifee.init(store)
       SplashScreen.hide()
-      store.hackCheckIfUpgradeNeeded()
       Linking.getInitialURL().then((url: string | null) => {
         if (url) {
           handleLink(url)
diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index 2bfc84ea9..a1dbc4af1 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -31,7 +31,7 @@ import {ProfileScreen} from './view/screens/Profile'
 import {ProfileFollowersScreen} from './view/screens/ProfileFollowers'
 import {ProfileFollowsScreen} from './view/screens/ProfileFollows'
 import {PostThreadScreen} from './view/screens/PostThread'
-import {PostUpvotedByScreen} from './view/screens/PostUpvotedBy'
+import {PostLikedByScreen} from './view/screens/PostLikedBy'
 import {PostRepostedByScreen} from './view/screens/PostRepostedBy'
 import {DebugScreen} from './view/screens/Debug'
 import {LogScreen} from './view/screens/Log'
@@ -62,7 +62,7 @@ function commonScreens(Stack: typeof HomeTab) {
       />
       <Stack.Screen name="ProfileFollows" component={ProfileFollowsScreen} />
       <Stack.Screen name="PostThread" component={PostThreadScreen} />
-      <Stack.Screen name="PostUpvotedBy" component={PostUpvotedByScreen} />
+      <Stack.Screen name="PostLikedBy" component={PostLikedByScreen} />
       <Stack.Screen name="PostRepostedBy" component={PostRepostedByScreen} />
       <Stack.Screen name="Debug" component={DebugScreen} />
       <Stack.Screen name="Log" component={LogScreen} />
diff --git a/src/lib/api/api-polyfill.ts b/src/lib/api/api-polyfill.ts
index b7be6913a..7c38625a2 100644
--- a/src/lib/api/api-polyfill.ts
+++ b/src/lib/api/api-polyfill.ts
@@ -1,11 +1,11 @@
-import AtpAgent from '@atproto/api'
+import {BskyAgent, stringifyLex, jsonToLex} from '@atproto/api'
 import RNFS from 'react-native-fs'
 
 const GET_TIMEOUT = 15e3 // 15s
 const POST_TIMEOUT = 60e3 // 60s
 
 export function doPolyfill() {
-  AtpAgent.configure({fetch: fetchHandler})
+  BskyAgent.configure({fetch: fetchHandler})
 }
 
 interface FetchHandlerResponse {
@@ -22,7 +22,7 @@ async function fetchHandler(
 ): Promise<FetchHandlerResponse> {
   const reqMimeType = reqHeaders['Content-Type'] || reqHeaders['content-type']
   if (reqMimeType && reqMimeType.startsWith('application/json')) {
-    reqBody = JSON.stringify(reqBody)
+    reqBody = stringifyLex(reqBody)
   } else if (
     typeof reqBody === 'string' &&
     (reqBody.startsWith('/') || reqBody.startsWith('file:'))
@@ -65,7 +65,7 @@ async function fetchHandler(
   let resBody
   if (resMimeType) {
     if (resMimeType.startsWith('application/json')) {
-      resBody = await res.json()
+      resBody = jsonToLex(await res.json())
     } else if (resMimeType.startsWith('text/')) {
       resBody = await res.text()
     } else {
diff --git a/src/lib/api/api-polyfill.web.ts b/src/lib/api/api-polyfill.web.ts
index 1469cf905..1ad22b3d0 100644
--- a/src/lib/api/api-polyfill.web.ts
+++ b/src/lib/api/api-polyfill.web.ts
@@ -1,4 +1,3 @@
 export function doPolyfill() {
-  // TODO needed? native fetch may work fine -prf
-  // AtpApi.xrpc.fetch = fetchHandler
+  // no polyfill is needed on web
 }
diff --git a/src/lib/api/build-suggested-posts.ts b/src/lib/api/build-suggested-posts.ts
index defa45311..b9feefc72 100644
--- a/src/lib/api/build-suggested-posts.ts
+++ b/src/lib/api/build-suggested-posts.ts
@@ -1,9 +1,9 @@
 import {RootStoreModel} from 'state/index'
 import {
-  AppBskyFeedFeedViewPost,
+  AppBskyFeedDefs,
   AppBskyFeedGetAuthorFeed as GetAuthorFeed,
 } from '@atproto/api'
-type ReasonRepost = AppBskyFeedFeedViewPost.ReasonRepost
+type ReasonRepost = AppBskyFeedDefs.ReasonRepost
 
 async function getMultipleAuthorsPosts(
   rootStore: RootStoreModel,
@@ -12,12 +12,12 @@ async function getMultipleAuthorsPosts(
   limit: number = 10,
 ) {
   const responses = await Promise.all(
-    authors.map((author, index) =>
-      rootStore.api.app.bsky.feed
+    authors.map((actor, index) =>
+      rootStore.agent
         .getAuthorFeed({
-          author,
+          actor,
           limit,
-          before: cursor ? cursor.split(',')[index] : undefined,
+          cursor: cursor ? cursor.split(',')[index] : undefined,
         })
         .catch(_err => ({success: false, headers: {}, data: {feed: []}})),
     ),
@@ -29,14 +29,14 @@ function mergePosts(
   responses: GetAuthorFeed.Response[],
   {repostsOnly, bestOfOnly}: {repostsOnly?: boolean; bestOfOnly?: boolean},
 ) {
-  let posts: AppBskyFeedFeedViewPost.Main[] = []
+  let posts: AppBskyFeedDefs.FeedViewPost[] = []
 
   if (bestOfOnly) {
     for (const res of responses) {
       if (res.success) {
-        // filter the feed down to the post with the most upvotes
+        // filter the feed down to the post with the most likes
         res.data.feed = res.data.feed.reduce(
-          (acc: AppBskyFeedFeedViewPost.Main[], v) => {
+          (acc: AppBskyFeedDefs.FeedViewPost[], v) => {
             if (
               !acc?.[0] &&
               !v.reason &&
@@ -49,7 +49,7 @@ function mergePosts(
               acc &&
               !v.reason &&
               !v.reply &&
-              v.post.upvoteCount > acc[0]?.post.upvoteCount &&
+              (v.post.likeCount || 0) > (acc[0]?.post.likeCount || 0) &&
               isRecentEnough(v.post.indexedAt)
             ) {
               return [v]
@@ -92,7 +92,7 @@ function mergePosts(
   return posts
 }
 
-function isARepostOfSomeoneElse(post: AppBskyFeedFeedViewPost.Main): boolean {
+function isARepostOfSomeoneElse(post: AppBskyFeedDefs.FeedViewPost): boolean {
   return (
     post.reason?.$type === 'app.bsky.feed.feedViewPost#reasonRepost' &&
     post.post.author.did !== (post.reason as ReasonRepost).by.did
diff --git a/src/lib/api/feed-manip.ts b/src/lib/api/feed-manip.ts
index e9a32b7a6..6fdc9a48f 100644
--- a/src/lib/api/feed-manip.ts
+++ b/src/lib/api/feed-manip.ts
@@ -1,8 +1,8 @@
-import {AppBskyFeedFeedViewPost} from '@atproto/api'
+import {AppBskyFeedDefs} from '@atproto/api'
 import lande from 'lande'
-type FeedViewPost = AppBskyFeedFeedViewPost.Main
-import {hasProp} from '@atproto/lexicon'
+import {hasProp} from 'lib/type-guards'
 import {LANGUAGES_MAP_CODE2} from '../../locale/languages'
+type FeedViewPost = AppBskyFeedDefs.FeedViewPost
 
 export type FeedTunerFn = (
   tuner: FeedTuner,
@@ -174,7 +174,7 @@ export class FeedTuner {
       }
       const item = slices[i].rootItem
       const isRepost = Boolean(item.reason)
-      if (!isRepost && item.post.upvoteCount < 2) {
+      if (!isRepost && (item.post.likeCount || 0) < 2) {
         slices.splice(i, 1)
       }
     }
diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts
index 85eca4a61..a5aa916df 100644
--- a/src/lib/api/index.ts
+++ b/src/lib/api/index.ts
@@ -1,16 +1,16 @@
 import {
   AppBskyEmbedImages,
   AppBskyEmbedExternal,
-  ComAtprotoBlobUpload,
   AppBskyEmbedRecord,
+  AppBskyEmbedRecordWithMedia,
+  ComAtprotoRepoUploadBlob,
+  RichText,
 } from '@atproto/api'
 import {AtUri} from '../../third-party/uri'
 import {RootStoreModel} from 'state/models/root-store'
-import {extractEntities} from 'lib/strings/rich-text-detection'
 import {isNetworkError} from 'lib/strings/errors'
 import {LinkMeta} from '../link-meta/link-meta'
 import {Image} from '../media/manip'
-import {RichText} from '../strings/rich-text'
 import {isWeb} from 'platform/detection'
 
 export interface ExternalEmbedDraft {
@@ -27,7 +27,7 @@ export async function resolveName(store: RootStoreModel, didOrHandle: string) {
   if (didOrHandle.startsWith('did:')) {
     return didOrHandle
   }
-  const res = await store.api.com.atproto.handle.resolve({
+  const res = await store.agent.resolveHandle({
     handle: didOrHandle,
   })
   return res.data.did
@@ -37,15 +37,15 @@ export async function uploadBlob(
   store: RootStoreModel,
   blob: string,
   encoding: string,
-): Promise<ComAtprotoBlobUpload.Response> {
+): Promise<ComAtprotoRepoUploadBlob.Response> {
   if (isWeb) {
     // `blob` should be a data uri
-    return store.api.com.atproto.blob.upload(convertDataURIToUint8Array(blob), {
+    return store.agent.uploadBlob(convertDataURIToUint8Array(blob), {
       encoding,
     })
   } else {
     // `blob` should be a path to a file in the local FS
-    return store.api.com.atproto.blob.upload(
+    return store.agent.uploadBlob(
       blob, // this will be special-cased by the fetch monkeypatch in /src/state/lib/api.ts
       {encoding},
     )
@@ -70,22 +70,18 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
     | AppBskyEmbedImages.Main
     | AppBskyEmbedExternal.Main
     | AppBskyEmbedRecord.Main
+    | AppBskyEmbedRecordWithMedia.Main
     | undefined
   let reply
-  const text = new RichText(opts.rawText, undefined, {
-    cleanNewlines: true,
-  }).text.trim()
+  const rt = new RichText(
+    {text: opts.rawText.trim()},
+    {
+      cleanNewlines: true,
+    },
+  )
 
   opts.onStateChange?.('Processing...')
-  const entities = extractEntities(text, opts.knownHandles)
-  if (entities) {
-    for (const ent of entities) {
-      if (ent.type === 'mention') {
-        const prof = await store.profiles.getProfile(ent.value)
-        ent.value = prof.data.did
-      }
-    }
-  }
+  await rt.detectFacets(store.agent)
 
   if (opts.quote) {
     embed = {
@@ -95,24 +91,37 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
         cid: opts.quote.cid,
       },
     } as AppBskyEmbedRecord.Main
-  } else if (opts.images?.length) {
-    embed = {
-      $type: 'app.bsky.embed.images',
-      images: [],
-    } as AppBskyEmbedImages.Main
-    let i = 1
+  }
+
+  if (opts.images?.length) {
+    const images: AppBskyEmbedImages.Image[] = []
     for (const image of opts.images) {
-      opts.onStateChange?.(`Uploading image #${i++}...`)
+      opts.onStateChange?.(`Uploading image #${images.length + 1}...`)
       const res = await uploadBlob(store, image, 'image/jpeg')
-      embed.images.push({
-        image: {
-          cid: res.data.cid,
-          mimeType: 'image/jpeg',
-        },
+      images.push({
+        image: res.data.blob,
         alt: '', // TODO supply alt text
       })
     }
-  } else if (opts.extLink) {
+
+    if (opts.quote) {
+      embed = {
+        $type: 'app.bsky.embed.recordWithMedia',
+        record: embed,
+        media: {
+          $type: 'app.bsky.embed.images',
+          images,
+        },
+      } as AppBskyEmbedRecordWithMedia.Main
+    } else {
+      embed = {
+        $type: 'app.bsky.embed.images',
+        images,
+      } as AppBskyEmbedImages.Main
+    }
+  }
+
+  if (opts.extLink && !opts.images?.length) {
     let thumb
     if (opts.extLink.localThumb) {
       opts.onStateChange?.('Uploading link thumbnail...')
@@ -138,27 +147,41 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
           opts.extLink.localThumb.path,
           encoding,
         )
-        thumb = {
-          cid: thumbUploadRes.data.cid,
-          mimeType: encoding,
-        }
+        thumb = thumbUploadRes.data.blob
       }
     }
-    embed = {
-      $type: 'app.bsky.embed.external',
-      external: {
-        uri: opts.extLink.uri,
-        title: opts.extLink.meta?.title || '',
-        description: opts.extLink.meta?.description || '',
-        thumb,
-      },
-    } as AppBskyEmbedExternal.Main
+
+    if (opts.quote) {
+      embed = {
+        $type: 'app.bsky.embed.recordWithMedia',
+        record: embed,
+        media: {
+          $type: 'app.bsky.embed.external',
+          external: {
+            uri: opts.extLink.uri,
+            title: opts.extLink.meta?.title || '',
+            description: opts.extLink.meta?.description || '',
+            thumb,
+          },
+        } as AppBskyEmbedExternal.Main,
+      } as AppBskyEmbedRecordWithMedia.Main
+    } else {
+      embed = {
+        $type: 'app.bsky.embed.external',
+        external: {
+          uri: opts.extLink.uri,
+          title: opts.extLink.meta?.title || '',
+          description: opts.extLink.meta?.description || '',
+          thumb,
+        },
+      } as AppBskyEmbedExternal.Main
+    }
   }
 
   if (opts.replyTo) {
     const replyToUrip = new AtUri(opts.replyTo)
-    const parentPost = await store.api.app.bsky.feed.post.get({
-      user: replyToUrip.host,
+    const parentPost = await store.agent.getPost({
+      repo: replyToUrip.host,
       rkey: replyToUrip.rkey,
     })
     if (parentPost) {
@@ -175,16 +198,12 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
 
   try {
     opts.onStateChange?.('Posting...')
-    return await store.api.app.bsky.feed.post.create(
-      {did: store.me.did || ''},
-      {
-        text,
-        reply,
-        embed,
-        entities,
-        createdAt: new Date().toISOString(),
-      },
-    )
+    return await store.agent.post({
+      text: rt.text,
+      facets: rt.facets,
+      reply,
+      embed,
+    })
   } catch (e: any) {
     console.error(`Failed to create post: ${e.toString()}`)
     if (isNetworkError(e)) {
@@ -197,49 +216,6 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
   }
 }
 
-export async function repost(store: RootStoreModel, uri: string, cid: string) {
-  return await store.api.app.bsky.feed.repost.create(
-    {did: store.me.did || ''},
-    {
-      subject: {uri, cid},
-      createdAt: new Date().toISOString(),
-    },
-  )
-}
-
-export async function unrepost(store: RootStoreModel, repostUri: string) {
-  const repostUrip = new AtUri(repostUri)
-  return await store.api.app.bsky.feed.repost.delete({
-    did: repostUrip.hostname,
-    rkey: repostUrip.rkey,
-  })
-}
-
-export async function follow(
-  store: RootStoreModel,
-  subjectDid: string,
-  subjectDeclarationCid: string,
-) {
-  return await store.api.app.bsky.graph.follow.create(
-    {did: store.me.did || ''},
-    {
-      subject: {
-        did: subjectDid,
-        declarationCid: subjectDeclarationCid,
-      },
-      createdAt: new Date().toISOString(),
-    },
-  )
-}
-
-export async function unfollow(store: RootStoreModel, followUri: string) {
-  const followUrip = new AtUri(followUri)
-  return await store.api.app.bsky.graph.follow.delete({
-    did: followUrip.hostname,
-    rkey: followUrip.rkey,
-  })
-}
-
 // helpers
 // =
 
diff --git a/src/lib/media/picker.e2e.tsx b/src/lib/media/picker.e2e.tsx
new file mode 100644
index 000000000..9f4765ac2
--- /dev/null
+++ b/src/lib/media/picker.e2e.tsx
@@ -0,0 +1,116 @@
+import {RootStoreModel} from 'state/index'
+import {PickerOpts, CameraOpts, CropperOpts, PickedMedia} from './types'
+import {
+  scaleDownDimensions,
+  Dim,
+  compressIfNeeded,
+  moveToPremanantPath,
+} from 'lib/media/manip'
+export type {PickedMedia} from './types'
+import RNFS from 'react-native-fs'
+
+let _imageCounter = 0
+async function getFile() {
+  const files = await RNFS.readDir(
+    RNFS.LibraryDirectoryPath.split('/')
+      .slice(0, -5)
+      .concat(['Media', 'DCIM', '100APPLE'])
+      .join('/'),
+  )
+  return files[_imageCounter++ % files.length]
+}
+
+export async function openPicker(
+  _store: RootStoreModel,
+  opts: PickerOpts,
+): Promise<PickedMedia[]> {
+  const mediaType = opts.mediaType || 'photo'
+  const items = await getFile()
+  const toMedia = (item: RNFS.ReadDirItem) => ({
+    mediaType,
+    path: item.path,
+    mime: 'image/jpeg',
+    size: item.size,
+    width: 4288,
+    height: 2848,
+  })
+  if (Array.isArray(items)) {
+    return items.map(toMedia)
+  }
+  return [toMedia(items)]
+}
+
+export async function openCamera(
+  _store: RootStoreModel,
+  opts: CameraOpts,
+): Promise<PickedMedia> {
+  const mediaType = opts.mediaType || 'photo'
+  const item = await getFile()
+  return {
+    mediaType,
+    path: item.path,
+    mime: 'image/jpeg',
+    size: item.size,
+    width: 4288,
+    height: 2848,
+  }
+}
+
+export async function openCropper(
+  _store: RootStoreModel,
+  opts: CropperOpts,
+): Promise<PickedMedia> {
+  const mediaType = opts.mediaType || 'photo'
+  const item = await getFile()
+  return {
+    mediaType,
+    path: item.path,
+    mime: 'image/jpeg',
+    size: item.size,
+    width: 4288,
+    height: 2848,
+  }
+}
+
+export async function pickImagesFlow(
+  store: RootStoreModel,
+  maxFiles: number,
+  maxDim: Dim,
+  maxSize: number,
+) {
+  const items = await openPicker(store, {
+    multiple: true,
+    maxFiles,
+    mediaType: 'photo',
+  })
+  const result = []
+  for (const image of items) {
+    result.push(
+      await cropAndCompressFlow(store, image.path, image, maxDim, maxSize),
+    )
+  }
+  return result
+}
+
+export async function cropAndCompressFlow(
+  store: RootStoreModel,
+  path: string,
+  imgDim: Dim,
+  maxDim: Dim,
+  maxSize: number,
+) {
+  // choose target dimensions based on the original
+  // this causes the photo cropper to start with the full image "selected"
+  const {width, height} = scaleDownDimensions(imgDim, maxDim)
+  const cropperRes = await openCropper(store, {
+    mediaType: 'photo',
+    path,
+    freeStyleCropEnabled: true,
+    width,
+    height,
+  })
+
+  const img = await compressIfNeeded(cropperRes, maxSize)
+  const permanentPath = await moveToPremanantPath(img.path)
+  return permanentPath
+}
diff --git a/src/lib/notifee.ts b/src/lib/notifee.ts
index 4baf64050..4b53ed724 100644
--- a/src/lib/notifee.ts
+++ b/src/lib/notifee.ts
@@ -45,7 +45,7 @@ export function displayNotificationFromModel(
   let author = notif.author.displayName || notif.author.handle
   let title: string
   let body: string = ''
-  if (notif.isUpvote) {
+  if (notif.isLike) {
     title = `${author} liked your post`
     body = notif.additionalPost?.thread?.postRecord?.text || ''
   } else if (notif.isRepost) {
@@ -65,7 +65,7 @@ export function displayNotificationFromModel(
   }
   let image
   if (
-    AppBskyEmbedImages.isPresented(notif.additionalPost?.thread?.post.embed) &&
+    AppBskyEmbedImages.isView(notif.additionalPost?.thread?.post.embed) &&
     notif.additionalPost?.thread?.post.embed.images[0]?.thumb
   ) {
     image = notif.additionalPost.thread.post.embed.images[0].thumb
diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts
index cc48e2dbe..59d94efa8 100644
--- a/src/lib/routes/types.ts
+++ b/src/lib/routes/types.ts
@@ -10,7 +10,7 @@ export type CommonNavigatorParams = {
   ProfileFollowers: {name: string}
   ProfileFollows: {name: string}
   PostThread: {name: string; rkey: string}
-  PostUpvotedBy: {name: string; rkey: string}
+  PostLikedBy: {name: string; rkey: string}
   PostRepostedBy: {name: string; rkey: string}
   Debug: undefined
   Log: undefined
diff --git a/src/lib/strings/rich-text-detection.ts b/src/lib/strings/rich-text-detection.ts
index 386ed48e1..51d09ec5d 100644
--- a/src/lib/strings/rich-text-detection.ts
+++ b/src/lib/strings/rich-text-detection.ts
@@ -1,64 +1,5 @@
-import {AppBskyFeedPost} from '@atproto/api'
-type Entity = AppBskyFeedPost.Entity
 import {isValidDomain} from './url-helpers'
 
-export function extractEntities(
-  text: string,
-  knownHandles?: Set<string>,
-): Entity[] | undefined {
-  let match
-  let ents: Entity[] = []
-  {
-    // mentions
-    const re = /(^|\s|\()(@)([a-zA-Z0-9.-]+)(\b)/g
-    while ((match = re.exec(text))) {
-      if (knownHandles && !knownHandles.has(match[3])) {
-        continue // not a known handle
-      } else if (!match[3].includes('.')) {
-        continue // probably not a handle
-      }
-      const start = text.indexOf(match[3], match.index) - 1
-      ents.push({
-        type: 'mention',
-        value: match[3],
-        index: {start, end: start + match[3].length + 1},
-      })
-    }
-  }
-  {
-    // links
-    const re =
-      /(^|\s|\()((https?:\/\/[\S]+)|((?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/gim
-    while ((match = re.exec(text))) {
-      let value = match[2]
-      if (!value.startsWith('http')) {
-        const domain = match.groups?.domain
-        if (!domain || !isValidDomain(domain)) {
-          continue
-        }
-        value = `https://${value}`
-      }
-      const start = text.indexOf(match[2], match.index)
-      const index = {start, end: start + match[2].length}
-      // strip ending puncuation
-      if (/[.,;!?]$/.test(value)) {
-        value = value.slice(0, -1)
-        index.end--
-      }
-      if (/[)]$/.test(value) && !value.includes('(')) {
-        value = value.slice(0, -1)
-        index.end--
-      }
-      ents.push({
-        type: 'link',
-        value,
-        index,
-      })
-    }
-  }
-  return ents.length > 0 ? ents : undefined
-}
-
 interface DetectedLink {
   link: string
 }
diff --git a/src/lib/strings/rich-text-sanitize.ts b/src/lib/strings/rich-text-sanitize.ts
deleted file mode 100644
index 0b5895707..000000000
--- a/src/lib/strings/rich-text-sanitize.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import {RichText} from './rich-text'
-
-const EXCESS_SPACE_RE = /[\r\n]([\u00AD\u2060\u200D\u200C\u200B\s]*[\r\n]){2,}/
-const REPLACEMENT_STR = '\n\n'
-
-export function removeExcessNewlines(richText: RichText): RichText {
-  return clean(richText, EXCESS_SPACE_RE, REPLACEMENT_STR)
-}
-
-// TODO: check on whether this works correctly with multi-byte codepoints
-export function clean(
-  richText: RichText,
-  targetRegexp: RegExp,
-  replacementString: string,
-): RichText {
-  richText = richText.clone()
-
-  let match = richText.text.match(targetRegexp)
-  while (match && typeof match.index !== 'undefined') {
-    const oldText = richText.text
-    const removeStartIndex = match.index
-    const removeEndIndex = removeStartIndex + match[0].length
-    richText.delete(removeStartIndex, removeEndIndex)
-    if (richText.text === oldText) {
-      break // sanity check
-    }
-    richText.insert(removeStartIndex, replacementString)
-    match = richText.text.match(targetRegexp)
-  }
-
-  return richText
-}
diff --git a/src/lib/strings/rich-text.ts b/src/lib/strings/rich-text.ts
deleted file mode 100644
index 1df2144e0..000000000
--- a/src/lib/strings/rich-text.ts
+++ /dev/null
@@ -1,216 +0,0 @@
-/*
-= Rich Text Manipulation
-
-When we sanitize rich text, we have to update the entity indices as the
-text is modified. This can be modeled as inserts() and deletes() of the
-rich text string. The possible scenarios are outlined below, along with
-their expected behaviors.
-
-NOTE: Slices are start inclusive, end exclusive
-
-== richTextInsert()
-
-Target string:
-
-   0 1 2 3 4 5 6 7 8 910   // string indices
-   h e l l o   w o r l d   // string value
-       ^-------^           // target slice {start: 2, end: 7}
-
-Scenarios:
-
-A: ^                       // insert "test" at 0
-B:        ^                // insert "test" at 4
-C:                 ^       // insert "test" at 8
-
-A = before           -> move both by num added
-B = inner            -> move end by num added
-C = after            -> noop
-
-Results:
-
-A: 0 1 2 3 4 5 6 7 8 910   // string indices
-   t e s t h e l l o   w   // string value
-               ^-------^   // target slice {start: 6, end: 11}
-
-B: 0 1 2 3 4 5 6 7 8 910   // string indices
-   h e l l t e s t o   w   // string value
-       ^---------------^   // target slice {start: 2, end: 11}
-
-C: 0 1 2 3 4 5 6 7 8 910   // string indices
-   h e l l o   w o t e s   // string value
-       ^-------^           // target slice {start: 2, end: 7}
-
-== richTextDelete()
-
-Target string:
-
-   0 1 2 3 4 5 6 7 8 910   // string indices
-   h e l l o   w o r l d   // string value
-       ^-------^           // target slice {start: 2, end: 7}
-
-Scenarios:
-
-A: ^---------------^       // remove slice {start: 0, end: 9}
-B:               ^-----^   // remove slice {start: 7, end: 11}
-C:         ^-----------^   // remove slice {start: 4, end: 11}
-D:       ^-^               // remove slice {start: 3, end: 5}
-E:   ^-----^               // remove slice {start: 1, end: 5}
-F: ^-^                     // remove slice {start: 0, end: 2}
-
-A = entirely outer   -> delete slice
-B = entirely after   -> noop
-C = partially after  -> move end to remove-start
-D = entirely inner   -> move end by num removed
-E = partially before -> move start to remove-start index, move end by num removed
-F = entirely before  -> move both by num removed
-
-Results:
-
-A: 0 1 2 3 4 5 6 7 8 910   // string indices
-   l d                     // string value
-                           // target slice (deleted)
-
-B: 0 1 2 3 4 5 6 7 8 910   // string indices
-   h e l l o   w           // string value
-       ^-------^           // target slice {start: 2, end: 7}
-
-C: 0 1 2 3 4 5 6 7 8 910   // string indices
-   h e l l                 // string value
-       ^-^                 // target slice {start: 2, end: 4}
-
-D: 0 1 2 3 4 5 6 7 8 910   // string indices
-   h e l   w o r l d       // string value
-       ^---^               // target slice {start: 2, end: 5}
-
-E: 0 1 2 3 4 5 6 7 8 910   // string indices
-   h   w o r l d           // string value
-     ^-^                   // target slice {start: 1, end: 3}
-
-F: 0 1 2 3 4 5 6 7 8 910   // string indices
-   l l o   w o r l d       // string value
-   ^-------^               // target slice {start: 0, end: 5}
- */
-
-import cloneDeep from 'lodash.clonedeep'
-import {AppBskyFeedPost} from '@atproto/api'
-import {removeExcessNewlines} from './rich-text-sanitize'
-
-export type Entity = AppBskyFeedPost.Entity
-export interface RichTextOpts {
-  cleanNewlines?: boolean
-}
-
-export class RichText {
-  constructor(
-    public text: string,
-    public entities?: Entity[],
-    opts?: RichTextOpts,
-  ) {
-    if (opts?.cleanNewlines) {
-      removeExcessNewlines(this).copyInto(this)
-    }
-  }
-
-  clone() {
-    return new RichText(this.text, cloneDeep(this.entities))
-  }
-
-  copyInto(target: RichText) {
-    target.text = this.text
-    target.entities = cloneDeep(this.entities)
-  }
-
-  insert(insertIndex: number, insertText: string) {
-    this.text =
-      this.text.slice(0, insertIndex) +
-      insertText +
-      this.text.slice(insertIndex)
-
-    if (!this.entities?.length) {
-      return this
-    }
-
-    const numCharsAdded = insertText.length
-    for (const ent of this.entities) {
-      // see comment at top of file for labels of each scenario
-      // scenario A (before)
-      if (insertIndex <= ent.index.start) {
-        // move both by num added
-        ent.index.start += numCharsAdded
-        ent.index.end += numCharsAdded
-      }
-      // scenario B (inner)
-      else if (insertIndex >= ent.index.start && insertIndex < ent.index.end) {
-        // move end by num added
-        ent.index.end += numCharsAdded
-      }
-      // scenario C (after)
-      // noop
-    }
-    return this
-  }
-
-  delete(removeStartIndex: number, removeEndIndex: number) {
-    this.text =
-      this.text.slice(0, removeStartIndex) + this.text.slice(removeEndIndex)
-
-    if (!this.entities?.length) {
-      return this
-    }
-
-    const numCharsRemoved = removeEndIndex - removeStartIndex
-    for (const ent of this.entities) {
-      // see comment at top of file for labels of each scenario
-      // scenario A (entirely outer)
-      if (
-        removeStartIndex <= ent.index.start &&
-        removeEndIndex >= ent.index.end
-      ) {
-        // delete slice (will get removed in final pass)
-        ent.index.start = 0
-        ent.index.end = 0
-      }
-      // scenario B (entirely after)
-      else if (removeStartIndex > ent.index.end) {
-        // noop
-      }
-      // scenario C (partially after)
-      else if (
-        removeStartIndex > ent.index.start &&
-        removeStartIndex <= ent.index.end &&
-        removeEndIndex > ent.index.end
-      ) {
-        // move end to remove start
-        ent.index.end = removeStartIndex
-      }
-      // scenario D (entirely inner)
-      else if (
-        removeStartIndex >= ent.index.start &&
-        removeEndIndex <= ent.index.end
-      ) {
-        // move end by num removed
-        ent.index.end -= numCharsRemoved
-      }
-      // scenario E (partially before)
-      else if (
-        removeStartIndex < ent.index.start &&
-        removeEndIndex >= ent.index.start &&
-        removeEndIndex <= ent.index.end
-      ) {
-        // move start to remove-start index, move end by num removed
-        ent.index.start = removeStartIndex
-        ent.index.end -= numCharsRemoved
-      }
-      // scenario F (entirely before)
-      else if (removeEndIndex < ent.index.start) {
-        // move both by num removed
-        ent.index.start -= numCharsRemoved
-        ent.index.end -= numCharsRemoved
-      }
-    }
-
-    // filter out any entities that were made irrelevant
-    this.entities = this.entities.filter(ent => ent.index.start < ent.index.end)
-    return this
-  }
-}
diff --git a/src/lib/styles.ts b/src/lib/styles.ts
index aa255b21f..409c77548 100644
--- a/src/lib/styles.ts
+++ b/src/lib/styles.ts
@@ -71,6 +71,7 @@ export const s = StyleSheet.create({
   borderBottom1: {borderBottomWidth: 1},
   borderLeft1: {borderLeftWidth: 1},
   hidden: {display: 'none'},
+  dimmed: {opacity: 0.5},
 
   // font weights
   fw600: {fontWeight: '600'},
diff --git a/src/platform/polyfills.ts b/src/platform/polyfills.ts
index 3dbd13981..a64c2c33a 100644
--- a/src/platform/polyfills.ts
+++ b/src/platform/polyfills.ts
@@ -1,3 +1,5 @@
+import 'fast-text-encoding'
+import Graphemer from 'graphemer'
 export {}
 
 /**
@@ -48,3 +50,18 @@ globalThis.atob = (str: string): string => {
   }
   return result
 }
+
+const splitter = new Graphemer()
+globalThis.Intl = globalThis.Intl || {}
+
+// @ts-ignore we're polyfilling -prf
+globalThis.Intl.Segmenter =
+  // @ts-ignore we're polyfilling -prf
+  globalThis.Intl.Segmenter ||
+  class Segmenter {
+    constructor() {}
+    // NOTE
+    // this is not a precisely correct polyfill but it's sufficient for our needs
+    // -prf
+    segment = splitter.iterateGraphemes
+  }
diff --git a/src/platform/polyfills.web.ts b/src/platform/polyfills.web.ts
index 7a42f4887..e46963a6f 100644
--- a/src/platform/polyfills.web.ts
+++ b/src/platform/polyfills.web.ts
@@ -2,3 +2,11 @@
 
 // @ts-ignore whatever typescript wants to complain about here, I dont care about -prf
 window.setImmediate = (cb: () => void) => setTimeout(cb, 0)
+
+// @ts-ignore not on the TS signature due to bad support -prf
+if (!globalThis.Intl?.Segmenter) {
+  // NOTE loading as a separate script to reduce main bundle size, as this is only needed in FF -prf
+  const script = document.createElement('script')
+  script.setAttribute('src', '/static/js/intl-segmenter-polyfill.min.js')
+  document.head.appendChild(script)
+}
diff --git a/src/routes.ts b/src/routes.ts
index 6c02a7c50..167efcfb7 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -9,7 +9,7 @@ export const router = new Router({
   ProfileFollowers: '/profile/:name/followers',
   ProfileFollows: '/profile/:name/follows',
   PostThread: '/profile/:name/post/:rkey',
-  PostUpvotedBy: '/profile/:name/post/:rkey/upvoted-by',
+  PostLikedBy: '/profile/:name/post/:rkey/liked-by',
   PostRepostedBy: '/profile/:name/post/:rkey/reposted-by',
   Debug: '/sys/debug',
   Log: '/sys/log',
diff --git a/src/state/index.ts b/src/state/index.ts
index f0713efeb..4755c28f4 100644
--- a/src/state/index.ts
+++ b/src/state/index.ts
@@ -1,6 +1,6 @@
 import {autorun} from 'mobx'
 import {AppState, Platform} from 'react-native'
-import {AtpAgent} from '@atproto/api'
+import {BskyAgent} from '@atproto/api'
 import {RootStoreModel} from './models/root-store'
 import * as apiPolyfill from 'lib/api/api-polyfill'
 import * as storage from 'lib/storage'
@@ -19,7 +19,7 @@ export async function setupState(serviceUri = DEFAULT_SERVICE) {
 
   apiPolyfill.doPolyfill()
 
-  rootStore = new RootStoreModel(new AtpAgent({service: serviceUri}))
+  rootStore = new RootStoreModel(new BskyAgent({service: serviceUri}))
   try {
     data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {}
     rootStore.log.debug('Initial hydrate', {hasSession: !!data.session})
diff --git a/src/state/models/cache/image-sizes.ts b/src/state/models/cache/image-sizes.ts
index ff0486278..2fd6e0013 100644
--- a/src/state/models/cache/image-sizes.ts
+++ b/src/state/models/cache/image-sizes.ts
@@ -3,7 +3,7 @@ import {Dim} from 'lib/media/manip'
 
 export class ImageSizesCache {
   sizes: Map<string, Dim> = new Map()
-  private activeRequests: Map<string, Promise<Dim>> = new Map()
+  activeRequests: Map<string, Promise<Dim>> = new Map()
 
   constructor() {}
 
diff --git a/src/state/models/cache/my-follows.ts b/src/state/models/cache/my-follows.ts
index 725b7841e..14eeaae21 100644
--- a/src/state/models/cache/my-follows.ts
+++ b/src/state/models/cache/my-follows.ts
@@ -1,15 +1,12 @@
 import {makeAutoObservable, runInAction} from 'mobx'
-import {FollowRecord, AppBskyActorProfile, AppBskyActorRef} from '@atproto/api'
+import {FollowRecord, AppBskyActorDefs} from '@atproto/api'
 import {RootStoreModel} from '../root-store'
 import {bundleAsync} from 'lib/async/bundle'
 
 const CACHE_TTL = 1000 * 60 * 60 // hourly
 type FollowsListResponse = Awaited<ReturnType<FollowRecord['list']>>
 type FollowsListResponseRecord = FollowsListResponse['records'][0]
-type Profile =
-  | AppBskyActorProfile.ViewBasic
-  | AppBskyActorProfile.View
-  | AppBskyActorRef.WithInfo
+type Profile = AppBskyActorDefs.ProfileViewBasic | AppBskyActorDefs.ProfileView
 
 /**
  * This model is used to maintain a synced local cache of the user's
@@ -53,21 +50,21 @@ export class MyFollowsCache {
 
   fetch = bundleAsync(async () => {
     this.rootStore.log.debug('MyFollowsModel:fetch running full fetch')
-    let before
+    let rkeyStart
     let records: FollowsListResponseRecord[] = []
     do {
       const res: FollowsListResponse =
-        await this.rootStore.api.app.bsky.graph.follow.list({
-          user: this.rootStore.me.did,
-          before,
+        await this.rootStore.agent.app.bsky.graph.follow.list({
+          repo: this.rootStore.me.did,
+          rkeyStart,
         })
       records = records.concat(res.records)
-      before = res.cursor
-    } while (typeof before !== 'undefined')
+      rkeyStart = res.cursor
+    } while (typeof rkeyStart !== 'undefined')
     runInAction(() => {
       this.followDidToRecordMap = {}
       for (const record of records) {
-        this.followDidToRecordMap[record.value.subject.did] = record.uri
+        this.followDidToRecordMap[record.value.subject] = record.uri
       }
       this.lastSync = Date.now()
       this.myDid = this.rootStore.me.did
diff --git a/src/state/models/discovery/foafs.ts b/src/state/models/discovery/foafs.ts
index 241338a16..27cee8503 100644
--- a/src/state/models/discovery/foafs.ts
+++ b/src/state/models/discovery/foafs.ts
@@ -1,15 +1,15 @@
-import {AppBskyActorProfile, AppBskyActorRef} from '@atproto/api'
+import {AppBskyActorDefs} from '@atproto/api'
 import {makeAutoObservable, runInAction} from 'mobx'
 import sampleSize from 'lodash.samplesize'
 import {bundleAsync} from 'lib/async/bundle'
 import {RootStoreModel} from '../root-store'
 
-export type RefWithInfoAndFollowers = AppBskyActorRef.WithInfo & {
-  followers: AppBskyActorProfile.View[]
+export type RefWithInfoAndFollowers = AppBskyActorDefs.ProfileViewBasic & {
+  followers: AppBskyActorDefs.ProfileView[]
 }
 
-export type ProfileViewFollows = AppBskyActorProfile.View & {
-  follows: AppBskyActorRef.WithInfo[]
+export type ProfileViewFollows = AppBskyActorDefs.ProfileView & {
+  follows: AppBskyActorDefs.ProfileViewBasic[]
 }
 
 export class FoafsModel {
@@ -51,14 +51,14 @@ export class FoafsModel {
       this.popular.length = 0
 
       // fetch their profiles
-      const profiles = await this.rootStore.api.app.bsky.actor.getProfiles({
+      const profiles = await this.rootStore.agent.getProfiles({
         actors: this.sources,
       })
 
       // fetch their follows
       const results = await Promise.allSettled(
         this.sources.map(source =>
-          this.rootStore.api.app.bsky.graph.getFollows({user: source}),
+          this.rootStore.agent.getFollows({actor: source}),
         ),
       )
 
diff --git a/src/state/models/discovery/suggested-actors.ts b/src/state/models/discovery/suggested-actors.ts
index cf8e2dd7b..91c5efd02 100644
--- a/src/state/models/discovery/suggested-actors.ts
+++ b/src/state/models/discovery/suggested-actors.ts
@@ -1,5 +1,5 @@
 import {makeAutoObservable, runInAction} from 'mobx'
-import {AppBskyActorProfile as Profile} from '@atproto/api'
+import {AppBskyActorDefs} from '@atproto/api'
 import shuffle from 'lodash.shuffle'
 import {RootStoreModel} from '../root-store'
 import {cleanError} from 'lib/strings/errors'
@@ -8,7 +8,9 @@ import {SUGGESTED_FOLLOWS} from 'lib/constants'
 
 const PAGE_SIZE = 30
 
-export type SuggestedActor = Profile.ViewBasic | Profile.View
+export type SuggestedActor =
+  | AppBskyActorDefs.ProfileViewBasic
+  | AppBskyActorDefs.ProfileView
 
 export class SuggestedActorsModel {
   // state
@@ -20,7 +22,7 @@ export class SuggestedActorsModel {
   hasMore = true
   loadMoreCursor?: string
 
-  private hardCodedSuggestions: SuggestedActor[] | undefined
+  hardCodedSuggestions: SuggestedActor[] | undefined
 
   // data
   suggestions: SuggestedActor[] = []
@@ -82,7 +84,7 @@ export class SuggestedActorsModel {
           this.loadMoreCursor = undefined
         } else {
           // pull from the PDS' algo
-          res = await this.rootStore.api.app.bsky.actor.getSuggestions({
+          res = await this.rootStore.agent.app.bsky.actor.getSuggestions({
             limit: this.pageSize,
             cursor: this.loadMoreCursor,
           })
@@ -104,7 +106,7 @@ export class SuggestedActorsModel {
     }
   })
 
-  private async fetchHardcodedSuggestions() {
+  async fetchHardcodedSuggestions() {
     if (this.hardCodedSuggestions) {
       return
     }
@@ -118,9 +120,9 @@ export class SuggestedActorsModel {
       ]
 
       // fetch the profiles in chunks of 25 (the limit allowed by `getProfiles`)
-      let profiles: Profile.View[] = []
+      let profiles: AppBskyActorDefs.ProfileView[] = []
       do {
-        const res = await this.rootStore.api.app.bsky.actor.getProfiles({
+        const res = await this.rootStore.agent.getProfiles({
           actors: actors.splice(0, 25),
         })
         profiles = profiles.concat(res.data.profiles)
@@ -152,13 +154,13 @@ export class SuggestedActorsModel {
   // state transitions
   // =
 
-  private _xLoading(isRefreshing = false) {
+  _xLoading(isRefreshing = false) {
     this.isLoading = true
     this.isRefreshing = isRefreshing
     this.error = ''
   }
 
-  private _xIdle(err?: any) {
+  _xIdle(err?: any) {
     this.isLoading = false
     this.isRefreshing = false
     this.hasLoaded = true
diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts
index 083863fe2..8b62c958f 100644
--- a/src/state/models/feed-view.ts
+++ b/src/state/models/feed-view.ts
@@ -1,32 +1,29 @@
 import {makeAutoObservable, runInAction} from 'mobx'
 import {
   AppBskyFeedGetTimeline as GetTimeline,
-  AppBskyFeedFeedViewPost,
+  AppBskyFeedDefs,
   AppBskyFeedPost,
   AppBskyFeedGetAuthorFeed as GetAuthorFeed,
+  RichText,
 } from '@atproto/api'
 import AwaitLock from 'await-lock'
 import {bundleAsync} from 'lib/async/bundle'
 import sampleSize from 'lodash.samplesize'
-type FeedViewPost = AppBskyFeedFeedViewPost.Main
-type ReasonRepost = AppBskyFeedFeedViewPost.ReasonRepost
-type PostView = AppBskyFeedPost.View
-import {AtUri} from '../../third-party/uri'
 import {RootStoreModel} from './root-store'
-import * as apilib from 'lib/api/index'
 import {cleanError} from 'lib/strings/errors'
-import {RichText} from 'lib/strings/rich-text'
 import {SUGGESTED_FOLLOWS} from 'lib/constants'
 import {
   getCombinedCursors,
   getMultipleAuthorsPosts,
   mergePosts,
 } from 'lib/api/build-suggested-posts'
-
 import {FeedTuner, FeedViewPostsSlice} from 'lib/api/feed-manip'
 
-const PAGE_SIZE = 30
+type FeedViewPost = AppBskyFeedDefs.FeedViewPost
+type ReasonRepost = AppBskyFeedDefs.ReasonRepost
+type PostView = AppBskyFeedDefs.PostView
 
+const PAGE_SIZE = 30
 let _idCounter = 0
 
 export class FeedItemModel {
@@ -51,11 +48,7 @@ export class FeedItemModel {
       const valid = AppBskyFeedPost.validateRecord(this.post.record)
       if (valid.success) {
         this.postRecord = this.post.record
-        this.richText = new RichText(
-          this.postRecord.text,
-          this.postRecord.entities,
-          {cleanNewlines: true},
-        )
+        this.richText = new RichText(this.postRecord, {cleanNewlines: true})
       } else {
         rootStore.log.warn(
           'Received an invalid app.bsky.feed.post record',
@@ -82,7 +75,7 @@ export class FeedItemModel {
   copyMetrics(v: FeedViewPost) {
     this.post.replyCount = v.post.replyCount
     this.post.repostCount = v.post.repostCount
-    this.post.upvoteCount = v.post.upvoteCount
+    this.post.likeCount = v.post.likeCount
     this.post.viewer = v.post.viewer
   }
 
@@ -92,68 +85,43 @@ export class FeedItemModel {
     }
   }
 
-  async toggleUpvote() {
-    const wasUpvoted = !!this.post.viewer.upvote
-    const wasDownvoted = !!this.post.viewer.downvote
-    const res = await this.rootStore.api.app.bsky.feed.setVote({
-      subject: {
-        uri: this.post.uri,
-        cid: this.post.cid,
-      },
-      direction: wasUpvoted ? 'none' : 'up',
-    })
-    runInAction(() => {
-      if (wasDownvoted) {
-        this.post.downvoteCount--
-      }
-      if (wasUpvoted) {
-        this.post.upvoteCount--
-      } else {
-        this.post.upvoteCount++
-      }
-      this.post.viewer.upvote = res.data.upvote
-      this.post.viewer.downvote = res.data.downvote
-    })
-  }
-
-  async toggleDownvote() {
-    const wasUpvoted = !!this.post.viewer.upvote
-    const wasDownvoted = !!this.post.viewer.downvote
-    const res = await this.rootStore.api.app.bsky.feed.setVote({
-      subject: {
-        uri: this.post.uri,
-        cid: this.post.cid,
-      },
-      direction: wasDownvoted ? 'none' : 'down',
-    })
-    runInAction(() => {
-      if (wasUpvoted) {
-        this.post.upvoteCount--
-      }
-      if (wasDownvoted) {
-        this.post.downvoteCount--
-      } else {
-        this.post.downvoteCount++
-      }
-      this.post.viewer.upvote = res.data.upvote
-      this.post.viewer.downvote = res.data.downvote
-    })
+  async toggleLike() {
+    if (this.post.viewer?.like) {
+      await this.rootStore.agent.deleteLike(this.post.viewer.like)
+      runInAction(() => {
+        this.post.likeCount = this.post.likeCount || 0
+        this.post.viewer = this.post.viewer || {}
+        this.post.likeCount--
+        this.post.viewer.like = undefined
+      })
+    } else {
+      const res = await this.rootStore.agent.like(this.post.uri, this.post.cid)
+      runInAction(() => {
+        this.post.likeCount = this.post.likeCount || 0
+        this.post.viewer = this.post.viewer || {}
+        this.post.likeCount++
+        this.post.viewer.like = res.uri
+      })
+    }
   }
 
   async toggleRepost() {
-    if (this.post.viewer.repost) {
-      await apilib.unrepost(this.rootStore, this.post.viewer.repost)
+    if (this.post.viewer?.repost) {
+      await this.rootStore.agent.deleteRepost(this.post.viewer.repost)
       runInAction(() => {
+        this.post.repostCount = this.post.repostCount || 0
+        this.post.viewer = this.post.viewer || {}
         this.post.repostCount--
         this.post.viewer.repost = undefined
       })
     } else {
-      const res = await apilib.repost(
-        this.rootStore,
+      const res = await this.rootStore.agent.repost(
         this.post.uri,
         this.post.cid,
       )
       runInAction(() => {
+        this.post.repostCount = this.post.repostCount || 0
+        this.post.viewer = this.post.viewer || {}
         this.post.repostCount++
         this.post.viewer.repost = res.uri
       })
@@ -161,10 +129,7 @@ export class FeedItemModel {
   }
 
   async delete() {
-    await this.rootStore.api.app.bsky.feed.post.delete({
-      did: this.post.author.did,
-      rkey: new AtUri(this.post.uri).rkey,
-    })
+    await this.rootStore.agent.deletePost(this.post.uri)
     this.rootStore.emitPostDeleted(this.post.uri)
   }
 }
@@ -250,7 +215,7 @@ export class FeedModel {
   tuner = new FeedTuner()
 
   // used to linearize async modifications to state
-  private lock = new AwaitLock()
+  lock = new AwaitLock()
 
   // data
   slices: FeedSliceModel[] = []
@@ -291,8 +256,8 @@ export class FeedModel {
         const params = this.params as GetAuthorFeed.QueryParams
         const item = slice.rootItem
         const isRepost =
-          item?.reasonRepost?.by?.handle === params.author ||
-          item?.reasonRepost?.by?.did === params.author
+          item?.reasonRepost?.by?.handle === params.actor ||
+          item?.reasonRepost?.by?.did === params.actor
         return (
           !item.reply || // not a reply
           isRepost || // but allow if it's a repost
@@ -338,7 +303,7 @@ export class FeedModel {
     return this.setup()
   }
 
-  private get feedTuners() {
+  get feedTuners() {
     if (this.feedType === 'goodstuff') {
       return [
         FeedTuner.dedupReposts,
@@ -406,7 +371,7 @@ export class FeedModel {
       this._xLoading()
       try {
         const res = await this._getFeed({
-          before: this.loadMoreCursor,
+          cursor: this.loadMoreCursor,
           limit: PAGE_SIZE,
         })
         await this._appendAll(res)
@@ -439,7 +404,7 @@ export class FeedModel {
       try {
         do {
           const res: GetTimeline.Response = await this._getFeed({
-            before: cursor,
+            cursor,
             limit: Math.min(numToFetch, 100),
           })
           if (res.data.feed.length === 0) {
@@ -478,14 +443,18 @@ export class FeedModel {
           new FeedSliceModel(this.rootStore, `item-${_idCounter++}`, slice),
       )
       if (autoPrepend) {
-        this.slices = nextSlicesModels.concat(
-          this.slices.filter(slice1 =>
-            nextSlicesModels.find(slice2 => slice1.uri === slice2.uri),
-          ),
-        )
-        this.setHasNewLatest(false)
+        runInAction(() => {
+          this.slices = nextSlicesModels.concat(
+            this.slices.filter(slice1 =>
+              nextSlicesModels.find(slice2 => slice1.uri === slice2.uri),
+            ),
+          )
+          this.setHasNewLatest(false)
+        })
       } else {
-        this.nextSlices = nextSlicesModels
+        runInAction(() => {
+          this.nextSlices = nextSlicesModels
+        })
         this.setHasNewLatest(true)
       }
     } else {
@@ -519,13 +488,13 @@ export class FeedModel {
   // state transitions
   // =
 
-  private _xLoading(isRefreshing = false) {
+  _xLoading(isRefreshing = false) {
     this.isLoading = true
     this.isRefreshing = isRefreshing
     this.error = ''
   }
 
-  private _xIdle(err?: any) {
+  _xIdle(err?: any) {
     this.isLoading = false
     this.isRefreshing = false
     this.hasLoaded = true
@@ -538,14 +507,12 @@ export class FeedModel {
   // helper functions
   // =
 
-  private async _replaceAll(
-    res: GetTimeline.Response | GetAuthorFeed.Response,
-  ) {
+  async _replaceAll(res: GetTimeline.Response | GetAuthorFeed.Response) {
     this.pollCursor = res.data.feed[0]?.post.uri
     return this._appendAll(res, true)
   }
 
-  private async _appendAll(
+  async _appendAll(
     res: GetTimeline.Response | GetAuthorFeed.Response,
     replace = false,
   ) {
@@ -572,7 +539,7 @@ export class FeedModel {
     })
   }
 
-  private _updateAll(res: GetTimeline.Response | GetAuthorFeed.Response) {
+  _updateAll(res: GetTimeline.Response | GetAuthorFeed.Response) {
     for (const item of res.data.feed) {
       const existingSlice = this.slices.find(slice =>
         slice.containsUri(item.post.uri),
@@ -596,7 +563,7 @@ export class FeedModel {
       const responses = await getMultipleAuthorsPosts(
         this.rootStore,
         sampleSize(SUGGESTED_FOLLOWS(String(this.rootStore.agent.service)), 20),
-        params.before,
+        params.cursor,
         20,
       )
       const combinedCursor = getCombinedCursors(responses)
@@ -611,9 +578,7 @@ export class FeedModel {
         headers: lastHeaders,
       }
     } else if (this.feedType === 'home') {
-      return this.rootStore.api.app.bsky.feed.getTimeline(
-        params as GetTimeline.QueryParams,
-      )
+      return this.rootStore.agent.getTimeline(params as GetTimeline.QueryParams)
     } else if (this.feedType === 'goodstuff') {
       const res = await getGoodStuff(
         this.rootStore.session.currentSession?.accessJwt || '',
@@ -624,7 +589,7 @@ export class FeedModel {
       )
       return res
     } else {
-      return this.rootStore.api.app.bsky.feed.getAuthorFeed(
+      return this.rootStore.agent.getAuthorFeed(
         params as GetAuthorFeed.QueryParams,
       )
     }
diff --git a/src/state/models/votes-view.ts b/src/state/models/likes-view.ts
index ad8698d21..5f9df692e 100644
--- a/src/state/models/votes-view.ts
+++ b/src/state/models/likes-view.ts
@@ -1,6 +1,6 @@
 import {makeAutoObservable, runInAction} from 'mobx'
 import {AtUri} from '../../third-party/uri'
-import {AppBskyFeedGetVotes as GetVotes} from '@atproto/api'
+import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
 import {RootStoreModel} from './root-store'
 import {cleanError} from 'lib/strings/errors'
 import {bundleAsync} from 'lib/async/bundle'
@@ -8,24 +8,24 @@ import * as apilib from 'lib/api/index'
 
 const PAGE_SIZE = 30
 
-export type VoteItem = GetVotes.Vote
+export type LikeItem = GetLikes.Like
 
-export class VotesViewModel {
+export class LikesViewModel {
   // state
   isLoading = false
   isRefreshing = false
   hasLoaded = false
   error = ''
   resolvedUri = ''
-  params: GetVotes.QueryParams
+  params: GetLikes.QueryParams
   hasMore = true
   loadMoreCursor?: string
 
   // data
   uri: string = ''
-  votes: VoteItem[] = []
+  likes: LikeItem[] = []
 
-  constructor(public rootStore: RootStoreModel, params: GetVotes.QueryParams) {
+  constructor(public rootStore: RootStoreModel, params: GetLikes.QueryParams) {
     makeAutoObservable(
       this,
       {
@@ -68,9 +68,9 @@ export class VotesViewModel {
       const params = Object.assign({}, this.params, {
         uri: this.resolvedUri,
         limit: PAGE_SIZE,
-        before: replace ? undefined : this.loadMoreCursor,
+        cursor: replace ? undefined : this.loadMoreCursor,
       })
-      const res = await this.rootStore.api.app.bsky.feed.getVotes(params)
+      const res = await this.rootStore.agent.getLikes(params)
       if (replace) {
         this._replaceAll(res)
       } else {
@@ -85,13 +85,13 @@ export class VotesViewModel {
   // state transitions
   // =
 
-  private _xLoading(isRefreshing = false) {
+  _xLoading(isRefreshing = false) {
     this.isLoading = true
     this.isRefreshing = isRefreshing
     this.error = ''
   }
 
-  private _xIdle(err?: any) {
+  _xIdle(err?: any) {
     this.isLoading = false
     this.isRefreshing = false
     this.hasLoaded = true
@@ -104,7 +104,7 @@ export class VotesViewModel {
   // helper functions
   // =
 
-  private async _resolveUri() {
+  async _resolveUri() {
     const urip = new AtUri(this.params.uri)
     if (!urip.host.startsWith('did:')) {
       try {
@@ -118,14 +118,14 @@ export class VotesViewModel {
     })
   }
 
-  private _replaceAll(res: GetVotes.Response) {
-    this.votes = []
+  _replaceAll(res: GetLikes.Response) {
+    this.likes = []
     this._appendAll(res)
   }
 
-  private _appendAll(res: GetVotes.Response) {
+  _appendAll(res: GetLikes.Response) {
     this.loadMoreCursor = res.data.cursor
     this.hasMore = !!this.loadMoreCursor
-    this.votes = this.votes.concat(res.data.votes)
+    this.likes = this.likes.concat(res.data.likes)
   }
 }
diff --git a/src/state/models/log.ts b/src/state/models/log.ts
index ed701dc61..d80617139 100644
--- a/src/state/models/log.ts
+++ b/src/state/models/log.ts
@@ -1,5 +1,5 @@
 import {makeAutoObservable} from 'mobx'
-import {XRPCError, XRPCInvalidResponseError} from '@atproto/xrpc'
+// import {XRPCError, XRPCInvalidResponseError} from '@atproto/xrpc' TODO
 
 const MAX_ENTRIES = 300
 
@@ -32,7 +32,7 @@ export class LogModel {
     makeAutoObservable(this)
   }
 
-  private add(entry: LogEntry) {
+  add(entry: LogEntry) {
     this.entries.push(entry)
     while (this.entries.length > MAX_ENTRIES) {
       this.entries = this.entries.slice(50)
@@ -79,14 +79,14 @@ export class LogModel {
 function detailsToStr(details?: any) {
   if (details && typeof details !== 'string') {
     if (
-      details instanceof XRPCInvalidResponseError ||
+      // details instanceof XRPCInvalidResponseError || TODO
       details.constructor.name === 'XRPCInvalidResponseError'
     ) {
       return `The server gave an ill-formatted response.\nMethod: ${
         details.lexiconNsid
       }.\nError: ${details.validationError.toString()}`
     } else if (
-      details instanceof XRPCError ||
+      // details instanceof XRPCError || TODO
       details.constructor.name === 'XRPCError'
     ) {
       return `An XRPC error occurred.\nStatus: ${details.status}\nError: ${details.error}\nMessage: ${details.message}`
diff --git a/src/state/models/me.ts b/src/state/models/me.ts
index 120749155..5f670b8f9 100644
--- a/src/state/models/me.ts
+++ b/src/state/models/me.ts
@@ -85,7 +85,7 @@ export class MeModel {
     if (sess.hasSession) {
       this.did = sess.currentSession?.did || ''
       this.handle = sess.currentSession?.handle || ''
-      const profile = await this.rootStore.api.app.bsky.actor.getProfile({
+      const profile = await this.rootStore.agent.getProfile({
         actor: this.did,
       })
       runInAction(() => {
diff --git a/src/state/models/notifications-view.ts b/src/state/models/notifications-view.ts
index e88af590b..4f7a52fd9 100644
--- a/src/state/models/notifications-view.ts
+++ b/src/state/models/notifications-view.ts
@@ -1,11 +1,10 @@
 import {makeAutoObservable, runInAction} from 'mobx'
 import {
-  AppBskyNotificationList as ListNotifications,
-  AppBskyActorRef as ActorRef,
+  AppBskyNotificationListNotifications as ListNotifications,
+  AppBskyActorDefs,
   AppBskyFeedPost,
   AppBskyFeedRepost,
-  AppBskyFeedVote,
-  AppBskyGraphAssertion,
+  AppBskyFeedLike,
   AppBskyGraphFollow,
 } from '@atproto/api'
 import AwaitLock from 'await-lock'
@@ -28,8 +27,7 @@ export interface GroupedNotification extends ListNotifications.Notification {
 type SupportedRecord =
   | AppBskyFeedPost.Record
   | AppBskyFeedRepost.Record
-  | AppBskyFeedVote.Record
-  | AppBskyGraphAssertion.Record
+  | AppBskyFeedLike.Record
   | AppBskyGraphFollow.Record
 
 export class NotificationsViewItemModel {
@@ -39,11 +37,10 @@ export class NotificationsViewItemModel {
   // data
   uri: string = ''
   cid: string = ''
-  author: ActorRef.WithInfo = {
+  author: AppBskyActorDefs.ProfileViewBasic = {
     did: '',
     handle: '',
     avatar: '',
-    declaration: {cid: '', actorType: ''},
   }
   reason: string = ''
   reasonSubject?: string
@@ -86,8 +83,8 @@ export class NotificationsViewItemModel {
     }
   }
 
-  get isUpvote() {
-    return this.reason === 'vote'
+  get isLike() {
+    return this.reason === 'like'
   }
 
   get isRepost() {
@@ -102,16 +99,22 @@ export class NotificationsViewItemModel {
     return this.reason === 'reply'
   }
 
-  get isFollow() {
-    return this.reason === 'follow'
+  get isQuote() {
+    return this.reason === 'quote'
   }
 
-  get isAssertion() {
-    return this.reason === 'assertion'
+  get isFollow() {
+    return this.reason === 'follow'
   }
 
   get needsAdditionalData() {
-    if (this.isUpvote || this.isRepost || this.isReply || this.isMention) {
+    if (
+      this.isLike ||
+      this.isRepost ||
+      this.isReply ||
+      this.isQuote ||
+      this.isMention
+    ) {
       return !this.additionalPost
     }
     return false
@@ -124,7 +127,7 @@ export class NotificationsViewItemModel {
     const record = this.record
     if (
       AppBskyFeedRepost.isRecord(record) ||
-      AppBskyFeedVote.isRecord(record)
+      AppBskyFeedLike.isRecord(record)
     ) {
       return record.subject.uri
     }
@@ -135,8 +138,7 @@ export class NotificationsViewItemModel {
     for (const ns of [
       AppBskyFeedPost,
       AppBskyFeedRepost,
-      AppBskyFeedVote,
-      AppBskyGraphAssertion,
+      AppBskyFeedLike,
       AppBskyGraphFollow,
     ]) {
       if (ns.isRecord(v)) {
@@ -163,9 +165,9 @@ export class NotificationsViewItemModel {
       return
     }
     let postUri
-    if (this.isReply || this.isMention) {
+    if (this.isReply || this.isQuote || this.isMention) {
       postUri = this.uri
-    } else if (this.isUpvote || this.isRepost) {
+    } else if (this.isLike || this.isRepost) {
       postUri = this.subjectUri
     }
     if (postUri) {
@@ -194,7 +196,7 @@ export class NotificationsViewModel {
   loadMoreCursor?: string
 
   // used to linearize async modifications to state
-  private lock = new AwaitLock()
+  lock = new AwaitLock()
 
   // data
   notifications: NotificationsViewItemModel[] = []
@@ -266,7 +268,7 @@ export class NotificationsViewModel {
         const params = Object.assign({}, this.params, {
           limit: PAGE_SIZE,
         })
-        const res = await this.rootStore.api.app.bsky.notification.list(params)
+        const res = await this.rootStore.agent.listNotifications(params)
         await this._replaceAll(res)
         this._xIdle()
       } catch (e: any) {
@@ -297,9 +299,9 @@ export class NotificationsViewModel {
       try {
         const params = Object.assign({}, this.params, {
           limit: PAGE_SIZE,
-          before: this.loadMoreCursor,
+          cursor: this.loadMoreCursor,
         })
-        const res = await this.rootStore.api.app.bsky.notification.list(params)
+        const res = await this.rootStore.agent.listNotifications(params)
         await this._appendAll(res)
         this._xIdle()
       } catch (e: any) {
@@ -325,7 +327,7 @@ export class NotificationsViewModel {
     try {
       this._xLoading()
       try {
-        const res = await this.rootStore.api.app.bsky.notification.list({
+        const res = await this.rootStore.agent.listNotifications({
           limit: PAGE_SIZE,
         })
         await this._prependAll(res)
@@ -357,8 +359,8 @@ export class NotificationsViewModel {
       try {
         do {
           const res: ListNotifications.Response =
-            await this.rootStore.api.app.bsky.notification.list({
-              before: cursor,
+            await this.rootStore.agent.listNotifications({
+              cursor,
               limit: Math.min(numToFetch, 100),
             })
           if (res.data.notifications.length === 0) {
@@ -390,7 +392,7 @@ export class NotificationsViewModel {
    */
   loadUnreadCount = bundleAsync(async () => {
     const old = this.unreadCount
-    const res = await this.rootStore.api.app.bsky.notification.getCount()
+    const res = await this.rootStore.agent.countUnreadNotifications()
     runInAction(() => {
       this.unreadCount = res.data.count
     })
@@ -408,9 +410,7 @@ export class NotificationsViewModel {
       for (const notif of this.notifications) {
         notif.isRead = true
       }
-      await this.rootStore.api.app.bsky.notification.updateSeen({
-        seenAt: new Date().toISOString(),
-      })
+      await this.rootStore.agent.updateSeenNotifications()
     } catch (e: any) {
       this.rootStore.log.warn('Failed to update notifications read state', e)
     }
@@ -418,7 +418,7 @@ export class NotificationsViewModel {
 
   async getNewMostRecent(): Promise<NotificationsViewItemModel | undefined> {
     let old = this.mostRecentNotificationUri
-    const res = await this.rootStore.api.app.bsky.notification.list({
+    const res = await this.rootStore.agent.listNotifications({
       limit: 1,
     })
     if (!res.data.notifications[0] || old === res.data.notifications[0].uri) {
@@ -437,13 +437,13 @@ export class NotificationsViewModel {
   // state transitions
   // =
 
-  private _xLoading(isRefreshing = false) {
+  _xLoading(isRefreshing = false) {
     this.isLoading = true
     this.isRefreshing = isRefreshing
     this.error = ''
   }
 
-  private _xIdle(err?: any) {
+  _xIdle(err?: any) {
     this.isLoading = false
     this.isRefreshing = false
     this.hasLoaded = true
@@ -456,14 +456,14 @@ export class NotificationsViewModel {
   // helper functions
   // =
 
-  private async _replaceAll(res: ListNotifications.Response) {
+  async _replaceAll(res: ListNotifications.Response) {
     if (res.data.notifications[0]) {
       this.mostRecentNotificationUri = res.data.notifications[0].uri
     }
     return this._appendAll(res, true)
   }
 
-  private async _appendAll(res: ListNotifications.Response, replace = false) {
+  async _appendAll(res: ListNotifications.Response, replace = false) {
     this.loadMoreCursor = res.data.cursor
     this.hasMore = !!this.loadMoreCursor
     const promises = []
@@ -494,7 +494,7 @@ export class NotificationsViewModel {
     })
   }
 
-  private async _prependAll(res: ListNotifications.Response) {
+  async _prependAll(res: ListNotifications.Response) {
     const promises = []
     const itemModels: NotificationsViewItemModel[] = []
     const dedupedNotifs = res.data.notifications.filter(
@@ -525,7 +525,7 @@ export class NotificationsViewModel {
     })
   }
 
-  private _updateAll(res: ListNotifications.Response) {
+  _updateAll(res: ListNotifications.Response) {
     for (const item of res.data.notifications) {
       const existingItem = this.notifications.find(item2 => isEq(item, item2))
       if (existingItem) {
diff --git a/src/state/models/post-thread-view.ts b/src/state/models/post-thread-view.ts
index d58ee691b..c5395b9c8 100644
--- a/src/state/models/post-thread-view.ts
+++ b/src/state/models/post-thread-view.ts
@@ -2,12 +2,13 @@ import {makeAutoObservable, runInAction} from 'mobx'
 import {
   AppBskyFeedGetPostThread as GetPostThread,
   AppBskyFeedPost as FeedPost,
+  AppBskyFeedDefs,
+  RichText,
 } from '@atproto/api'
 import {AtUri} from '../../third-party/uri'
 import {RootStoreModel} from './root-store'
 import * as apilib from 'lib/api/index'
 import {cleanError} from 'lib/strings/errors'
-import {RichText} from 'lib/strings/rich-text'
 
 function* reactKeyGenerator(): Generator<string> {
   let counter = 0
@@ -26,10 +27,10 @@ export class PostThreadViewPostModel {
   _hasMore = false
 
   // data
-  post: FeedPost.View
+  post: AppBskyFeedDefs.PostView
   postRecord?: FeedPost.Record
-  parent?: PostThreadViewPostModel | GetPostThread.NotFoundPost
-  replies?: (PostThreadViewPostModel | GetPostThread.NotFoundPost)[]
+  parent?: PostThreadViewPostModel | AppBskyFeedDefs.NotFoundPost
+  replies?: (PostThreadViewPostModel | AppBskyFeedDefs.NotFoundPost)[]
   richText?: RichText
 
   get uri() {
@@ -43,7 +44,7 @@ export class PostThreadViewPostModel {
   constructor(
     public rootStore: RootStoreModel,
     reactKey: string,
-    v: GetPostThread.ThreadViewPost,
+    v: AppBskyFeedDefs.ThreadViewPost,
   ) {
     this._reactKey = reactKey
     this.post = v.post
@@ -51,11 +52,7 @@ export class PostThreadViewPostModel {
       const valid = FeedPost.validateRecord(this.post.record)
       if (valid.success) {
         this.postRecord = this.post.record
-        this.richText = new RichText(
-          this.postRecord.text,
-          this.postRecord.entities,
-          {cleanNewlines: true},
-        )
+        this.richText = new RichText(this.postRecord, {cleanNewlines: true})
       } else {
         rootStore.log.warn(
           'Received an invalid app.bsky.feed.post record',
@@ -74,14 +71,14 @@ export class PostThreadViewPostModel {
 
   assignTreeModels(
     keyGen: Generator<string>,
-    v: GetPostThread.ThreadViewPost,
+    v: AppBskyFeedDefs.ThreadViewPost,
     higlightedPostUri: string,
     includeParent = true,
     includeChildren = true,
   ) {
     // parents
     if (includeParent && v.parent) {
-      if (GetPostThread.isThreadViewPost(v.parent)) {
+      if (AppBskyFeedDefs.isThreadViewPost(v.parent)) {
         const parentModel = new PostThreadViewPostModel(
           this.rootStore,
           keyGen.next().value,
@@ -100,7 +97,7 @@ export class PostThreadViewPostModel {
           )
         }
         this.parent = parentModel
-      } else if (GetPostThread.isNotFoundPost(v.parent)) {
+      } else if (AppBskyFeedDefs.isNotFoundPost(v.parent)) {
         this.parent = v.parent
       }
     }
@@ -108,7 +105,7 @@ export class PostThreadViewPostModel {
     if (includeChildren && v.replies) {
       const replies = []
       for (const item of v.replies) {
-        if (GetPostThread.isThreadViewPost(item)) {
+        if (AppBskyFeedDefs.isThreadViewPost(item)) {
           const itemModel = new PostThreadViewPostModel(
             this.rootStore,
             keyGen.next().value,
@@ -128,7 +125,7 @@ export class PostThreadViewPostModel {
             )
           }
           replies.push(itemModel)
-        } else if (GetPostThread.isNotFoundPost(item)) {
+        } else if (AppBskyFeedDefs.isNotFoundPost(item)) {
           replies.push(item)
         }
       }
@@ -136,68 +133,43 @@ export class PostThreadViewPostModel {
     }
   }
 
-  async toggleUpvote() {
-    const wasUpvoted = !!this.post.viewer.upvote
-    const wasDownvoted = !!this.post.viewer.downvote
-    const res = await this.rootStore.api.app.bsky.feed.setVote({
-      subject: {
-        uri: this.post.uri,
-        cid: this.post.cid,
-      },
-      direction: wasUpvoted ? 'none' : 'up',
-    })
-    runInAction(() => {
-      if (wasDownvoted) {
-        this.post.downvoteCount--
-      }
-      if (wasUpvoted) {
-        this.post.upvoteCount--
-      } else {
-        this.post.upvoteCount++
-      }
-      this.post.viewer.upvote = res.data.upvote
-      this.post.viewer.downvote = res.data.downvote
-    })
-  }
-
-  async toggleDownvote() {
-    const wasUpvoted = !!this.post.viewer.upvote
-    const wasDownvoted = !!this.post.viewer.downvote
-    const res = await this.rootStore.api.app.bsky.feed.setVote({
-      subject: {
-        uri: this.post.uri,
-        cid: this.post.cid,
-      },
-      direction: wasDownvoted ? 'none' : 'down',
-    })
-    runInAction(() => {
-      if (wasUpvoted) {
-        this.post.upvoteCount--
-      }
-      if (wasDownvoted) {
-        this.post.downvoteCount--
-      } else {
-        this.post.downvoteCount++
-      }
-      this.post.viewer.upvote = res.data.upvote
-      this.post.viewer.downvote = res.data.downvote
-    })
+  async toggleLike() {
+    if (this.post.viewer?.like) {
+      await this.rootStore.agent.deleteLike(this.post.viewer.like)
+      runInAction(() => {
+        this.post.likeCount = this.post.likeCount || 0
+        this.post.viewer = this.post.viewer || {}
+        this.post.likeCount--
+        this.post.viewer.like = undefined
+      })
+    } else {
+      const res = await this.rootStore.agent.like(this.post.uri, this.post.cid)
+      runInAction(() => {
+        this.post.likeCount = this.post.likeCount || 0
+        this.post.viewer = this.post.viewer || {}
+        this.post.likeCount++
+        this.post.viewer.like = res.uri
+      })
+    }
   }
 
   async toggleRepost() {
-    if (this.post.viewer.repost) {
-      await apilib.unrepost(this.rootStore, this.post.viewer.repost)
+    if (this.post.viewer?.repost) {
+      await this.rootStore.agent.deleteRepost(this.post.viewer.repost)
       runInAction(() => {
+        this.post.repostCount = this.post.repostCount || 0
+        this.post.viewer = this.post.viewer || {}
         this.post.repostCount--
         this.post.viewer.repost = undefined
       })
     } else {
-      const res = await apilib.repost(
-        this.rootStore,
+      const res = await this.rootStore.agent.repost(
         this.post.uri,
         this.post.cid,
       )
       runInAction(() => {
+        this.post.repostCount = this.post.repostCount || 0
+        this.post.viewer = this.post.viewer || {}
         this.post.repostCount++
         this.post.viewer.repost = res.uri
       })
@@ -205,10 +177,7 @@ export class PostThreadViewPostModel {
   }
 
   async delete() {
-    await this.rootStore.api.app.bsky.feed.post.delete({
-      did: this.post.author.did,
-      rkey: new AtUri(this.post.uri).rkey,
-    })
+    await this.rootStore.agent.deletePost(this.post.uri)
     this.rootStore.emitPostDeleted(this.post.uri)
   }
 }
@@ -301,14 +270,14 @@ export class PostThreadViewModel {
   // state transitions
   // =
 
-  private _xLoading(isRefreshing = false) {
+  _xLoading(isRefreshing = false) {
     this.isLoading = true
     this.isRefreshing = isRefreshing
     this.error = ''
     this.notFound = false
   }
 
-  private _xIdle(err?: any) {
+  _xIdle(err?: any) {
     this.isLoading = false
     this.isRefreshing = false
     this.hasLoaded = true
@@ -322,7 +291,7 @@ export class PostThreadViewModel {
   // loader functions
   // =
 
-  private async _resolveUri() {
+  async _resolveUri() {
     const urip = new AtUri(this.params.uri)
     if (!urip.host.startsWith('did:')) {
       try {
@@ -336,10 +305,10 @@ export class PostThreadViewModel {
     })
   }
 
-  private async _load(isRefreshing = false) {
+  async _load(isRefreshing = false) {
     this._xLoading(isRefreshing)
     try {
-      const res = await this.rootStore.api.app.bsky.feed.getPostThread(
+      const res = await this.rootStore.agent.getPostThread(
         Object.assign({}, this.params, {uri: this.resolvedUri}),
       )
       this._replaceAll(res)
@@ -349,18 +318,18 @@ export class PostThreadViewModel {
     }
   }
 
-  private _replaceAll(res: GetPostThread.Response) {
+  _replaceAll(res: GetPostThread.Response) {
     sortThread(res.data.thread)
     const keyGen = reactKeyGenerator()
     const thread = new PostThreadViewPostModel(
       this.rootStore,
       keyGen.next().value,
-      res.data.thread as GetPostThread.ThreadViewPost,
+      res.data.thread as AppBskyFeedDefs.ThreadViewPost,
     )
     thread._isHighlightedPost = true
     thread.assignTreeModels(
       keyGen,
-      res.data.thread as GetPostThread.ThreadViewPost,
+      res.data.thread as AppBskyFeedDefs.ThreadViewPost,
       thread.uri,
     )
     this.thread = thread
@@ -368,25 +337,25 @@ export class PostThreadViewModel {
 }
 
 type MaybePost =
-  | GetPostThread.ThreadViewPost
-  | GetPostThread.NotFoundPost
+  | AppBskyFeedDefs.ThreadViewPost
+  | AppBskyFeedDefs.NotFoundPost
   | {[k: string]: unknown; $type: string}
 function sortThread(post: MaybePost) {
   if (post.notFound) {
     return
   }
-  post = post as GetPostThread.ThreadViewPost
+  post = post as AppBskyFeedDefs.ThreadViewPost
   if (post.replies) {
     post.replies.sort((a: MaybePost, b: MaybePost) => {
-      post = post as GetPostThread.ThreadViewPost
+      post = post as AppBskyFeedDefs.ThreadViewPost
       if (a.notFound) {
         return 1
       }
       if (b.notFound) {
         return -1
       }
-      a = a as GetPostThread.ThreadViewPost
-      b = b as GetPostThread.ThreadViewPost
+      a = a as AppBskyFeedDefs.ThreadViewPost
+      b = b as AppBskyFeedDefs.ThreadViewPost
       const aIsByOp = a.post.author.did === post.post.author.did
       const bIsByOp = b.post.author.did === post.post.author.did
       if (aIsByOp && bIsByOp) {
diff --git a/src/state/models/post.ts b/src/state/models/post.ts
index 749e98bb0..c7f2896ba 100644
--- a/src/state/models/post.ts
+++ b/src/state/models/post.ts
@@ -58,12 +58,12 @@ export class PostModel implements RemoveIndex<Post.Record> {
   // state transitions
   // =
 
-  private _xLoading() {
+  _xLoading() {
     this.isLoading = true
     this.error = ''
   }
 
-  private _xIdle(err?: any) {
+  _xIdle(err?: any) {
     this.isLoading = false
     this.hasLoaded = true
     this.error = cleanError(err)
@@ -75,12 +75,12 @@ export class PostModel implements RemoveIndex<Post.Record> {
   // loader functions
   // =
 
-  private async _load() {
+  async _load() {
     this._xLoading()
     try {
       const urip = new AtUri(this.uri)
-      const res = await this.rootStore.api.app.bsky.feed.post.get({
-        user: urip.host,
+      const res = await this.rootStore.agent.getPost({
+        repo: urip.host,
         rkey: urip.rkey,
       })
       // TODO
@@ -94,7 +94,7 @@ export class PostModel implements RemoveIndex<Post.Record> {
     }
   }
 
-  private _replaceAll(res: Post.Record) {
+  _replaceAll(res: Post.Record) {
     this.text = res.text
     this.entities = res.entities
     this.reply = res.reply
diff --git a/src/state/models/profile-view.ts b/src/state/models/profile-view.ts
index 9d3eeff58..eacc6a298 100644
--- a/src/state/models/profile-view.ts
+++ b/src/state/models/profile-view.ts
@@ -2,15 +2,12 @@ import {makeAutoObservable, runInAction} from 'mobx'
 import {PickedMedia} from 'lib/media/picker'
 import {
   AppBskyActorGetProfile as GetProfile,
-  AppBskySystemDeclRef,
-  AppBskyActorUpdateProfile,
+  AppBskyActorProfile,
+  RichText,
 } from '@atproto/api'
-type DeclRef = AppBskySystemDeclRef.Main
-import {extractEntities} from 'lib/strings/rich-text-detection'
 import {RootStoreModel} from './root-store'
 import * as apilib from 'lib/api/index'
 import {cleanError} from 'lib/strings/errors'
-import {RichText} from 'lib/strings/rich-text'
 
 export const ACTOR_TYPE_USER = 'app.bsky.system.actorUser'
 
@@ -35,22 +32,18 @@ export class ProfileViewModel {
   // data
   did: string = ''
   handle: string = ''
-  declaration: DeclRef = {
-    cid: '',
-    actorType: '',
-  }
   creator: string = ''
-  displayName?: string
-  description?: string
-  avatar?: string
-  banner?: string
+  displayName?: string = ''
+  description?: string = ''
+  avatar?: string = ''
+  banner?: string = ''
   followersCount: number = 0
   followsCount: number = 0
   postsCount: number = 0
   viewer = new ProfileViewViewerModel()
 
   // added data
-  descriptionRichText?: RichText
+  descriptionRichText?: RichText = new RichText({text: ''})
 
   constructor(
     public rootStore: RootStoreModel,
@@ -79,10 +72,6 @@ export class ProfileViewModel {
     return this.hasLoaded && !this.hasContent
   }
 
-  get isUser() {
-    return this.declaration.actorType === ACTOR_TYPE_USER
-  }
-
   // public api
   // =
 
@@ -111,18 +100,14 @@ export class ProfileViewModel {
     }
 
     if (followUri) {
-      await apilib.unfollow(this.rootStore, followUri)
+      await this.rootStore.agent.deleteFollow(followUri)
       runInAction(() => {
         this.followersCount--
         this.viewer.following = undefined
         this.rootStore.me.follows.removeFollow(this.did)
       })
     } else {
-      const res = await apilib.follow(
-        this.rootStore,
-        this.did,
-        this.declaration.cid,
-      )
+      const res = await this.rootStore.agent.follow(this.did)
       runInAction(() => {
         this.followersCount++
         this.viewer.following = res.uri
@@ -132,49 +117,48 @@ export class ProfileViewModel {
   }
 
   async updateProfile(
-    updates: AppBskyActorUpdateProfile.InputSchema,
+    updates: AppBskyActorProfile.Record,
     newUserAvatar: PickedMedia | undefined | null,
     newUserBanner: PickedMedia | undefined | null,
   ) {
-    if (newUserAvatar) {
-      const res = await apilib.uploadBlob(
-        this.rootStore,
-        newUserAvatar.path,
-        newUserAvatar.mime,
-      )
-      updates.avatar = {
-        cid: res.data.cid,
-        mimeType: newUserAvatar.mime,
+    await this.rootStore.agent.upsertProfile(async existing => {
+      existing = existing || {}
+      existing.displayName = updates.displayName
+      existing.description = updates.description
+      if (newUserAvatar) {
+        const res = await apilib.uploadBlob(
+          this.rootStore,
+          newUserAvatar.path,
+          newUserAvatar.mime,
+        )
+        existing.avatar = res.data.blob
+      } else if (newUserAvatar === null) {
+        existing.avatar = undefined
       }
-    } else if (newUserAvatar === null) {
-      updates.avatar = null
-    }
-    if (newUserBanner) {
-      const res = await apilib.uploadBlob(
-        this.rootStore,
-        newUserBanner.path,
-        newUserBanner.mime,
-      )
-      updates.banner = {
-        cid: res.data.cid,
-        mimeType: newUserBanner.mime,
+      if (newUserBanner) {
+        const res = await apilib.uploadBlob(
+          this.rootStore,
+          newUserBanner.path,
+          newUserBanner.mime,
+        )
+        existing.banner = res.data.blob
+      } else if (newUserBanner === null) {
+        existing.banner = undefined
       }
-    } else if (newUserBanner === null) {
-      updates.banner = null
-    }
-    await this.rootStore.api.app.bsky.actor.updateProfile(updates)
+      return existing
+    })
     await this.rootStore.me.load()
     await this.refresh()
   }
 
   async muteAccount() {
-    await this.rootStore.api.app.bsky.graph.mute({user: this.did})
+    await this.rootStore.agent.mute(this.did)
     this.viewer.muted = true
     await this.refresh()
   }
 
   async unmuteAccount() {
-    await this.rootStore.api.app.bsky.graph.unmute({user: this.did})
+    await this.rootStore.agent.unmute(this.did)
     this.viewer.muted = false
     await this.refresh()
   }
@@ -182,13 +166,13 @@ export class ProfileViewModel {
   // state transitions
   // =
 
-  private _xLoading(isRefreshing = false) {
+  _xLoading(isRefreshing = false) {
     this.isLoading = true
     this.isRefreshing = isRefreshing
     this.error = ''
   }
 
-  private _xIdle(err?: any) {
+  _xIdle(err?: any) {
     this.isLoading = false
     this.isRefreshing = false
     this.hasLoaded = true
@@ -201,40 +185,40 @@ export class ProfileViewModel {
   // loader functions
   // =
 
-  private async _load(isRefreshing = false) {
+  async _load(isRefreshing = false) {
     this._xLoading(isRefreshing)
     try {
-      const res = await this.rootStore.api.app.bsky.actor.getProfile(
-        this.params,
-      )
+      const res = await this.rootStore.agent.getProfile(this.params)
       this.rootStore.profiles.overwrite(this.params.actor, res) // cache invalidation
       this._replaceAll(res)
+      await this._createRichText()
       this._xIdle()
     } catch (e: any) {
       this._xIdle(e)
     }
   }
 
-  private _replaceAll(res: GetProfile.Response) {
+  _replaceAll(res: GetProfile.Response) {
     this.did = res.data.did
     this.handle = res.data.handle
-    Object.assign(this.declaration, res.data.declaration)
-    this.creator = res.data.creator
     this.displayName = res.data.displayName
     this.description = res.data.description
     this.avatar = res.data.avatar
     this.banner = res.data.banner
-    this.followersCount = res.data.followersCount
-    this.followsCount = res.data.followsCount
-    this.postsCount = res.data.postsCount
+    this.followersCount = res.data.followersCount || 0
+    this.followsCount = res.data.followsCount || 0
+    this.postsCount = res.data.postsCount || 0
     if (res.data.viewer) {
       Object.assign(this.viewer, res.data.viewer)
       this.rootStore.me.follows.hydrate(this.did, res.data.viewer.following)
     }
+  }
+
+  async _createRichText() {
     this.descriptionRichText = new RichText(
-      this.description || '',
-      extractEntities(this.description || ''),
+      {text: this.description || ''},
       {cleanNewlines: true},
     )
+    await this.descriptionRichText.detectFacets(this.rootStore.agent)
   }
 }
diff --git a/src/state/models/profiles-view.ts b/src/state/models/profiles-view.ts
index 4241e50e1..30e6d0442 100644
--- a/src/state/models/profiles-view.ts
+++ b/src/state/models/profiles-view.ts
@@ -31,7 +31,7 @@ export class ProfilesViewModel {
       }
     }
     try {
-      const promise = this.rootStore.api.app.bsky.actor.getProfile({
+      const promise = this.rootStore.agent.getProfile({
         actor: did,
       })
       this.cache.set(did, promise)
diff --git a/src/state/models/reposted-by-view.ts b/src/state/models/reposted-by-view.ts
index 69a728d6f..c9b089c70 100644
--- a/src/state/models/reposted-by-view.ts
+++ b/src/state/models/reposted-by-view.ts
@@ -2,7 +2,7 @@ import {makeAutoObservable, runInAction} from 'mobx'
 import {AtUri} from '../../third-party/uri'
 import {
   AppBskyFeedGetRepostedBy as GetRepostedBy,
-  AppBskyActorRef as ActorRef,
+  AppBskyActorDefs,
 } from '@atproto/api'
 import {RootStoreModel} from './root-store'
 import {bundleAsync} from 'lib/async/bundle'
@@ -11,7 +11,7 @@ import * as apilib from 'lib/api/index'
 
 const PAGE_SIZE = 30
 
-export type RepostedByItem = ActorRef.WithInfo
+export type RepostedByItem = AppBskyActorDefs.ProfileViewBasic
 
 export class RepostedByViewModel {
   // state
@@ -71,9 +71,9 @@ export class RepostedByViewModel {
       const params = Object.assign({}, this.params, {
         uri: this.resolvedUri,
         limit: PAGE_SIZE,
-        before: replace ? undefined : this.loadMoreCursor,
+        cursor: replace ? undefined : this.loadMoreCursor,
       })
-      const res = await this.rootStore.api.app.bsky.feed.getRepostedBy(params)
+      const res = await this.rootStore.agent.getRepostedBy(params)
       if (replace) {
         this._replaceAll(res)
       } else {
@@ -88,13 +88,13 @@ export class RepostedByViewModel {
   // state transitions
   // =
 
-  private _xLoading(isRefreshing = false) {
+  _xLoading(isRefreshing = false) {
     this.isLoading = true
     this.isRefreshing = isRefreshing
     this.error = ''
   }
 
-  private _xIdle(err?: any) {
+  _xIdle(err?: any) {
     this.isLoading = false
     this.isRefreshing = false
     this.hasLoaded = true
@@ -107,7 +107,7 @@ export class RepostedByViewModel {
   // helper functions
   // =
 
-  private async _resolveUri() {
+  async _resolveUri() {
     const urip = new AtUri(this.params.uri)
     if (!urip.host.startsWith('did:')) {
       try {
@@ -121,12 +121,12 @@ export class RepostedByViewModel {
     })
   }
 
-  private _replaceAll(res: GetRepostedBy.Response) {
+  _replaceAll(res: GetRepostedBy.Response) {
     this.repostedBy = []
     this._appendAll(res)
   }
 
-  private _appendAll(res: GetRepostedBy.Response) {
+  _appendAll(res: GetRepostedBy.Response) {
     this.loadMoreCursor = res.data.cursor
     this.hasMore = !!this.loadMoreCursor
     this.repostedBy = this.repostedBy.concat(res.data.repostedBy)
diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts
index d8336d005..0c2a31d28 100644
--- a/src/state/models/root-store.ts
+++ b/src/state/models/root-store.ts
@@ -2,8 +2,8 @@
  * The root store is the base of all modeled state.
  */
 
-import {makeAutoObservable, runInAction} from 'mobx'
-import {AtpAgent} from '@atproto/api'
+import {makeAutoObservable} from 'mobx'
+import {BskyAgent} from '@atproto/api'
 import {createContext, useContext} from 'react'
 import {DeviceEventEmitter, EmitterSubscription} from 'react-native'
 import * as BgScheduler from 'lib/bg-scheduler'
@@ -29,7 +29,7 @@ export const appInfo = z.object({
 export type AppInfo = z.infer<typeof appInfo>
 
 export class RootStoreModel {
-  agent: AtpAgent
+  agent: BskyAgent
   appInfo?: AppInfo
   log = new LogModel()
   session = new SessionModel(this)
@@ -40,41 +40,16 @@ export class RootStoreModel {
   linkMetas = new LinkMetasCache(this)
   imageSizes = new ImageSizesCache()
 
-  // HACK
-  // this flag is to track the lexicon breaking refactor
-  // it should be removed once we get that done
-  // -prf
-  hackUpgradeNeeded = false
-  async hackCheckIfUpgradeNeeded() {
-    try {
-      this.log.debug('hackCheckIfUpgradeNeeded()')
-      const res = await fetch('https://bsky.social/xrpc/app.bsky.feed.getLikes')
-      await res.text()
-      runInAction(() => {
-        this.hackUpgradeNeeded = res.status !== 501
-        this.log.debug(
-          `hackCheckIfUpgradeNeeded() said ${this.hackUpgradeNeeded}`,
-        )
-      })
-    } catch (e) {
-      this.log.error('Failed to hackCheckIfUpgradeNeeded', {e})
-    }
-  }
-
-  constructor(agent: AtpAgent) {
+  constructor(agent: BskyAgent) {
     this.agent = agent
     makeAutoObservable(this, {
-      api: false,
+      agent: false,
       serialize: false,
       hydrate: false,
     })
     this.initBgFetch()
   }
 
-  get api() {
-    return this.agent.api
-  }
-
   setAppInfo(info: AppInfo) {
     this.appInfo = info
   }
@@ -131,7 +106,7 @@ export class RootStoreModel {
   /**
    * Called by the session model. Refreshes session-oriented state.
    */
-  async handleSessionChange(agent: AtpAgent) {
+  async handleSessionChange(agent: BskyAgent) {
     this.log.debug('RootStoreModel:handleSessionChange')
     this.agent = agent
     this.me.clear()
@@ -259,7 +234,7 @@ export class RootStoreModel {
   async onBgFetch(taskId: string) {
     this.log.debug(`Background fetch fired for task ${taskId}`)
     if (this.session.hasSession) {
-      const res = await this.api.app.bsky.notification.getCount()
+      const res = await this.agent.countUnreadNotifications()
       const hasNewNotifs = this.me.notifications.unreadCount !== res.data.count
       this.emitUnreadNotifications(res.data.count)
       this.log.debug(
@@ -286,7 +261,7 @@ export class RootStoreModel {
 }
 
 const throwawayInst = new RootStoreModel(
-  new AtpAgent({service: 'http://localhost'}),
+  new BskyAgent({service: 'http://localhost'}),
 ) // this will be replaced by the loader, we just need to supply a value at init
 const RootStoreContext = createContext<RootStoreModel>(throwawayInst)
 export const RootStoreProvider = RootStoreContext.Provider
diff --git a/src/state/models/session.ts b/src/state/models/session.ts
index e131b2b2c..c2e10880d 100644
--- a/src/state/models/session.ts
+++ b/src/state/models/session.ts
@@ -1,9 +1,9 @@
 import {makeAutoObservable, runInAction} from 'mobx'
 import {
-  AtpAgent,
+  BskyAgent,
   AtpSessionEvent,
   AtpSessionData,
-  ComAtprotoServerGetAccountsConfig as GetAccountsConfig,
+  ComAtprotoServerDescribeServer as DescribeServer,
 } from '@atproto/api'
 import normalizeUrl from 'normalize-url'
 import {isObj, hasProp} from 'lib/type-guards'
@@ -11,7 +11,7 @@ import {networkRetry} from 'lib/async/retry'
 import {z} from 'zod'
 import {RootStoreModel} from './root-store'
 
-export type ServiceDescription = GetAccountsConfig.OutputSchema
+export type ServiceDescription = DescribeServer.OutputSchema
 
 export const activeSession = z.object({
   service: z.string(),
@@ -40,7 +40,7 @@ export class SessionModel {
   // emergency log facility to help us track down this logout issue
   // remove when resolved
   // -prf
-  private _log(message: string, details?: Record<string, any>) {
+  _log(message: string, details?: Record<string, any>) {
     details = details || {}
     details.state = {
       data: this.data,
@@ -73,6 +73,7 @@ export class SessionModel {
       rootStore: false,
       serialize: false,
       hydrate: false,
+      hasSession: false,
     })
   }
 
@@ -154,7 +155,7 @@ export class SessionModel {
   /**
    * Sets the active session
    */
-  async setActiveSession(agent: AtpAgent, did: string) {
+  async setActiveSession(agent: BskyAgent, did: string) {
     this._log('SessionModel:setActiveSession')
     this.data = {
       service: agent.service.toString(),
@@ -166,7 +167,7 @@ export class SessionModel {
   /**
    * Upserts a session into the accounts
    */
-  private persistSession(
+  persistSession(
     service: string,
     did: string,
     event: AtpSessionEvent,
@@ -225,7 +226,7 @@ export class SessionModel {
   /**
    * Clears any session tokens from the accounts; used on logout.
    */
-  private clearSessionTokens() {
+  clearSessionTokens() {
     this._log('SessionModel:clearSessionTokens')
     this.accounts = this.accounts.map(acct => ({
       service: acct.service,
@@ -239,10 +240,8 @@ export class SessionModel {
   /**
    * Fetches additional information about an account on load.
    */
-  private async loadAccountInfo(agent: AtpAgent, did: string) {
-    const res = await agent.api.app.bsky.actor
-      .getProfile({actor: did})
-      .catch(_e => undefined)
+  async loadAccountInfo(agent: BskyAgent, did: string) {
+    const res = await agent.getProfile({actor: did}).catch(_e => undefined)
     if (res) {
       return {
         dispayName: res.data.displayName,
@@ -255,8 +254,8 @@ export class SessionModel {
    * Helper to fetch the accounts config settings from an account.
    */
   async describeService(service: string): Promise<ServiceDescription> {
-    const agent = new AtpAgent({service})
-    const res = await agent.api.com.atproto.server.getAccountsConfig({})
+    const agent = new BskyAgent({service})
+    const res = await agent.com.atproto.server.describeServer({})
     return res.data
   }
 
@@ -272,7 +271,7 @@ export class SessionModel {
       return false
     }
 
-    const agent = new AtpAgent({
+    const agent = new BskyAgent({
       service: account.service,
       persistSession: (evt: AtpSessionEvent, sess?: AtpSessionData) => {
         this.persistSession(account.service, account.did, evt, sess)
@@ -321,7 +320,7 @@ export class SessionModel {
     password: string
   }) {
     this._log('SessionModel:login')
-    const agent = new AtpAgent({service})
+    const agent = new BskyAgent({service})
     await agent.login({identifier, password})
     if (!agent.session) {
       throw new Error('Failed to establish session')
@@ -355,7 +354,7 @@ export class SessionModel {
     inviteCode?: string
   }) {
     this._log('SessionModel:createAccount')
-    const agent = new AtpAgent({service})
+    const agent = new BskyAgent({service})
     await agent.createAccount({
       handle,
       password,
@@ -389,7 +388,7 @@ export class SessionModel {
     // need to evaluate why deleting the session has caused errors at times
     // -prf
     /*if (this.hasSession) {
-      this.rootStore.api.com.atproto.session.delete().catch((e: any) => {
+      this.rootStore.agent.com.atproto.session.delete().catch((e: any) => {
         this.rootStore.log.warn(
           '(Minor issue) Failed to delete session on the server',
           e,
@@ -415,7 +414,7 @@ export class SessionModel {
     if (!sess) {
       return
     }
-    const res = await this.rootStore.api.app.bsky.actor
+    const res = await this.rootStore.agent
       .getProfile({actor: sess.did})
       .catch(_e => undefined)
     if (res?.success) {
diff --git a/src/state/models/suggested-posts-view.ts b/src/state/models/suggested-posts-view.ts
index 7a5ca81b9..46bf235ff 100644
--- a/src/state/models/suggested-posts-view.ts
+++ b/src/state/models/suggested-posts-view.ts
@@ -72,12 +72,12 @@ export class SuggestedPostsView {
   // state transitions
   // =
 
-  private _xLoading() {
+  _xLoading() {
     this.isLoading = true
     this.error = ''
   }
 
-  private _xIdle(err?: any) {
+  _xIdle(err?: any) {
     this.isLoading = false
     this.hasLoaded = true
     this.error = cleanError(err)
diff --git a/src/state/models/ui/create-account.ts b/src/state/models/ui/create-account.ts
index a212fe05e..e661cb59d 100644
--- a/src/state/models/ui/create-account.ts
+++ b/src/state/models/ui/create-account.ts
@@ -2,7 +2,7 @@ import {makeAutoObservable} from 'mobx'
 import {RootStoreModel} from '../root-store'
 import {ServiceDescription} from '../session'
 import {DEFAULT_SERVICE} from 'state/index'
-import {ComAtprotoAccountCreate} from '@atproto/api'
+import {ComAtprotoServerCreateAccount} from '@atproto/api'
 import * as EmailValidator from 'email-validator'
 import {createFullHandle} from 'lib/strings/handles'
 import {cleanError} from 'lib/strings/errors'
@@ -99,7 +99,7 @@ export class CreateAccountModel {
       })
     } catch (e: any) {
       let errMsg = e.toString()
-      if (e instanceof ComAtprotoAccountCreate.InvalidInviteCodeError) {
+      if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) {
         errMsg =
           'Invite code not accepted. Check that you input it correctly and try again.'
       }
diff --git a/src/state/models/ui/profile.ts b/src/state/models/ui/profile.ts
index 280541b74..59529aa39 100644
--- a/src/state/models/ui/profile.ts
+++ b/src/state/models/ui/profile.ts
@@ -40,7 +40,7 @@ export class ProfileUiModel {
     )
     this.profile = new ProfileViewModel(rootStore, {actor: params.user})
     this.feed = new FeedModel(rootStore, 'author', {
-      author: params.user,
+      actor: params.user,
       limit: 10,
     })
   }
@@ -64,16 +64,8 @@ export class ProfileUiModel {
     return this.profile.isRefreshing || this.currentView.isRefreshing
   }
 
-  get isUser() {
-    return this.profile.isUser
-  }
-
   get selectorItems() {
-    if (this.isUser) {
-      return USER_SELECTOR_ITEMS
-    } else {
-      return USER_SELECTOR_ITEMS
-    }
+    return USER_SELECTOR_ITEMS
   }
 
   get selectedView() {
diff --git a/src/state/models/ui/search.ts b/src/state/models/ui/search.ts
index 91e1b24bf..8436b0984 100644
--- a/src/state/models/ui/search.ts
+++ b/src/state/models/ui/search.ts
@@ -1,6 +1,6 @@
 import {makeAutoObservable, runInAction} from 'mobx'
 import {searchProfiles, searchPosts} from 'lib/api/search'
-import {AppBskyActorProfile as Profile} from '@atproto/api'
+import {AppBskyActorDefs} from '@atproto/api'
 import {RootStoreModel} from '../root-store'
 
 export class SearchUIModel {
@@ -8,7 +8,7 @@ export class SearchUIModel {
   isProfilesLoading = false
   query: string = ''
   postUris: string[] = []
-  profiles: Profile.View[] = []
+  profiles: AppBskyActorDefs.ProfileView[] = []
 
   constructor(public rootStore: RootStoreModel) {
     makeAutoObservable(this)
@@ -34,10 +34,10 @@ export class SearchUIModel {
       this.isPostsLoading = false
     })
 
-    let profiles: Profile.View[] = []
+    let profiles: AppBskyActorDefs.ProfileView[] = []
     if (profilesSearch?.length) {
       do {
-        const res = await this.rootStore.api.app.bsky.actor.getProfiles({
+        const res = await this.rootStore.agent.getProfiles({
           actors: profilesSearch.splice(0, 25).map(p => p.did),
         })
         profiles = profiles.concat(res.data.profiles)
diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts
index fec1e2899..7f57d5b54 100644
--- a/src/state/models/ui/shell.ts
+++ b/src/state/models/ui/shell.ts
@@ -1,3 +1,4 @@
+import {AppBskyEmbedRecord} from '@atproto/api'
 import {RootStoreModel} from '../root-store'
 import {makeAutoObservable} from 'mobx'
 import {ProfileViewModel} from '../profile-view'
@@ -111,6 +112,7 @@ export interface ComposerOptsQuote {
     displayName?: string
     avatar?: string
   }
+  embeds?: AppBskyEmbedRecord.ViewRecord['embeds']
 }
 export interface ComposerOpts {
   replyTo?: ComposerOptsPostRef
diff --git a/src/state/models/user-autocomplete-view.ts b/src/state/models/user-autocomplete-view.ts
index 8e4211c27..ad89bb08b 100644
--- a/src/state/models/user-autocomplete-view.ts
+++ b/src/state/models/user-autocomplete-view.ts
@@ -1,5 +1,5 @@
 import {makeAutoObservable, runInAction} from 'mobx'
-import {AppBskyActorRef} from '@atproto/api'
+import {AppBskyActorDefs} from '@atproto/api'
 import AwaitLock from 'await-lock'
 import {RootStoreModel} from './root-store'
 
@@ -11,8 +11,8 @@ export class UserAutocompleteViewModel {
   lock = new AwaitLock()
 
   // data
-  follows: AppBskyActorRef.WithInfo[] = []
-  searchRes: AppBskyActorRef.WithInfo[] = []
+  follows: AppBskyActorDefs.ProfileViewBasic[] = []
+  searchRes: AppBskyActorDefs.ProfileViewBasic[] = []
   knownHandles: Set<string> = new Set()
 
   constructor(public rootStore: RootStoreModel) {
@@ -76,9 +76,9 @@ export class UserAutocompleteViewModel {
   // internal
   // =
 
-  private async _getFollows() {
-    const res = await this.rootStore.api.app.bsky.graph.getFollows({
-      user: this.rootStore.me.did || '',
+  async _getFollows() {
+    const res = await this.rootStore.agent.getFollows({
+      actor: this.rootStore.me.did || '',
     })
     runInAction(() => {
       this.follows = res.data.follows
@@ -88,13 +88,13 @@ export class UserAutocompleteViewModel {
     })
   }
 
-  private async _search() {
-    const res = await this.rootStore.api.app.bsky.actor.searchTypeahead({
+  async _search() {
+    const res = await this.rootStore.agent.searchActorsTypeahead({
       term: this.prefix,
       limit: 8,
     })
     runInAction(() => {
-      this.searchRes = res.data.users
+      this.searchRes = res.data.actors
       for (const u of this.searchRes) {
         this.knownHandles.add(u.handle)
       }
diff --git a/src/state/models/user-followers-view.ts b/src/state/models/user-followers-view.ts
index 7400262a4..055032eb7 100644
--- a/src/state/models/user-followers-view.ts
+++ b/src/state/models/user-followers-view.ts
@@ -1,7 +1,7 @@
 import {makeAutoObservable} from 'mobx'
 import {
   AppBskyGraphGetFollowers as GetFollowers,
-  AppBskyActorRef as ActorRef,
+  AppBskyActorDefs as ActorDefs,
 } from '@atproto/api'
 import {RootStoreModel} from './root-store'
 import {cleanError} from 'lib/strings/errors'
@@ -9,7 +9,7 @@ import {bundleAsync} from 'lib/async/bundle'
 
 const PAGE_SIZE = 30
 
-export type FollowerItem = ActorRef.WithInfo
+export type FollowerItem = ActorDefs.ProfileViewBasic
 
 export class UserFollowersViewModel {
   // state
@@ -22,10 +22,9 @@ export class UserFollowersViewModel {
   loadMoreCursor?: string
 
   // data
-  subject: ActorRef.WithInfo = {
+  subject: ActorDefs.ProfileViewBasic = {
     did: '',
     handle: '',
-    declaration: {cid: '', actorType: ''},
   }
   followers: FollowerItem[] = []
 
@@ -71,9 +70,9 @@ export class UserFollowersViewModel {
     try {
       const params = Object.assign({}, this.params, {
         limit: PAGE_SIZE,
-        before: replace ? undefined : this.loadMoreCursor,
+        cursor: replace ? undefined : this.loadMoreCursor,
       })
-      const res = await this.rootStore.api.app.bsky.graph.getFollowers(params)
+      const res = await this.rootStore.agent.getFollowers(params)
       if (replace) {
         this._replaceAll(res)
       } else {
@@ -88,13 +87,13 @@ export class UserFollowersViewModel {
   // state transitions
   // =
 
-  private _xLoading(isRefreshing = false) {
+  _xLoading(isRefreshing = false) {
     this.isLoading = true
     this.isRefreshing = isRefreshing
     this.error = ''
   }
 
-  private _xIdle(err?: any) {
+  _xIdle(err?: any) {
     this.isLoading = false
     this.isRefreshing = false
     this.hasLoaded = true
@@ -107,12 +106,12 @@ export class UserFollowersViewModel {
   // helper functions
   // =
 
-  private _replaceAll(res: GetFollowers.Response) {
+  _replaceAll(res: GetFollowers.Response) {
     this.followers = []
     this._appendAll(res)
   }
 
-  private _appendAll(res: GetFollowers.Response) {
+  _appendAll(res: GetFollowers.Response) {
     this.loadMoreCursor = res.data.cursor
     this.hasMore = !!this.loadMoreCursor
     this.followers = this.followers.concat(res.data.followers)
diff --git a/src/state/models/user-follows-view.ts b/src/state/models/user-follows-view.ts
index 7d28d7ebd..6d9d84592 100644
--- a/src/state/models/user-follows-view.ts
+++ b/src/state/models/user-follows-view.ts
@@ -1,7 +1,7 @@
 import {makeAutoObservable} from 'mobx'
 import {
   AppBskyGraphGetFollows as GetFollows,
-  AppBskyActorRef as ActorRef,
+  AppBskyActorDefs as ActorDefs,
 } from '@atproto/api'
 import {RootStoreModel} from './root-store'
 import {cleanError} from 'lib/strings/errors'
@@ -9,7 +9,7 @@ import {bundleAsync} from 'lib/async/bundle'
 
 const PAGE_SIZE = 30
 
-export type FollowItem = ActorRef.WithInfo
+export type FollowItem = ActorDefs.ProfileViewBasic
 
 export class UserFollowsViewModel {
   // state
@@ -22,10 +22,9 @@ export class UserFollowsViewModel {
   loadMoreCursor?: string
 
   // data
-  subject: ActorRef.WithInfo = {
+  subject: ActorDefs.ProfileViewBasic = {
     did: '',
     handle: '',
-    declaration: {cid: '', actorType: ''},
   }
   follows: FollowItem[] = []
 
@@ -71,9 +70,9 @@ export class UserFollowsViewModel {
     try {
       const params = Object.assign({}, this.params, {
         limit: PAGE_SIZE,
-        before: replace ? undefined : this.loadMoreCursor,
+        cursor: replace ? undefined : this.loadMoreCursor,
       })
-      const res = await this.rootStore.api.app.bsky.graph.getFollows(params)
+      const res = await this.rootStore.agent.getFollows(params)
       if (replace) {
         this._replaceAll(res)
       } else {
@@ -88,13 +87,13 @@ export class UserFollowsViewModel {
   // state transitions
   // =
 
-  private _xLoading(isRefreshing = false) {
+  _xLoading(isRefreshing = false) {
     this.isLoading = true
     this.isRefreshing = isRefreshing
     this.error = ''
   }
 
-  private _xIdle(err?: any) {
+  _xIdle(err?: any) {
     this.isLoading = false
     this.isRefreshing = false
     this.hasLoaded = true
@@ -107,12 +106,12 @@ export class UserFollowsViewModel {
   // helper functions
   // =
 
-  private _replaceAll(res: GetFollows.Response) {
+  _replaceAll(res: GetFollows.Response) {
     this.follows = []
     this._appendAll(res)
   }
 
-  private _appendAll(res: GetFollows.Response) {
+  _appendAll(res: GetFollows.Response) {
     this.loadMoreCursor = res.data.cursor
     this.hasMore = !!this.loadMoreCursor
     this.follows = this.follows.concat(res.data.follows)
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
diff --git a/tsconfig.json b/tsconfig.json
index f1e7f443d..409a613d7 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -6,7 +6,7 @@
       "lib/*": ["./src/lib/*"],
       "platform/*": ["./src/platform/*"],
       "state/*": ["./src/state/*"],
-      "view/*": ["./src/view/*"]
+      "view/*": ["./src/view/*"],
     }
   }
 }
diff --git a/web/static/js/intl-segmenter-polyfill.min.js b/web/static/js/intl-segmenter-polyfill.min.js
new file mode 100644
index 000000000..bdd9af2d3
--- /dev/null
+++ b/web/static/js/intl-segmenter-polyfill.min.js
@@ -0,0 +1,2 @@
+var k=Object.create;var M=Object.defineProperty;var m=Object.getOwnPropertyDescriptor;var y=Object.getOwnPropertyNames;var j=Object.getPrototypeOf,F=Object.prototype.hasOwnProperty;var l=(x,E)=>()=>(E||x((E={exports:{}}).exports,E),E.exports);var Z=(x,E,R,i)=>{if(E&&typeof E=="object"||typeof E=="function")for(let T of y(E))!F.call(x,T)&&T!==R&&M(x,T,{get:()=>E[T],enumerable:!(i=m(E,T))||i.enumerable});return x};var J=(x,E,R)=>(R=x!=null?k(j(x)):{},Z(E||!x||!x.__esModule?M(R,"default",{value:x,enumerable:!0}):R,x));var V=l(K=>{"use strict";Object.defineProperty(K,"__esModule",{value:!0});K.EXTENDED_PICTOGRAPHIC=K.CLUSTER_BREAK=void 0;var W;(function(x){x[x.CR=0]="CR",x[x.LF=1]="LF",x[x.CONTROL=2]="CONTROL",x[x.EXTEND=3]="EXTEND",x[x.REGIONAL_INDICATOR=4]="REGIONAL_INDICATOR",x[x.SPACINGMARK=5]="SPACINGMARK",x[x.L=6]="L",x[x.V=7]="V",x[x.T=8]="T",x[x.LV=9]="LV",x[x.LVT=10]="LVT",x[x.OTHER=11]="OTHER",x[x.PREPEND=12]="PREPEND",x[x.E_BASE=13]="E_BASE",x[x.E_MODIFIER=14]="E_MODIFIER",x[x.ZWJ=15]="ZWJ",x[x.GLUE_AFTER_ZWJ=16]="GLUE_AFTER_ZWJ",x[x.E_BASE_GAZ=17]="E_BASE_GAZ"})(W=K.CLUSTER_BREAK||(K.CLUSTER_BREAK={}));K.EXTENDED_PICTOGRAPHIC=101});var O=l(X=>{"use strict";Object.defineProperty(X,"__esModule",{value:!0});var r=V(),u=0,N=1,w=2,q=3,z=4,D=class{static isSurrogate(E,R){return 55296<=E.charCodeAt(R)&&E.charCodeAt(R)<=56319&&56320<=E.charCodeAt(R+1)&&E.charCodeAt(R+1)<=57343}static codePointAt(E,R){R===void 0&&(R=0);let i=E.charCodeAt(R);if(55296<=i&&i<=56319&&R<E.length-1){let T=i,t=E.charCodeAt(R+1);return 56320<=t&&t<=57343?(T-55296)*1024+(t-56320)+65536:T}if(56320<=i&&i<=57343&&R>=1){let T=E.charCodeAt(R-1),t=i;return 55296<=T&&T<=56319?(T-55296)*1024+(t-56320)+65536:t}return i}static shouldBreak(E,R,i,T,t,_){let n=[E].concat(R).concat([i]),S=[T].concat(t).concat([_]),A=n[n.length-2],L=i,B=_,G=n.lastIndexOf(r.CLUSTER_BREAK.REGIONAL_INDICATOR);if(G>0&&n.slice(1,G).every(function(s){return s===r.CLUSTER_BREAK.REGIONAL_INDICATOR})&&[r.CLUSTER_BREAK.PREPEND,r.CLUSTER_BREAK.REGIONAL_INDICATOR].indexOf(A)===-1)return n.filter(function(s){return s===r.CLUSTER_BREAK.REGIONAL_INDICATOR}).length%2===1?q:z;if(A===r.CLUSTER_BREAK.CR&&L===r.CLUSTER_BREAK.LF)return u;if(A===r.CLUSTER_BREAK.CONTROL||A===r.CLUSTER_BREAK.CR||A===r.CLUSTER_BREAK.LF)return N;if(L===r.CLUSTER_BREAK.CONTROL||L===r.CLUSTER_BREAK.CR||L===r.CLUSTER_BREAK.LF)return N;if(A===r.CLUSTER_BREAK.L&&(L===r.CLUSTER_BREAK.L||L===r.CLUSTER_BREAK.V||L===r.CLUSTER_BREAK.LV||L===r.CLUSTER_BREAK.LVT))return u;if((A===r.CLUSTER_BREAK.LV||A===r.CLUSTER_BREAK.V)&&(L===r.CLUSTER_BREAK.V||L===r.CLUSTER_BREAK.T))return u;if((A===r.CLUSTER_BREAK.LVT||A===r.CLUSTER_BREAK.T)&&L===r.CLUSTER_BREAK.T)return u;if(L===r.CLUSTER_BREAK.EXTEND||L===r.CLUSTER_BREAK.ZWJ)return u;if(L===r.CLUSTER_BREAK.SPACINGMARK)return u;if(A===r.CLUSTER_BREAK.PREPEND)return u;let a=S.slice(0,-1).lastIndexOf(r.EXTENDED_PICTOGRAPHIC);return a!==-1&&S[a]===r.EXTENDED_PICTOGRAPHIC&&n.slice(a+1,-2).every(function(s){return s===r.CLUSTER_BREAK.EXTEND})&&A===r.CLUSTER_BREAK.ZWJ&&B===r.EXTENDED_PICTOGRAPHIC?u:R.indexOf(r.CLUSTER_BREAK.REGIONAL_INDICATOR)!==-1?w:A===r.CLUSTER_BREAK.REGIONAL_INDICATOR&&L===r.CLUSTER_BREAK.REGIONAL_INDICATOR?u:N}};X.default=D});var H=l(I=>{"use strict";Object.defineProperty(I,"__esModule",{value:!0});var P=class{constructor(E,R){this._index=0,this._str=E,this._nextBreak=R}[Symbol.iterator](){return this}next(){let E;if((E=this._nextBreak(this._str,this._index))<this._str.length){let R=this._str.slice(this._index,E);return this._index=E,{value:R,done:!1}}if(this._index<this._str.length){let R=this._str.slice(this._index);return this._index=this._str.length,{value:R,done:!1}}return{value:void 0,done:!0}}};I.default=P});var g=l(b=>{"use strict";var h=b&&b.__importDefault||function(x){return x&&x.__esModule?x:{default:x}};Object.defineProperty(b,"__esModule",{value:!0});var f=V(),U=h(O()),Q=h(H()),C=class{static nextBreak(E,R){if(R===void 0&&(R=0),R<0)return 0;if(R>=E.length-1)return E.length;let i=U.default.codePointAt(E,R),T=C.getGraphemeBreakProperty(i),t=C.getEmojiProperty(i),_=[],n=[];for(let S=R+1;S<E.length;S++){if(U.default.isSurrogate(E,S-1))continue;let A=U.default.codePointAt(E,S),L=C.getGraphemeBreakProperty(A),B=C.getEmojiProperty(A);if(U.default.shouldBreak(T,_,L,t,n,B))return S;_.push(L),n.push(B)}return E.length}splitGraphemes(E){let R=[],i=0,T;for(;(T=C.nextBreak(E,i))<E.length;)R.push(E.slice(i,T)),i=T;return i<E.length&&R.push(E.slice(i)),R}iterateGraphemes(E){return new Q.default(E,C.nextBreak)}countGraphemes(E){let R=0,i=0,T;for(;(T=C.nextBreak(E,i))<E.length;)i=T,R++;return i<E.length&&R++,R}static getGraphemeBreakProperty(E){if(E<48905){if(E<44116){if(E<4141){if(E<2818){if(E<2363)if(E<1759){if(E<1471){if(E<127){if(E<11){if(E<10){if(0<=E&&E<=9)return f.CLUSTER_BREAK.CONTROL}else if(E===10)return f.CLUSTER_BREAK.LF}else if(E<13){if(11<=E&&E<=12)return f.CLUSTER_BREAK.CONTROL}else if(E<14){if(E===13)return f.CLUSTER_BREAK.CR}else if(14<=E&&E<=31)return f.CLUSTER_BREAK.CONTROL}else if(E<768){if(E<173){if(127<=E&&E<=159)return f.CLUSTER_BREAK.CONTROL}else if(E===173)return f.CLUSTER_BREAK.CONTROL}else if(E<1155){if(768<=E&&E<=879)return f.CLUSTER_BREAK.EXTEND}else if(E<1425){if(1155<=E&&E<=1161)return f.CLUSTER_BREAK.EXTEND}else if(1425<=E&&E<=1469)return f.CLUSTER_BREAK.EXTEND}else if(E<1552){if(E<1476){if(E<1473){if(E===1471)return f.CLUSTER_BREAK.EXTEND}else if(1473<=E&&E<=1474)return f.CLUSTER_BREAK.EXTEND}else if(E<1479){if(1476<=E&&E<=1477)return f.CLUSTER_BREAK.EXTEND}else if(E<1536){if(E===1479)return f.CLUSTER_BREAK.EXTEND}else if(1536<=E&&E<=1541)return f.CLUSTER_BREAK.PREPEND}else if(E<1648){if(E<1564){if(1552<=E&&E<=1562)return f.CLUSTER_BREAK.EXTEND}else if(E<1611){if(E===1564)return f.CLUSTER_BREAK.CONTROL}else if(1611<=E&&E<=1631)return f.CLUSTER_BREAK.EXTEND}else if(E<1750){if(E===1648)return f.CLUSTER_BREAK.EXTEND}else if(E<1757){if(1750<=E&&E<=1756)return f.CLUSTER_BREAK.EXTEND}else if(E===1757)return f.CLUSTER_BREAK.PREPEND}else if(E<2075){if(E<1840)if(E<1770){if(E<1767){if(1759<=E&&E<=1764)return f.CLUSTER_BREAK.EXTEND}else if(1767<=E&&E<=1768)return f.CLUSTER_BREAK.EXTEND}else if(E<1807){if(1770<=E&&E<=1773)return f.CLUSTER_BREAK.EXTEND}else{if(E===1807)return f.CLUSTER_BREAK.PREPEND;if(E===1809)return f.CLUSTER_BREAK.EXTEND}else if(E<2027){if(E<1958){if(1840<=E&&E<=1866)return f.CLUSTER_BREAK.EXTEND}else if(1958<=E&&E<=1968)return f.CLUSTER_BREAK.EXTEND}else if(E<2045){if(2027<=E&&E<=2035)return f.CLUSTER_BREAK.EXTEND}else if(E<2070){if(E===2045)return f.CLUSTER_BREAK.EXTEND}else if(2070<=E&&E<=2073)return f.CLUSTER_BREAK.EXTEND}else if(E<2200){if(E<2089){if(E<2085){if(2075<=E&&E<=2083)return f.CLUSTER_BREAK.EXTEND}else if(2085<=E&&E<=2087)return f.CLUSTER_BREAK.EXTEND}else if(E<2137){if(2089<=E&&E<=2093)return f.CLUSTER_BREAK.EXTEND}else if(E<2192){if(2137<=E&&E<=2139)return f.CLUSTER_BREAK.EXTEND}else if(2192<=E&&E<=2193)return f.CLUSTER_BREAK.PREPEND}else if(E<2275){if(E<2250){if(2200<=E&&E<=2207)return f.CLUSTER_BREAK.EXTEND}else if(E<2274){if(2250<=E&&E<=2273)return f.CLUSTER_BREAK.EXTEND}else if(E===2274)return f.CLUSTER_BREAK.PREPEND}else if(E<2307){if(2275<=E&&E<=2306)return f.CLUSTER_BREAK.EXTEND}else{if(E===2307)return f.CLUSTER_BREAK.SPACINGMARK;if(E===2362)return f.CLUSTER_BREAK.EXTEND}else if(E<2561){if(E<2434){if(E<2381){if(E<2366){if(E===2363)return f.CLUSTER_BREAK.SPACINGMARK;if(E===2364)return f.CLUSTER_BREAK.EXTEND}else if(E<2369){if(2366<=E&&E<=2368)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<2377){if(2369<=E&&E<=2376)return f.CLUSTER_BREAK.EXTEND}else if(2377<=E&&E<=2380)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<2385){if(E<2382){if(E===2381)return f.CLUSTER_BREAK.EXTEND}else if(2382<=E&&E<=2383)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<2402){if(2385<=E&&E<=2391)return f.CLUSTER_BREAK.EXTEND}else if(E<2433){if(2402<=E&&E<=2403)return f.CLUSTER_BREAK.EXTEND}else if(E===2433)return f.CLUSTER_BREAK.EXTEND}else if(E<2503){if(E<2494){if(E<2492){if(2434<=E&&E<=2435)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===2492)return f.CLUSTER_BREAK.EXTEND}else if(E<2495){if(E===2494)return f.CLUSTER_BREAK.EXTEND}else if(E<2497){if(2495<=E&&E<=2496)return f.CLUSTER_BREAK.SPACINGMARK}else if(2497<=E&&E<=2500)return f.CLUSTER_BREAK.EXTEND}else if(E<2519){if(E<2507){if(2503<=E&&E<=2504)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<2509){if(2507<=E&&E<=2508)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===2509)return f.CLUSTER_BREAK.EXTEND}else if(E<2530){if(E===2519)return f.CLUSTER_BREAK.EXTEND}else if(E<2558){if(2530<=E&&E<=2531)return f.CLUSTER_BREAK.EXTEND}else if(E===2558)return f.CLUSTER_BREAK.EXTEND}else if(E<2691){if(E<2631){if(E<2620){if(E<2563){if(2561<=E&&E<=2562)return f.CLUSTER_BREAK.EXTEND}else if(E===2563)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<2622){if(E===2620)return f.CLUSTER_BREAK.EXTEND}else if(E<2625){if(2622<=E&&E<=2624)return f.CLUSTER_BREAK.SPACINGMARK}else if(2625<=E&&E<=2626)return f.CLUSTER_BREAK.EXTEND}else if(E<2672){if(E<2635){if(2631<=E&&E<=2632)return f.CLUSTER_BREAK.EXTEND}else if(E<2641){if(2635<=E&&E<=2637)return f.CLUSTER_BREAK.EXTEND}else if(E===2641)return f.CLUSTER_BREAK.EXTEND}else if(E<2677){if(2672<=E&&E<=2673)return f.CLUSTER_BREAK.EXTEND}else if(E<2689){if(E===2677)return f.CLUSTER_BREAK.EXTEND}else if(2689<=E&&E<=2690)return f.CLUSTER_BREAK.EXTEND}else if(E<2761){if(E<2750){if(E===2691)return f.CLUSTER_BREAK.SPACINGMARK;if(E===2748)return f.CLUSTER_BREAK.EXTEND}else if(E<2753){if(2750<=E&&E<=2752)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<2759){if(2753<=E&&E<=2757)return f.CLUSTER_BREAK.EXTEND}else if(2759<=E&&E<=2760)return f.CLUSTER_BREAK.EXTEND}else if(E<2786){if(E<2763){if(E===2761)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<2765){if(2763<=E&&E<=2764)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===2765)return f.CLUSTER_BREAK.EXTEND}else if(E<2810){if(2786<=E&&E<=2787)return f.CLUSTER_BREAK.EXTEND}else if(E<2817){if(2810<=E&&E<=2815)return f.CLUSTER_BREAK.EXTEND}else if(E===2817)return f.CLUSTER_BREAK.EXTEND}else if(E<3315){if(E<3076){if(E<2946){if(E<2887){if(E<2878){if(E<2876){if(2818<=E&&E<=2819)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===2876)return f.CLUSTER_BREAK.EXTEND}else if(E<2880){if(2878<=E&&E<=2879)return f.CLUSTER_BREAK.EXTEND}else if(E<2881){if(E===2880)return f.CLUSTER_BREAK.SPACINGMARK}else if(2881<=E&&E<=2884)return f.CLUSTER_BREAK.EXTEND}else if(E<2893){if(E<2891){if(2887<=E&&E<=2888)return f.CLUSTER_BREAK.SPACINGMARK}else if(2891<=E&&E<=2892)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<2901){if(E===2893)return f.CLUSTER_BREAK.EXTEND}else if(E<2914){if(2901<=E&&E<=2903)return f.CLUSTER_BREAK.EXTEND}else if(2914<=E&&E<=2915)return f.CLUSTER_BREAK.EXTEND}else if(E<3014){if(E<3007){if(E===2946||E===3006)return f.CLUSTER_BREAK.EXTEND}else if(E<3008){if(E===3007)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3009){if(E===3008)return f.CLUSTER_BREAK.EXTEND}else if(3009<=E&&E<=3010)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3031){if(E<3018){if(3014<=E&&E<=3016)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3021){if(3018<=E&&E<=3020)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===3021)return f.CLUSTER_BREAK.EXTEND}else if(E<3072){if(E===3031)return f.CLUSTER_BREAK.EXTEND}else if(E<3073){if(E===3072)return f.CLUSTER_BREAK.EXTEND}else if(3073<=E&&E<=3075)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3262){if(E<3146){if(E<3134){if(E===3076||E===3132)return f.CLUSTER_BREAK.EXTEND}else if(E<3137){if(3134<=E&&E<=3136)return f.CLUSTER_BREAK.EXTEND}else if(E<3142){if(3137<=E&&E<=3140)return f.CLUSTER_BREAK.SPACINGMARK}else if(3142<=E&&E<=3144)return f.CLUSTER_BREAK.EXTEND}else if(E<3201){if(E<3157){if(3146<=E&&E<=3149)return f.CLUSTER_BREAK.EXTEND}else if(E<3170){if(3157<=E&&E<=3158)return f.CLUSTER_BREAK.EXTEND}else if(3170<=E&&E<=3171)return f.CLUSTER_BREAK.EXTEND}else if(E<3202){if(E===3201)return f.CLUSTER_BREAK.EXTEND}else if(E<3260){if(3202<=E&&E<=3203)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===3260)return f.CLUSTER_BREAK.EXTEND}else if(E<3270){if(E<3264){if(E===3262)return f.CLUSTER_BREAK.SPACINGMARK;if(E===3263)return f.CLUSTER_BREAK.EXTEND}else if(E<3266){if(3264<=E&&E<=3265)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3267){if(E===3266)return f.CLUSTER_BREAK.EXTEND}else if(3267<=E&&E<=3268)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3276){if(E<3271){if(E===3270)return f.CLUSTER_BREAK.EXTEND}else if(E<3274){if(3271<=E&&E<=3272)return f.CLUSTER_BREAK.SPACINGMARK}else if(3274<=E&&E<=3275)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3285){if(3276<=E&&E<=3277)return f.CLUSTER_BREAK.EXTEND}else if(E<3298){if(3285<=E&&E<=3286)return f.CLUSTER_BREAK.EXTEND}else if(3298<=E&&E<=3299)return f.CLUSTER_BREAK.EXTEND}else if(E<3551){if(E<3406){if(E<3391){if(E<3330){if(E<3328){if(E===3315)return f.CLUSTER_BREAK.SPACINGMARK}else if(3328<=E&&E<=3329)return f.CLUSTER_BREAK.EXTEND}else if(E<3387){if(3330<=E&&E<=3331)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3390){if(3387<=E&&E<=3388)return f.CLUSTER_BREAK.EXTEND}else if(E===3390)return f.CLUSTER_BREAK.EXTEND}else if(E<3398){if(E<3393){if(3391<=E&&E<=3392)return f.CLUSTER_BREAK.SPACINGMARK}else if(3393<=E&&E<=3396)return f.CLUSTER_BREAK.EXTEND}else if(E<3402){if(3398<=E&&E<=3400)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3405){if(3402<=E&&E<=3404)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===3405)return f.CLUSTER_BREAK.EXTEND}else if(E<3530){if(E<3426){if(E===3406)return f.CLUSTER_BREAK.PREPEND;if(E===3415)return f.CLUSTER_BREAK.EXTEND}else if(E<3457){if(3426<=E&&E<=3427)return f.CLUSTER_BREAK.EXTEND}else if(E<3458){if(E===3457)return f.CLUSTER_BREAK.EXTEND}else if(3458<=E&&E<=3459)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3538){if(E<3535){if(E===3530)return f.CLUSTER_BREAK.EXTEND}else if(E<3536){if(E===3535)return f.CLUSTER_BREAK.EXTEND}else if(3536<=E&&E<=3537)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3542){if(3538<=E&&E<=3540)return f.CLUSTER_BREAK.EXTEND}else if(E<3544){if(E===3542)return f.CLUSTER_BREAK.EXTEND}else if(3544<=E&&E<=3550)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3893){if(E<3655){if(E<3633){if(E<3570){if(E===3551)return f.CLUSTER_BREAK.EXTEND}else if(3570<=E&&E<=3571)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3635){if(E===3633)return f.CLUSTER_BREAK.EXTEND}else if(E<3636){if(E===3635)return f.CLUSTER_BREAK.SPACINGMARK}else if(3636<=E&&E<=3642)return f.CLUSTER_BREAK.EXTEND}else if(E<3764)if(E<3761){if(3655<=E&&E<=3662)return f.CLUSTER_BREAK.EXTEND}else{if(E===3761)return f.CLUSTER_BREAK.EXTEND;if(E===3763)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3784){if(3764<=E&&E<=3772)return f.CLUSTER_BREAK.EXTEND}else if(E<3864){if(3784<=E&&E<=3790)return f.CLUSTER_BREAK.EXTEND}else if(3864<=E&&E<=3865)return f.CLUSTER_BREAK.EXTEND}else if(E<3967){if(E<3897){if(E===3893||E===3895)return f.CLUSTER_BREAK.EXTEND}else if(E<3902){if(E===3897)return f.CLUSTER_BREAK.EXTEND}else if(E<3953){if(3902<=E&&E<=3903)return f.CLUSTER_BREAK.SPACINGMARK}else if(3953<=E&&E<=3966)return f.CLUSTER_BREAK.EXTEND}else if(E<3981){if(E<3968){if(E===3967)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3974){if(3968<=E&&E<=3972)return f.CLUSTER_BREAK.EXTEND}else if(3974<=E&&E<=3975)return f.CLUSTER_BREAK.EXTEND}else if(E<3993){if(3981<=E&&E<=3991)return f.CLUSTER_BREAK.EXTEND}else if(E<4038){if(3993<=E&&E<=4028)return f.CLUSTER_BREAK.EXTEND}else if(E===4038)return f.CLUSTER_BREAK.EXTEND}else if(E<7204){if(E<6448){if(E<5938){if(E<4226){if(E<4157){if(E<4146){if(E<4145){if(4141<=E&&E<=4144)return f.CLUSTER_BREAK.EXTEND}else if(E===4145)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<4153){if(4146<=E&&E<=4151)return f.CLUSTER_BREAK.EXTEND}else if(E<4155){if(4153<=E&&E<=4154)return f.CLUSTER_BREAK.EXTEND}else if(4155<=E&&E<=4156)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<4184){if(E<4182){if(4157<=E&&E<=4158)return f.CLUSTER_BREAK.EXTEND}else if(4182<=E&&E<=4183)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<4190){if(4184<=E&&E<=4185)return f.CLUSTER_BREAK.EXTEND}else if(E<4209){if(4190<=E&&E<=4192)return f.CLUSTER_BREAK.EXTEND}else if(4209<=E&&E<=4212)return f.CLUSTER_BREAK.EXTEND}else if(E<4352){if(E<4229){if(E===4226)return f.CLUSTER_BREAK.EXTEND;if(E===4228)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<4237){if(4229<=E&&E<=4230)return f.CLUSTER_BREAK.EXTEND}else if(E===4237||E===4253)return f.CLUSTER_BREAK.EXTEND}else if(E<4957){if(E<4448){if(4352<=E&&E<=4447)return f.CLUSTER_BREAK.L}else if(E<4520){if(4448<=E&&E<=4519)return f.CLUSTER_BREAK.V}else if(4520<=E&&E<=4607)return f.CLUSTER_BREAK.T}else if(E<5906){if(4957<=E&&E<=4959)return f.CLUSTER_BREAK.EXTEND}else if(E<5909){if(5906<=E&&E<=5908)return f.CLUSTER_BREAK.EXTEND}else if(E===5909)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<6089){if(E<6070){if(E<5970){if(E<5940){if(5938<=E&&E<=5939)return f.CLUSTER_BREAK.EXTEND}else if(E===5940)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<6002){if(5970<=E&&E<=5971)return f.CLUSTER_BREAK.EXTEND}else if(E<6068){if(6002<=E&&E<=6003)return f.CLUSTER_BREAK.EXTEND}else if(6068<=E&&E<=6069)return f.CLUSTER_BREAK.EXTEND}else if(E<6078){if(E<6071){if(E===6070)return f.CLUSTER_BREAK.SPACINGMARK}else if(6071<=E&&E<=6077)return f.CLUSTER_BREAK.EXTEND}else if(E<6086){if(6078<=E&&E<=6085)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<6087){if(E===6086)return f.CLUSTER_BREAK.EXTEND}else if(6087<=E&&E<=6088)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<6277)if(E<6155){if(E<6109){if(6089<=E&&E<=6099)return f.CLUSTER_BREAK.EXTEND}else if(E===6109)return f.CLUSTER_BREAK.EXTEND}else if(E<6158){if(6155<=E&&E<=6157)return f.CLUSTER_BREAK.EXTEND}else{if(E===6158)return f.CLUSTER_BREAK.CONTROL;if(E===6159)return f.CLUSTER_BREAK.EXTEND}else if(E<6435){if(E<6313){if(6277<=E&&E<=6278)return f.CLUSTER_BREAK.EXTEND}else if(E<6432){if(E===6313)return f.CLUSTER_BREAK.EXTEND}else if(6432<=E&&E<=6434)return f.CLUSTER_BREAK.EXTEND}else if(E<6439){if(6435<=E&&E<=6438)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<6441){if(6439<=E&&E<=6440)return f.CLUSTER_BREAK.EXTEND}else if(6441<=E&&E<=6443)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<6971){if(E<6744)if(E<6681){if(E<6451){if(E<6450){if(6448<=E&&E<=6449)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===6450)return f.CLUSTER_BREAK.EXTEND}else if(E<6457){if(6451<=E&&E<=6456)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<6679){if(6457<=E&&E<=6459)return f.CLUSTER_BREAK.EXTEND}else if(6679<=E&&E<=6680)return f.CLUSTER_BREAK.EXTEND}else if(E<6741){if(E<6683){if(6681<=E&&E<=6682)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===6683)return f.CLUSTER_BREAK.EXTEND}else if(E<6742){if(E===6741)return f.CLUSTER_BREAK.SPACINGMARK}else{if(E===6742)return f.CLUSTER_BREAK.EXTEND;if(E===6743)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<6771){if(E<6754){if(E<6752){if(6744<=E&&E<=6750)return f.CLUSTER_BREAK.EXTEND}else if(E===6752)return f.CLUSTER_BREAK.EXTEND}else if(E<6757){if(E===6754)return f.CLUSTER_BREAK.EXTEND}else if(E<6765){if(6757<=E&&E<=6764)return f.CLUSTER_BREAK.EXTEND}else if(6765<=E&&E<=6770)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<6912){if(E<6783){if(6771<=E&&E<=6780)return f.CLUSTER_BREAK.EXTEND}else if(E<6832){if(E===6783)return f.CLUSTER_BREAK.EXTEND}else if(6832<=E&&E<=6862)return f.CLUSTER_BREAK.EXTEND}else if(E<6916){if(6912<=E&&E<=6915)return f.CLUSTER_BREAK.EXTEND}else if(E<6964){if(E===6916)return f.CLUSTER_BREAK.SPACINGMARK}else if(6964<=E&&E<=6970)return f.CLUSTER_BREAK.EXTEND}else if(E<7080){if(E<7019){if(E<6973){if(E===6971)return f.CLUSTER_BREAK.SPACINGMARK;if(E===6972)return f.CLUSTER_BREAK.EXTEND}else if(E<6978){if(6973<=E&&E<=6977)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<6979){if(E===6978)return f.CLUSTER_BREAK.EXTEND}else if(6979<=E&&E<=6980)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<7073){if(E<7040){if(7019<=E&&E<=7027)return f.CLUSTER_BREAK.EXTEND}else if(E<7042){if(7040<=E&&E<=7041)return f.CLUSTER_BREAK.EXTEND}else if(E===7042)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<7074){if(E===7073)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<7078){if(7074<=E&&E<=7077)return f.CLUSTER_BREAK.EXTEND}else if(7078<=E&&E<=7079)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<7144)if(E<7083){if(E<7082){if(7080<=E&&E<=7081)return f.CLUSTER_BREAK.EXTEND}else if(E===7082)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<7142){if(7083<=E&&E<=7085)return f.CLUSTER_BREAK.EXTEND}else{if(E===7142)return f.CLUSTER_BREAK.EXTEND;if(E===7143)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<7150){if(E<7146){if(7144<=E&&E<=7145)return f.CLUSTER_BREAK.EXTEND}else if(E<7149){if(7146<=E&&E<=7148)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===7149)return f.CLUSTER_BREAK.EXTEND}else if(E<7151){if(E===7150)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<7154){if(7151<=E&&E<=7153)return f.CLUSTER_BREAK.EXTEND}else if(7154<=E&&E<=7155)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<43346){if(E<11647){if(E<7415){if(E<7380){if(E<7220){if(E<7212){if(7204<=E&&E<=7211)return f.CLUSTER_BREAK.SPACINGMARK}else if(7212<=E&&E<=7219)return f.CLUSTER_BREAK.EXTEND}else if(E<7222){if(7220<=E&&E<=7221)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<7376){if(7222<=E&&E<=7223)return f.CLUSTER_BREAK.EXTEND}else if(7376<=E&&E<=7378)return f.CLUSTER_BREAK.EXTEND}else if(E<7394){if(E<7393){if(7380<=E&&E<=7392)return f.CLUSTER_BREAK.EXTEND}else if(E===7393)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<7405){if(7394<=E&&E<=7400)return f.CLUSTER_BREAK.EXTEND}else if(E===7405||E===7412)return f.CLUSTER_BREAK.EXTEND}else if(E<8205)if(E<7616){if(E<7416){if(E===7415)return f.CLUSTER_BREAK.SPACINGMARK}else if(7416<=E&&E<=7417)return f.CLUSTER_BREAK.EXTEND}else if(E<8203){if(7616<=E&&E<=7679)return f.CLUSTER_BREAK.EXTEND}else{if(E===8203)return f.CLUSTER_BREAK.CONTROL;if(E===8204)return f.CLUSTER_BREAK.EXTEND}else if(E<8288){if(E<8206){if(E===8205)return f.CLUSTER_BREAK.ZWJ}else if(E<8232){if(8206<=E&&E<=8207)return f.CLUSTER_BREAK.CONTROL}else if(8232<=E&&E<=8238)return f.CLUSTER_BREAK.CONTROL}else if(E<8400){if(8288<=E&&E<=8303)return f.CLUSTER_BREAK.CONTROL}else if(E<11503){if(8400<=E&&E<=8432)return f.CLUSTER_BREAK.EXTEND}else if(11503<=E&&E<=11505)return f.CLUSTER_BREAK.EXTEND}else if(E<43043){if(E<42612){if(E<12330){if(E<11744){if(E===11647)return f.CLUSTER_BREAK.EXTEND}else if(11744<=E&&E<=11775)return f.CLUSTER_BREAK.EXTEND}else if(E<12441){if(12330<=E&&E<=12335)return f.CLUSTER_BREAK.EXTEND}else if(E<42607){if(12441<=E&&E<=12442)return f.CLUSTER_BREAK.EXTEND}else if(42607<=E&&E<=42610)return f.CLUSTER_BREAK.EXTEND}else if(E<43010){if(E<42654){if(42612<=E&&E<=42621)return f.CLUSTER_BREAK.EXTEND}else if(E<42736){if(42654<=E&&E<=42655)return f.CLUSTER_BREAK.EXTEND}else if(42736<=E&&E<=42737)return f.CLUSTER_BREAK.EXTEND}else if(E<43014){if(E===43010)return f.CLUSTER_BREAK.EXTEND}else if(E===43014||E===43019)return f.CLUSTER_BREAK.EXTEND}else if(E<43188){if(E<43047){if(E<43045){if(43043<=E&&E<=43044)return f.CLUSTER_BREAK.SPACINGMARK}else if(43045<=E&&E<=43046)return f.CLUSTER_BREAK.EXTEND}else if(E<43052){if(E===43047)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<43136){if(E===43052)return f.CLUSTER_BREAK.EXTEND}else if(43136<=E&&E<=43137)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<43263){if(E<43204){if(43188<=E&&E<=43203)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<43232){if(43204<=E&&E<=43205)return f.CLUSTER_BREAK.EXTEND}else if(43232<=E&&E<=43249)return f.CLUSTER_BREAK.EXTEND}else if(E<43302){if(E===43263)return f.CLUSTER_BREAK.EXTEND}else if(E<43335){if(43302<=E&&E<=43309)return f.CLUSTER_BREAK.EXTEND}else if(43335<=E&&E<=43345)return f.CLUSTER_BREAK.EXTEND}else if(E<43698){if(E<43493){if(E<43444)if(E<43392){if(E<43360){if(43346<=E&&E<=43347)return f.CLUSTER_BREAK.SPACINGMARK}else if(43360<=E&&E<=43388)return f.CLUSTER_BREAK.L}else if(E<43395){if(43392<=E&&E<=43394)return f.CLUSTER_BREAK.EXTEND}else{if(E===43395)return f.CLUSTER_BREAK.SPACINGMARK;if(E===43443)return f.CLUSTER_BREAK.EXTEND}else if(E<43450){if(E<43446){if(43444<=E&&E<=43445)return f.CLUSTER_BREAK.SPACINGMARK}else if(43446<=E&&E<=43449)return f.CLUSTER_BREAK.EXTEND}else if(E<43452){if(43450<=E&&E<=43451)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<43454){if(43452<=E&&E<=43453)return f.CLUSTER_BREAK.EXTEND}else if(43454<=E&&E<=43456)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<43573){if(E<43567){if(E<43561){if(E===43493)return f.CLUSTER_BREAK.EXTEND}else if(43561<=E&&E<=43566)return f.CLUSTER_BREAK.EXTEND}else if(E<43569){if(43567<=E&&E<=43568)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<43571){if(43569<=E&&E<=43570)return f.CLUSTER_BREAK.EXTEND}else if(43571<=E&&E<=43572)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<43597){if(E<43587){if(43573<=E&&E<=43574)return f.CLUSTER_BREAK.EXTEND}else if(E===43587||E===43596)return f.CLUSTER_BREAK.EXTEND}else if(E<43644){if(E===43597)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===43644||E===43696)return f.CLUSTER_BREAK.EXTEND}else if(E<44006){if(E<43756)if(E<43710){if(E<43703){if(43698<=E&&E<=43700)return f.CLUSTER_BREAK.EXTEND}else if(43703<=E&&E<=43704)return f.CLUSTER_BREAK.EXTEND}else if(E<43713){if(43710<=E&&E<=43711)return f.CLUSTER_BREAK.EXTEND}else{if(E===43713)return f.CLUSTER_BREAK.EXTEND;if(E===43755)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<43766){if(E<43758){if(43756<=E&&E<=43757)return f.CLUSTER_BREAK.EXTEND}else if(E<43765){if(43758<=E&&E<=43759)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===43765)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<44003){if(E===43766)return f.CLUSTER_BREAK.EXTEND}else if(E<44005){if(44003<=E&&E<=44004)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===44005)return f.CLUSTER_BREAK.EXTEND}else if(E<44032)if(E<44009){if(E<44008){if(44006<=E&&E<=44007)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===44008)return f.CLUSTER_BREAK.EXTEND}else if(E<44012){if(44009<=E&&E<=44010)return f.CLUSTER_BREAK.SPACINGMARK}else{if(E===44012)return f.CLUSTER_BREAK.SPACINGMARK;if(E===44013)return f.CLUSTER_BREAK.EXTEND}else if(E<44061){if(E<44033){if(E===44032)return f.CLUSTER_BREAK.LV}else if(E<44060){if(44033<=E&&E<=44059)return f.CLUSTER_BREAK.LVT}else if(E===44060)return f.CLUSTER_BREAK.LV}else if(E<44088){if(44061<=E&&E<=44087)return f.CLUSTER_BREAK.LVT}else if(E<44089){if(E===44088)return f.CLUSTER_BREAK.LV}else if(44089<=E&&E<=44115)return f.CLUSTER_BREAK.LVT}else if(E<46497){if(E<45293){if(E<44704){if(E<44397){if(E<44256){if(E<44173){if(E<44144){if(E<44117){if(E===44116)return f.CLUSTER_BREAK.LV}else if(44117<=E&&E<=44143)return f.CLUSTER_BREAK.LVT}else if(E<44145){if(E===44144)return f.CLUSTER_BREAK.LV}else if(E<44172){if(44145<=E&&E<=44171)return f.CLUSTER_BREAK.LVT}else if(E===44172)return f.CLUSTER_BREAK.LV}else if(E<44201){if(E<44200){if(44173<=E&&E<=44199)return f.CLUSTER_BREAK.LVT}else if(E===44200)return f.CLUSTER_BREAK.LV}else if(E<44228){if(44201<=E&&E<=44227)return f.CLUSTER_BREAK.LVT}else if(E<44229){if(E===44228)return f.CLUSTER_BREAK.LV}else if(44229<=E&&E<=44255)return f.CLUSTER_BREAK.LVT}else if(E<44313){if(E<44284){if(E<44257){if(E===44256)return f.CLUSTER_BREAK.LV}else if(44257<=E&&E<=44283)return f.CLUSTER_BREAK.LVT}else if(E<44285){if(E===44284)return f.CLUSTER_BREAK.LV}else if(E<44312){if(44285<=E&&E<=44311)return f.CLUSTER_BREAK.LVT}else if(E===44312)return f.CLUSTER_BREAK.LV}else if(E<44368){if(E<44340){if(44313<=E&&E<=44339)return f.CLUSTER_BREAK.LVT}else if(E<44341){if(E===44340)return f.CLUSTER_BREAK.LV}else if(44341<=E&&E<=44367)return f.CLUSTER_BREAK.LVT}else if(E<44369){if(E===44368)return f.CLUSTER_BREAK.LV}else if(E<44396){if(44369<=E&&E<=44395)return f.CLUSTER_BREAK.LVT}else if(E===44396)return f.CLUSTER_BREAK.LV}else if(E<44537){if(E<44480){if(E<44425){if(E<44424){if(44397<=E&&E<=44423)return f.CLUSTER_BREAK.LVT}else if(E===44424)return f.CLUSTER_BREAK.LV}else if(E<44452){if(44425<=E&&E<=44451)return f.CLUSTER_BREAK.LVT}else if(E<44453){if(E===44452)return f.CLUSTER_BREAK.LV}else if(44453<=E&&E<=44479)return f.CLUSTER_BREAK.LVT}else if(E<44508){if(E<44481){if(E===44480)return f.CLUSTER_BREAK.LV}else if(44481<=E&&E<=44507)return f.CLUSTER_BREAK.LVT}else if(E<44509){if(E===44508)return f.CLUSTER_BREAK.LV}else if(E<44536){if(44509<=E&&E<=44535)return f.CLUSTER_BREAK.LVT}else if(E===44536)return f.CLUSTER_BREAK.LV}else if(E<44620){if(E<44565){if(E<44564){if(44537<=E&&E<=44563)return f.CLUSTER_BREAK.LVT}else if(E===44564)return f.CLUSTER_BREAK.LV}else if(E<44592){if(44565<=E&&E<=44591)return f.CLUSTER_BREAK.LVT}else if(E<44593){if(E===44592)return f.CLUSTER_BREAK.LV}else if(44593<=E&&E<=44619)return f.CLUSTER_BREAK.LVT}else if(E<44649){if(E<44621){if(E===44620)return f.CLUSTER_BREAK.LV}else if(E<44648){if(44621<=E&&E<=44647)return f.CLUSTER_BREAK.LVT}else if(E===44648)return f.CLUSTER_BREAK.LV}else if(E<44676){if(44649<=E&&E<=44675)return f.CLUSTER_BREAK.LVT}else if(E<44677){if(E===44676)return f.CLUSTER_BREAK.LV}else if(44677<=E&&E<=44703)return f.CLUSTER_BREAK.LVT}else if(E<44985){if(E<44844){if(E<44761){if(E<44732){if(E<44705){if(E===44704)return f.CLUSTER_BREAK.LV}else if(44705<=E&&E<=44731)return f.CLUSTER_BREAK.LVT}else if(E<44733){if(E===44732)return f.CLUSTER_BREAK.LV}else if(E<44760){if(44733<=E&&E<=44759)return f.CLUSTER_BREAK.LVT}else if(E===44760)return f.CLUSTER_BREAK.LV}else if(E<44789){if(E<44788){if(44761<=E&&E<=44787)return f.CLUSTER_BREAK.LVT}else if(E===44788)return f.CLUSTER_BREAK.LV}else if(E<44816){if(44789<=E&&E<=44815)return f.CLUSTER_BREAK.LVT}else if(E<44817){if(E===44816)return f.CLUSTER_BREAK.LV}else if(44817<=E&&E<=44843)return f.CLUSTER_BREAK.LVT}else if(E<44901){if(E<44872){if(E<44845){if(E===44844)return f.CLUSTER_BREAK.LV}else if(44845<=E&&E<=44871)return f.CLUSTER_BREAK.LVT}else if(E<44873){if(E===44872)return f.CLUSTER_BREAK.LV}else if(E<44900){if(44873<=E&&E<=44899)return f.CLUSTER_BREAK.LVT}else if(E===44900)return f.CLUSTER_BREAK.LV}else if(E<44956){if(E<44928){if(44901<=E&&E<=44927)return f.CLUSTER_BREAK.LVT}else if(E<44929){if(E===44928)return f.CLUSTER_BREAK.LV}else if(44929<=E&&E<=44955)return f.CLUSTER_BREAK.LVT}else if(E<44957){if(E===44956)return f.CLUSTER_BREAK.LV}else if(E<44984){if(44957<=E&&E<=44983)return f.CLUSTER_BREAK.LVT}else if(E===44984)return f.CLUSTER_BREAK.LV}else if(E<45152){if(E<45068){if(E<45013){if(E<45012){if(44985<=E&&E<=45011)return f.CLUSTER_BREAK.LVT}else if(E===45012)return f.CLUSTER_BREAK.LV}else if(E<45040){if(45013<=E&&E<=45039)return f.CLUSTER_BREAK.LVT}else if(E<45041){if(E===45040)return f.CLUSTER_BREAK.LV}else if(45041<=E&&E<=45067)return f.CLUSTER_BREAK.LVT}else if(E<45097){if(E<45069){if(E===45068)return f.CLUSTER_BREAK.LV}else if(E<45096){if(45069<=E&&E<=45095)return f.CLUSTER_BREAK.LVT}else if(E===45096)return f.CLUSTER_BREAK.LV}else if(E<45124){if(45097<=E&&E<=45123)return f.CLUSTER_BREAK.LVT}else if(E<45125){if(E===45124)return f.CLUSTER_BREAK.LV}else if(45125<=E&&E<=45151)return f.CLUSTER_BREAK.LVT}else if(E<45209){if(E<45180){if(E<45153){if(E===45152)return f.CLUSTER_BREAK.LV}else if(45153<=E&&E<=45179)return f.CLUSTER_BREAK.LVT}else if(E<45181){if(E===45180)return f.CLUSTER_BREAK.LV}else if(E<45208){if(45181<=E&&E<=45207)return f.CLUSTER_BREAK.LVT}else if(E===45208)return f.CLUSTER_BREAK.LV}else if(E<45264){if(E<45236){if(45209<=E&&E<=45235)return f.CLUSTER_BREAK.LVT}else if(E<45237){if(E===45236)return f.CLUSTER_BREAK.LV}else if(45237<=E&&E<=45263)return f.CLUSTER_BREAK.LVT}else if(E<45265){if(E===45264)return f.CLUSTER_BREAK.LV}else if(E<45292){if(45265<=E&&E<=45291)return f.CLUSTER_BREAK.LVT}else if(E===45292)return f.CLUSTER_BREAK.LV}else if(E<45908){if(E<45600){if(E<45433){if(E<45376){if(E<45321){if(E<45320){if(45293<=E&&E<=45319)return f.CLUSTER_BREAK.LVT}else if(E===45320)return f.CLUSTER_BREAK.LV}else if(E<45348){if(45321<=E&&E<=45347)return f.CLUSTER_BREAK.LVT}else if(E<45349){if(E===45348)return f.CLUSTER_BREAK.LV}else if(45349<=E&&E<=45375)return f.CLUSTER_BREAK.LVT}else if(E<45404){if(E<45377){if(E===45376)return f.CLUSTER_BREAK.LV}else if(45377<=E&&E<=45403)return f.CLUSTER_BREAK.LVT}else if(E<45405){if(E===45404)return f.CLUSTER_BREAK.LV}else if(E<45432){if(45405<=E&&E<=45431)return f.CLUSTER_BREAK.LVT}else if(E===45432)return f.CLUSTER_BREAK.LV}else if(E<45516){if(E<45461){if(E<45460){if(45433<=E&&E<=45459)return f.CLUSTER_BREAK.LVT}else if(E===45460)return f.CLUSTER_BREAK.LV}else if(E<45488){if(45461<=E&&E<=45487)return f.CLUSTER_BREAK.LVT}else if(E<45489){if(E===45488)return f.CLUSTER_BREAK.LV}else if(45489<=E&&E<=45515)return f.CLUSTER_BREAK.LVT}else if(E<45545){if(E<45517){if(E===45516)return f.CLUSTER_BREAK.LV}else if(E<45544){if(45517<=E&&E<=45543)return f.CLUSTER_BREAK.LVT}else if(E===45544)return f.CLUSTER_BREAK.LV}else if(E<45572){if(45545<=E&&E<=45571)return f.CLUSTER_BREAK.LVT}else if(E<45573){if(E===45572)return f.CLUSTER_BREAK.LV}else if(45573<=E&&E<=45599)return f.CLUSTER_BREAK.LVT}else if(E<45741){if(E<45657){if(E<45628){if(E<45601){if(E===45600)return f.CLUSTER_BREAK.LV}else if(45601<=E&&E<=45627)return f.CLUSTER_BREAK.LVT}else if(E<45629){if(E===45628)return f.CLUSTER_BREAK.LV}else if(E<45656){if(45629<=E&&E<=45655)return f.CLUSTER_BREAK.LVT}else if(E===45656)return f.CLUSTER_BREAK.LV}else if(E<45712){if(E<45684){if(45657<=E&&E<=45683)return f.CLUSTER_BREAK.LVT}else if(E<45685){if(E===45684)return f.CLUSTER_BREAK.LV}else if(45685<=E&&E<=45711)return f.CLUSTER_BREAK.LVT}else if(E<45713){if(E===45712)return f.CLUSTER_BREAK.LV}else if(E<45740){if(45713<=E&&E<=45739)return f.CLUSTER_BREAK.LVT}else if(E===45740)return f.CLUSTER_BREAK.LV}else if(E<45824){if(E<45769){if(E<45768){if(45741<=E&&E<=45767)return f.CLUSTER_BREAK.LVT}else if(E===45768)return f.CLUSTER_BREAK.LV}else if(E<45796){if(45769<=E&&E<=45795)return f.CLUSTER_BREAK.LVT}else if(E<45797){if(E===45796)return f.CLUSTER_BREAK.LV}else if(45797<=E&&E<=45823)return f.CLUSTER_BREAK.LVT}else if(E<45853){if(E<45825){if(E===45824)return f.CLUSTER_BREAK.LV}else if(E<45852){if(45825<=E&&E<=45851)return f.CLUSTER_BREAK.LVT}else if(E===45852)return f.CLUSTER_BREAK.LV}else if(E<45880){if(45853<=E&&E<=45879)return f.CLUSTER_BREAK.LVT}else if(E<45881){if(E===45880)return f.CLUSTER_BREAK.LV}else if(45881<=E&&E<=45907)return f.CLUSTER_BREAK.LVT}else if(E<46189){if(E<46048){if(E<45965){if(E<45936){if(E<45909){if(E===45908)return f.CLUSTER_BREAK.LV}else if(45909<=E&&E<=45935)return f.CLUSTER_BREAK.LVT}else if(E<45937){if(E===45936)return f.CLUSTER_BREAK.LV}else if(E<45964){if(45937<=E&&E<=45963)return f.CLUSTER_BREAK.LVT}else if(E===45964)return f.CLUSTER_BREAK.LV}else if(E<45993){if(E<45992){if(45965<=E&&E<=45991)return f.CLUSTER_BREAK.LVT}else if(E===45992)return f.CLUSTER_BREAK.LV}else if(E<46020){if(45993<=E&&E<=46019)return f.CLUSTER_BREAK.LVT}else if(E<46021){if(E===46020)return f.CLUSTER_BREAK.LV}else if(46021<=E&&E<=46047)return f.CLUSTER_BREAK.LVT}else if(E<46105){if(E<46076){if(E<46049){if(E===46048)return f.CLUSTER_BREAK.LV}else if(46049<=E&&E<=46075)return f.CLUSTER_BREAK.LVT}else if(E<46077){if(E===46076)return f.CLUSTER_BREAK.LV}else if(E<46104){if(46077<=E&&E<=46103)return f.CLUSTER_BREAK.LVT}else if(E===46104)return f.CLUSTER_BREAK.LV}else if(E<46160){if(E<46132){if(46105<=E&&E<=46131)return f.CLUSTER_BREAK.LVT}else if(E<46133){if(E===46132)return f.CLUSTER_BREAK.LV}else if(46133<=E&&E<=46159)return f.CLUSTER_BREAK.LVT}else if(E<46161){if(E===46160)return f.CLUSTER_BREAK.LV}else if(E<46188){if(46161<=E&&E<=46187)return f.CLUSTER_BREAK.LVT}else if(E===46188)return f.CLUSTER_BREAK.LV}else if(E<46356){if(E<46272){if(E<46217){if(E<46216){if(46189<=E&&E<=46215)return f.CLUSTER_BREAK.LVT}else if(E===46216)return f.CLUSTER_BREAK.LV}else if(E<46244){if(46217<=E&&E<=46243)return f.CLUSTER_BREAK.LVT}else if(E<46245){if(E===46244)return f.CLUSTER_BREAK.LV}else if(46245<=E&&E<=46271)return f.CLUSTER_BREAK.LVT}else if(E<46301){if(E<46273){if(E===46272)return f.CLUSTER_BREAK.LV}else if(E<46300){if(46273<=E&&E<=46299)return f.CLUSTER_BREAK.LVT}else if(E===46300)return f.CLUSTER_BREAK.LV}else if(E<46328){if(46301<=E&&E<=46327)return f.CLUSTER_BREAK.LVT}else if(E<46329){if(E===46328)return f.CLUSTER_BREAK.LV}else if(46329<=E&&E<=46355)return f.CLUSTER_BREAK.LVT}else if(E<46413){if(E<46384){if(E<46357){if(E===46356)return f.CLUSTER_BREAK.LV}else if(46357<=E&&E<=46383)return f.CLUSTER_BREAK.LVT}else if(E<46385){if(E===46384)return f.CLUSTER_BREAK.LV}else if(E<46412){if(46385<=E&&E<=46411)return f.CLUSTER_BREAK.LVT}else if(E===46412)return f.CLUSTER_BREAK.LV}else if(E<46468){if(E<46440){if(46413<=E&&E<=46439)return f.CLUSTER_BREAK.LVT}else if(E<46441){if(E===46440)return f.CLUSTER_BREAK.LV}else if(46441<=E&&E<=46467)return f.CLUSTER_BREAK.LVT}else if(E<46469){if(E===46468)return f.CLUSTER_BREAK.LV}else if(E<46496){if(46469<=E&&E<=46495)return f.CLUSTER_BREAK.LVT}else if(E===46496)return f.CLUSTER_BREAK.LV}else if(E<47701){if(E<47112){if(E<46804){if(E<46637){if(E<46580){if(E<46525){if(E<46524){if(46497<=E&&E<=46523)return f.CLUSTER_BREAK.LVT}else if(E===46524)return f.CLUSTER_BREAK.LV}else if(E<46552){if(46525<=E&&E<=46551)return f.CLUSTER_BREAK.LVT}else if(E<46553){if(E===46552)return f.CLUSTER_BREAK.LV}else if(46553<=E&&E<=46579)return f.CLUSTER_BREAK.LVT}else if(E<46608){if(E<46581){if(E===46580)return f.CLUSTER_BREAK.LV}else if(46581<=E&&E<=46607)return f.CLUSTER_BREAK.LVT}else if(E<46609){if(E===46608)return f.CLUSTER_BREAK.LV}else if(E<46636){if(46609<=E&&E<=46635)return f.CLUSTER_BREAK.LVT}else if(E===46636)return f.CLUSTER_BREAK.LV}else if(E<46720){if(E<46665){if(E<46664){if(46637<=E&&E<=46663)return f.CLUSTER_BREAK.LVT}else if(E===46664)return f.CLUSTER_BREAK.LV}else if(E<46692){if(46665<=E&&E<=46691)return f.CLUSTER_BREAK.LVT}else if(E<46693){if(E===46692)return f.CLUSTER_BREAK.LV}else if(46693<=E&&E<=46719)return f.CLUSTER_BREAK.LVT}else if(E<46749){if(E<46721){if(E===46720)return f.CLUSTER_BREAK.LV}else if(E<46748){if(46721<=E&&E<=46747)return f.CLUSTER_BREAK.LVT}else if(E===46748)return f.CLUSTER_BREAK.LV}else if(E<46776){if(46749<=E&&E<=46775)return f.CLUSTER_BREAK.LVT}else if(E<46777){if(E===46776)return f.CLUSTER_BREAK.LV}else if(46777<=E&&E<=46803)return f.CLUSTER_BREAK.LVT}else if(E<46945){if(E<46861){if(E<46832){if(E<46805){if(E===46804)return f.CLUSTER_BREAK.LV}else if(46805<=E&&E<=46831)return f.CLUSTER_BREAK.LVT}else if(E<46833){if(E===46832)return f.CLUSTER_BREAK.LV}else if(E<46860){if(46833<=E&&E<=46859)return f.CLUSTER_BREAK.LVT}else if(E===46860)return f.CLUSTER_BREAK.LV}else if(E<46916){if(E<46888){if(46861<=E&&E<=46887)return f.CLUSTER_BREAK.LVT}else if(E<46889){if(E===46888)return f.CLUSTER_BREAK.LV}else if(46889<=E&&E<=46915)return f.CLUSTER_BREAK.LVT}else if(E<46917){if(E===46916)return f.CLUSTER_BREAK.LV}else if(E<46944){if(46917<=E&&E<=46943)return f.CLUSTER_BREAK.LVT}else if(E===46944)return f.CLUSTER_BREAK.LV}else if(E<47028){if(E<46973){if(E<46972){if(46945<=E&&E<=46971)return f.CLUSTER_BREAK.LVT}else if(E===46972)return f.CLUSTER_BREAK.LV}else if(E<47e3){if(46973<=E&&E<=46999)return f.CLUSTER_BREAK.LVT}else if(E<47001){if(E===47e3)return f.CLUSTER_BREAK.LV}else if(47001<=E&&E<=47027)return f.CLUSTER_BREAK.LVT}else if(E<47057){if(E<47029){if(E===47028)return f.CLUSTER_BREAK.LV}else if(E<47056){if(47029<=E&&E<=47055)return f.CLUSTER_BREAK.LVT}else if(E===47056)return f.CLUSTER_BREAK.LV}else if(E<47084){if(47057<=E&&E<=47083)return f.CLUSTER_BREAK.LVT}else if(E<47085){if(E===47084)return f.CLUSTER_BREAK.LV}else if(47085<=E&&E<=47111)return f.CLUSTER_BREAK.LVT}else if(E<47393){if(E<47252){if(E<47169){if(E<47140){if(E<47113){if(E===47112)return f.CLUSTER_BREAK.LV}else if(47113<=E&&E<=47139)return f.CLUSTER_BREAK.LVT}else if(E<47141){if(E===47140)return f.CLUSTER_BREAK.LV}else if(E<47168){if(47141<=E&&E<=47167)return f.CLUSTER_BREAK.LVT}else if(E===47168)return f.CLUSTER_BREAK.LV}else if(E<47197){if(E<47196){if(47169<=E&&E<=47195)return f.CLUSTER_BREAK.LVT}else if(E===47196)return f.CLUSTER_BREAK.LV}else if(E<47224){if(47197<=E&&E<=47223)return f.CLUSTER_BREAK.LVT}else if(E<47225){if(E===47224)return f.CLUSTER_BREAK.LV}else if(47225<=E&&E<=47251)return f.CLUSTER_BREAK.LVT}else if(E<47309){if(E<47280){if(E<47253){if(E===47252)return f.CLUSTER_BREAK.LV}else if(47253<=E&&E<=47279)return f.CLUSTER_BREAK.LVT}else if(E<47281){if(E===47280)return f.CLUSTER_BREAK.LV}else if(E<47308){if(47281<=E&&E<=47307)return f.CLUSTER_BREAK.LVT}else if(E===47308)return f.CLUSTER_BREAK.LV}else if(E<47364){if(E<47336){if(47309<=E&&E<=47335)return f.CLUSTER_BREAK.LVT}else if(E<47337){if(E===47336)return f.CLUSTER_BREAK.LV}else if(47337<=E&&E<=47363)return f.CLUSTER_BREAK.LVT}else if(E<47365){if(E===47364)return f.CLUSTER_BREAK.LV}else if(E<47392){if(47365<=E&&E<=47391)return f.CLUSTER_BREAK.LVT}else if(E===47392)return f.CLUSTER_BREAK.LV}else if(E<47560){if(E<47476){if(E<47421){if(E<47420){if(47393<=E&&E<=47419)return f.CLUSTER_BREAK.LVT}else if(E===47420)return f.CLUSTER_BREAK.LV}else if(E<47448){if(47421<=E&&E<=47447)return f.CLUSTER_BREAK.LVT}else if(E<47449){if(E===47448)return f.CLUSTER_BREAK.LV}else if(47449<=E&&E<=47475)return f.CLUSTER_BREAK.LVT}else if(E<47505){if(E<47477){if(E===47476)return f.CLUSTER_BREAK.LV}else if(E<47504){if(47477<=E&&E<=47503)return f.CLUSTER_BREAK.LVT}else if(E===47504)return f.CLUSTER_BREAK.LV}else if(E<47532){if(47505<=E&&E<=47531)return f.CLUSTER_BREAK.LVT}else if(E<47533){if(E===47532)return f.CLUSTER_BREAK.LV}else if(47533<=E&&E<=47559)return f.CLUSTER_BREAK.LVT}else if(E<47617){if(E<47588){if(E<47561){if(E===47560)return f.CLUSTER_BREAK.LV}else if(47561<=E&&E<=47587)return f.CLUSTER_BREAK.LVT}else if(E<47589){if(E===47588)return f.CLUSTER_BREAK.LV}else if(E<47616){if(47589<=E&&E<=47615)return f.CLUSTER_BREAK.LVT}else if(E===47616)return f.CLUSTER_BREAK.LV}else if(E<47672){if(E<47644){if(47617<=E&&E<=47643)return f.CLUSTER_BREAK.LVT}else if(E<47645){if(E===47644)return f.CLUSTER_BREAK.LV}else if(47645<=E&&E<=47671)return f.CLUSTER_BREAK.LVT}else if(E<47673){if(E===47672)return f.CLUSTER_BREAK.LV}else if(E<47700){if(47673<=E&&E<=47699)return f.CLUSTER_BREAK.LVT}else if(E===47700)return f.CLUSTER_BREAK.LV}else if(E<48316){if(E<48008){if(E<47841){if(E<47784){if(E<47729){if(E<47728){if(47701<=E&&E<=47727)return f.CLUSTER_BREAK.LVT}else if(E===47728)return f.CLUSTER_BREAK.LV}else if(E<47756){if(47729<=E&&E<=47755)return f.CLUSTER_BREAK.LVT}else if(E<47757){if(E===47756)return f.CLUSTER_BREAK.LV}else if(47757<=E&&E<=47783)return f.CLUSTER_BREAK.LVT}else if(E<47812){if(E<47785){if(E===47784)return f.CLUSTER_BREAK.LV}else if(47785<=E&&E<=47811)return f.CLUSTER_BREAK.LVT}else if(E<47813){if(E===47812)return f.CLUSTER_BREAK.LV}else if(E<47840){if(47813<=E&&E<=47839)return f.CLUSTER_BREAK.LVT}else if(E===47840)return f.CLUSTER_BREAK.LV}else if(E<47924){if(E<47869){if(E<47868){if(47841<=E&&E<=47867)return f.CLUSTER_BREAK.LVT}else if(E===47868)return f.CLUSTER_BREAK.LV}else if(E<47896){if(47869<=E&&E<=47895)return f.CLUSTER_BREAK.LVT}else if(E<47897){if(E===47896)return f.CLUSTER_BREAK.LV}else if(47897<=E&&E<=47923)return f.CLUSTER_BREAK.LVT}else if(E<47953){if(E<47925){if(E===47924)return f.CLUSTER_BREAK.LV}else if(E<47952){if(47925<=E&&E<=47951)return f.CLUSTER_BREAK.LVT}else if(E===47952)return f.CLUSTER_BREAK.LV}else if(E<47980){if(47953<=E&&E<=47979)return f.CLUSTER_BREAK.LVT}else if(E<47981){if(E===47980)return f.CLUSTER_BREAK.LV}else if(47981<=E&&E<=48007)return f.CLUSTER_BREAK.LVT}else if(E<48149){if(E<48065){if(E<48036){if(E<48009){if(E===48008)return f.CLUSTER_BREAK.LV}else if(48009<=E&&E<=48035)return f.CLUSTER_BREAK.LVT}else if(E<48037){if(E===48036)return f.CLUSTER_BREAK.LV}else if(E<48064){if(48037<=E&&E<=48063)return f.CLUSTER_BREAK.LVT}else if(E===48064)return f.CLUSTER_BREAK.LV}else if(E<48120){if(E<48092){if(48065<=E&&E<=48091)return f.CLUSTER_BREAK.LVT}else if(E<48093){if(E===48092)return f.CLUSTER_BREAK.LV}else if(48093<=E&&E<=48119)return f.CLUSTER_BREAK.LVT}else if(E<48121){if(E===48120)return f.CLUSTER_BREAK.LV}else if(E<48148){if(48121<=E&&E<=48147)return f.CLUSTER_BREAK.LVT}else if(E===48148)return f.CLUSTER_BREAK.LV}else if(E<48232){if(E<48177){if(E<48176){if(48149<=E&&E<=48175)return f.CLUSTER_BREAK.LVT}else if(E===48176)return f.CLUSTER_BREAK.LV}else if(E<48204){if(48177<=E&&E<=48203)return f.CLUSTER_BREAK.LVT}else if(E<48205){if(E===48204)return f.CLUSTER_BREAK.LV}else if(48205<=E&&E<=48231)return f.CLUSTER_BREAK.LVT}else if(E<48261){if(E<48233){if(E===48232)return f.CLUSTER_BREAK.LV}else if(E<48260){if(48233<=E&&E<=48259)return f.CLUSTER_BREAK.LVT}else if(E===48260)return f.CLUSTER_BREAK.LV}else if(E<48288){if(48261<=E&&E<=48287)return f.CLUSTER_BREAK.LVT}else if(E<48289){if(E===48288)return f.CLUSTER_BREAK.LV}else if(48289<=E&&E<=48315)return f.CLUSTER_BREAK.LVT}else if(E<48597){if(E<48456){if(E<48373){if(E<48344){if(E<48317){if(E===48316)return f.CLUSTER_BREAK.LV}else if(48317<=E&&E<=48343)return f.CLUSTER_BREAK.LVT}else if(E<48345){if(E===48344)return f.CLUSTER_BREAK.LV}else if(E<48372){if(48345<=E&&E<=48371)return f.CLUSTER_BREAK.LVT}else if(E===48372)return f.CLUSTER_BREAK.LV}else if(E<48401){if(E<48400){if(48373<=E&&E<=48399)return f.CLUSTER_BREAK.LVT}else if(E===48400)return f.CLUSTER_BREAK.LV}else if(E<48428){if(48401<=E&&E<=48427)return f.CLUSTER_BREAK.LVT}else if(E<48429){if(E===48428)return f.CLUSTER_BREAK.LV}else if(48429<=E&&E<=48455)return f.CLUSTER_BREAK.LVT}else if(E<48513){if(E<48484){if(E<48457){if(E===48456)return f.CLUSTER_BREAK.LV}else if(48457<=E&&E<=48483)return f.CLUSTER_BREAK.LVT}else if(E<48485){if(E===48484)return f.CLUSTER_BREAK.LV}else if(E<48512){if(48485<=E&&E<=48511)return f.CLUSTER_BREAK.LVT}else if(E===48512)return f.CLUSTER_BREAK.LV}else if(E<48568){if(E<48540){if(48513<=E&&E<=48539)return f.CLUSTER_BREAK.LVT}else if(E<48541){if(E===48540)return f.CLUSTER_BREAK.LV}else if(48541<=E&&E<=48567)return f.CLUSTER_BREAK.LVT}else if(E<48569){if(E===48568)return f.CLUSTER_BREAK.LV}else if(E<48596){if(48569<=E&&E<=48595)return f.CLUSTER_BREAK.LVT}else if(E===48596)return f.CLUSTER_BREAK.LV}else if(E<48764){if(E<48680){if(E<48625){if(E<48624){if(48597<=E&&E<=48623)return f.CLUSTER_BREAK.LVT}else if(E===48624)return f.CLUSTER_BREAK.LV}else if(E<48652){if(48625<=E&&E<=48651)return f.CLUSTER_BREAK.LVT}else if(E<48653){if(E===48652)return f.CLUSTER_BREAK.LV}else if(48653<=E&&E<=48679)return f.CLUSTER_BREAK.LVT}else if(E<48709){if(E<48681){if(E===48680)return f.CLUSTER_BREAK.LV}else if(E<48708){if(48681<=E&&E<=48707)return f.CLUSTER_BREAK.LVT}else if(E===48708)return f.CLUSTER_BREAK.LV}else if(E<48736){if(48709<=E&&E<=48735)return f.CLUSTER_BREAK.LVT}else if(E<48737){if(E===48736)return f.CLUSTER_BREAK.LV}else if(48737<=E&&E<=48763)return f.CLUSTER_BREAK.LVT}else if(E<48821){if(E<48792){if(E<48765){if(E===48764)return f.CLUSTER_BREAK.LV}else if(48765<=E&&E<=48791)return f.CLUSTER_BREAK.LVT}else if(E<48793){if(E===48792)return f.CLUSTER_BREAK.LV}else if(E<48820){if(48793<=E&&E<=48819)return f.CLUSTER_BREAK.LVT}else if(E===48820)return f.CLUSTER_BREAK.LV}else if(E<48876){if(E<48848){if(48821<=E&&E<=48847)return f.CLUSTER_BREAK.LVT}else if(E<48849){if(E===48848)return f.CLUSTER_BREAK.LV}else if(48849<=E&&E<=48875)return f.CLUSTER_BREAK.LVT}else if(E<48877){if(E===48876)return f.CLUSTER_BREAK.LV}else if(E<48904){if(48877<=E&&E<=48903)return f.CLUSTER_BREAK.LVT}else if(E===48904)return f.CLUSTER_BREAK.LV}else if(E<53720){if(E<51312){if(E<50108){if(E<49493){if(E<49212){if(E<49045){if(E<48988){if(E<48933){if(E<48932){if(48905<=E&&E<=48931)return f.CLUSTER_BREAK.LVT}else if(E===48932)return f.CLUSTER_BREAK.LV}else if(E<48960){if(48933<=E&&E<=48959)return f.CLUSTER_BREAK.LVT}else if(E<48961){if(E===48960)return f.CLUSTER_BREAK.LV}else if(48961<=E&&E<=48987)return f.CLUSTER_BREAK.LVT}else if(E<49016){if(E<48989){if(E===48988)return f.CLUSTER_BREAK.LV}else if(48989<=E&&E<=49015)return f.CLUSTER_BREAK.LVT}else if(E<49017){if(E===49016)return f.CLUSTER_BREAK.LV}else if(E<49044){if(49017<=E&&E<=49043)return f.CLUSTER_BREAK.LVT}else if(E===49044)return f.CLUSTER_BREAK.LV}else if(E<49128){if(E<49073){if(E<49072){if(49045<=E&&E<=49071)return f.CLUSTER_BREAK.LVT}else if(E===49072)return f.CLUSTER_BREAK.LV}else if(E<49100){if(49073<=E&&E<=49099)return f.CLUSTER_BREAK.LVT}else if(E<49101){if(E===49100)return f.CLUSTER_BREAK.LV}else if(49101<=E&&E<=49127)return f.CLUSTER_BREAK.LVT}else if(E<49157){if(E<49129){if(E===49128)return f.CLUSTER_BREAK.LV}else if(E<49156){if(49129<=E&&E<=49155)return f.CLUSTER_BREAK.LVT}else if(E===49156)return f.CLUSTER_BREAK.LV}else if(E<49184){if(49157<=E&&E<=49183)return f.CLUSTER_BREAK.LVT}else if(E<49185){if(E===49184)return f.CLUSTER_BREAK.LV}else if(49185<=E&&E<=49211)return f.CLUSTER_BREAK.LVT}else if(E<49352){if(E<49269){if(E<49240){if(E<49213){if(E===49212)return f.CLUSTER_BREAK.LV}else if(49213<=E&&E<=49239)return f.CLUSTER_BREAK.LVT}else if(E<49241){if(E===49240)return f.CLUSTER_BREAK.LV}else if(E<49268){if(49241<=E&&E<=49267)return f.CLUSTER_BREAK.LVT}else if(E===49268)return f.CLUSTER_BREAK.LV}else if(E<49297){if(E<49296){if(49269<=E&&E<=49295)return f.CLUSTER_BREAK.LVT}else if(E===49296)return f.CLUSTER_BREAK.LV}else if(E<49324){if(49297<=E&&E<=49323)return f.CLUSTER_BREAK.LVT}else if(E<49325){if(E===49324)return f.CLUSTER_BREAK.LV}else if(49325<=E&&E<=49351)return f.CLUSTER_BREAK.LVT}else if(E<49409){if(E<49380){if(E<49353){if(E===49352)return f.CLUSTER_BREAK.LV}else if(49353<=E&&E<=49379)return f.CLUSTER_BREAK.LVT}else if(E<49381){if(E===49380)return f.CLUSTER_BREAK.LV}else if(E<49408){if(49381<=E&&E<=49407)return f.CLUSTER_BREAK.LVT}else if(E===49408)return f.CLUSTER_BREAK.LV}else if(E<49464){if(E<49436){if(49409<=E&&E<=49435)return f.CLUSTER_BREAK.LVT}else if(E<49437){if(E===49436)return f.CLUSTER_BREAK.LV}else if(49437<=E&&E<=49463)return f.CLUSTER_BREAK.LVT}else if(E<49465){if(E===49464)return f.CLUSTER_BREAK.LV}else if(E<49492){if(49465<=E&&E<=49491)return f.CLUSTER_BREAK.LVT}else if(E===49492)return f.CLUSTER_BREAK.LV}else if(E<49800){if(E<49633){if(E<49576){if(E<49521){if(E<49520){if(49493<=E&&E<=49519)return f.CLUSTER_BREAK.LVT}else if(E===49520)return f.CLUSTER_BREAK.LV}else if(E<49548){if(49521<=E&&E<=49547)return f.CLUSTER_BREAK.LVT}else if(E<49549){if(E===49548)return f.CLUSTER_BREAK.LV}else if(49549<=E&&E<=49575)return f.CLUSTER_BREAK.LVT}else if(E<49604){if(E<49577){if(E===49576)return f.CLUSTER_BREAK.LV}else if(49577<=E&&E<=49603)return f.CLUSTER_BREAK.LVT}else if(E<49605){if(E===49604)return f.CLUSTER_BREAK.LV}else if(E<49632){if(49605<=E&&E<=49631)return f.CLUSTER_BREAK.LVT}else if(E===49632)return f.CLUSTER_BREAK.LV}else if(E<49716){if(E<49661){if(E<49660){if(49633<=E&&E<=49659)return f.CLUSTER_BREAK.LVT}else if(E===49660)return f.CLUSTER_BREAK.LV}else if(E<49688){if(49661<=E&&E<=49687)return f.CLUSTER_BREAK.LVT}else if(E<49689){if(E===49688)return f.CLUSTER_BREAK.LV}else if(49689<=E&&E<=49715)return f.CLUSTER_BREAK.LVT}else if(E<49745){if(E<49717){if(E===49716)return f.CLUSTER_BREAK.LV}else if(E<49744){if(49717<=E&&E<=49743)return f.CLUSTER_BREAK.LVT}else if(E===49744)return f.CLUSTER_BREAK.LV}else if(E<49772){if(49745<=E&&E<=49771)return f.CLUSTER_BREAK.LVT}else if(E<49773){if(E===49772)return f.CLUSTER_BREAK.LV}else if(49773<=E&&E<=49799)return f.CLUSTER_BREAK.LVT}else if(E<49941){if(E<49857){if(E<49828){if(E<49801){if(E===49800)return f.CLUSTER_BREAK.LV}else if(49801<=E&&E<=49827)return f.CLUSTER_BREAK.LVT}else if(E<49829){if(E===49828)return f.CLUSTER_BREAK.LV}else if(E<49856){if(49829<=E&&E<=49855)return f.CLUSTER_BREAK.LVT}else if(E===49856)return f.CLUSTER_BREAK.LV}else if(E<49912){if(E<49884){if(49857<=E&&E<=49883)return f.CLUSTER_BREAK.LVT}else if(E<49885){if(E===49884)return f.CLUSTER_BREAK.LV}else if(49885<=E&&E<=49911)return f.CLUSTER_BREAK.LVT}else if(E<49913){if(E===49912)return f.CLUSTER_BREAK.LV}else if(E<49940){if(49913<=E&&E<=49939)return f.CLUSTER_BREAK.LVT}else if(E===49940)return f.CLUSTER_BREAK.LV}else if(E<50024){if(E<49969){if(E<49968){if(49941<=E&&E<=49967)return f.CLUSTER_BREAK.LVT}else if(E===49968)return f.CLUSTER_BREAK.LV}else if(E<49996){if(49969<=E&&E<=49995)return f.CLUSTER_BREAK.LVT}else if(E<49997){if(E===49996)return f.CLUSTER_BREAK.LV}else if(49997<=E&&E<=50023)return f.CLUSTER_BREAK.LVT}else if(E<50053){if(E<50025){if(E===50024)return f.CLUSTER_BREAK.LV}else if(E<50052){if(50025<=E&&E<=50051)return f.CLUSTER_BREAK.LVT}else if(E===50052)return f.CLUSTER_BREAK.LV}else if(E<50080){if(50053<=E&&E<=50079)return f.CLUSTER_BREAK.LVT}else if(E<50081){if(E===50080)return f.CLUSTER_BREAK.LV}else if(50081<=E&&E<=50107)return f.CLUSTER_BREAK.LVT}else if(E<50697){if(E<50389){if(E<50248){if(E<50165){if(E<50136){if(E<50109){if(E===50108)return f.CLUSTER_BREAK.LV}else if(50109<=E&&E<=50135)return f.CLUSTER_BREAK.LVT}else if(E<50137){if(E===50136)return f.CLUSTER_BREAK.LV}else if(E<50164){if(50137<=E&&E<=50163)return f.CLUSTER_BREAK.LVT}else if(E===50164)return f.CLUSTER_BREAK.LV}else if(E<50193){if(E<50192){if(50165<=E&&E<=50191)return f.CLUSTER_BREAK.LVT}else if(E===50192)return f.CLUSTER_BREAK.LV}else if(E<50220){if(50193<=E&&E<=50219)return f.CLUSTER_BREAK.LVT}else if(E<50221){if(E===50220)return f.CLUSTER_BREAK.LV}else if(50221<=E&&E<=50247)return f.CLUSTER_BREAK.LVT}else if(E<50305){if(E<50276){if(E<50249){if(E===50248)return f.CLUSTER_BREAK.LV}else if(50249<=E&&E<=50275)return f.CLUSTER_BREAK.LVT}else if(E<50277){if(E===50276)return f.CLUSTER_BREAK.LV}else if(E<50304){if(50277<=E&&E<=50303)return f.CLUSTER_BREAK.LVT}else if(E===50304)return f.CLUSTER_BREAK.LV}else if(E<50360){if(E<50332){if(50305<=E&&E<=50331)return f.CLUSTER_BREAK.LVT}else if(E<50333){if(E===50332)return f.CLUSTER_BREAK.LV}else if(50333<=E&&E<=50359)return f.CLUSTER_BREAK.LVT}else if(E<50361){if(E===50360)return f.CLUSTER_BREAK.LV}else if(E<50388){if(50361<=E&&E<=50387)return f.CLUSTER_BREAK.LVT}else if(E===50388)return f.CLUSTER_BREAK.LV}else if(E<50556){if(E<50472){if(E<50417){if(E<50416){if(50389<=E&&E<=50415)return f.CLUSTER_BREAK.LVT}else if(E===50416)return f.CLUSTER_BREAK.LV}else if(E<50444){if(50417<=E&&E<=50443)return f.CLUSTER_BREAK.LVT}else if(E<50445){if(E===50444)return f.CLUSTER_BREAK.LV}else if(50445<=E&&E<=50471)return f.CLUSTER_BREAK.LVT}else if(E<50501){if(E<50473){if(E===50472)return f.CLUSTER_BREAK.LV}else if(E<50500){if(50473<=E&&E<=50499)return f.CLUSTER_BREAK.LVT}else if(E===50500)return f.CLUSTER_BREAK.LV}else if(E<50528){if(50501<=E&&E<=50527)return f.CLUSTER_BREAK.LVT}else if(E<50529){if(E===50528)return f.CLUSTER_BREAK.LV}else if(50529<=E&&E<=50555)return f.CLUSTER_BREAK.LVT}else if(E<50613){if(E<50584){if(E<50557){if(E===50556)return f.CLUSTER_BREAK.LV}else if(50557<=E&&E<=50583)return f.CLUSTER_BREAK.LVT}else if(E<50585){if(E===50584)return f.CLUSTER_BREAK.LV}else if(E<50612){if(50585<=E&&E<=50611)return f.CLUSTER_BREAK.LVT}else if(E===50612)return f.CLUSTER_BREAK.LV}else if(E<50668){if(E<50640){if(50613<=E&&E<=50639)return f.CLUSTER_BREAK.LVT}else if(E<50641){if(E===50640)return f.CLUSTER_BREAK.LV}else if(50641<=E&&E<=50667)return f.CLUSTER_BREAK.LVT}else if(E<50669){if(E===50668)return f.CLUSTER_BREAK.LV}else if(E<50696){if(50669<=E&&E<=50695)return f.CLUSTER_BREAK.LVT}else if(E===50696)return f.CLUSTER_BREAK.LV}else if(E<51004){if(E<50837){if(E<50780){if(E<50725){if(E<50724){if(50697<=E&&E<=50723)return f.CLUSTER_BREAK.LVT}else if(E===50724)return f.CLUSTER_BREAK.LV}else if(E<50752){if(50725<=E&&E<=50751)return f.CLUSTER_BREAK.LVT}else if(E<50753){if(E===50752)return f.CLUSTER_BREAK.LV}else if(50753<=E&&E<=50779)return f.CLUSTER_BREAK.LVT}else if(E<50808){if(E<50781){if(E===50780)return f.CLUSTER_BREAK.LV}else if(50781<=E&&E<=50807)return f.CLUSTER_BREAK.LVT}else if(E<50809){if(E===50808)return f.CLUSTER_BREAK.LV}else if(E<50836){if(50809<=E&&E<=50835)return f.CLUSTER_BREAK.LVT}else if(E===50836)return f.CLUSTER_BREAK.LV}else if(E<50920){if(E<50865){if(E<50864){if(50837<=E&&E<=50863)return f.CLUSTER_BREAK.LVT}else if(E===50864)return f.CLUSTER_BREAK.LV}else if(E<50892){if(50865<=E&&E<=50891)return f.CLUSTER_BREAK.LVT}else if(E<50893){if(E===50892)return f.CLUSTER_BREAK.LV}else if(50893<=E&&E<=50919)return f.CLUSTER_BREAK.LVT}else if(E<50949){if(E<50921){if(E===50920)return f.CLUSTER_BREAK.LV}else if(E<50948){if(50921<=E&&E<=50947)return f.CLUSTER_BREAK.LVT}else if(E===50948)return f.CLUSTER_BREAK.LV}else if(E<50976){if(50949<=E&&E<=50975)return f.CLUSTER_BREAK.LVT}else if(E<50977){if(E===50976)return f.CLUSTER_BREAK.LV}else if(50977<=E&&E<=51003)return f.CLUSTER_BREAK.LVT}else if(E<51145){if(E<51061){if(E<51032){if(E<51005){if(E===51004)return f.CLUSTER_BREAK.LV}else if(51005<=E&&E<=51031)return f.CLUSTER_BREAK.LVT}else if(E<51033){if(E===51032)return f.CLUSTER_BREAK.LV}else if(E<51060){if(51033<=E&&E<=51059)return f.CLUSTER_BREAK.LVT}else if(E===51060)return f.CLUSTER_BREAK.LV}else if(E<51116){if(E<51088){if(51061<=E&&E<=51087)return f.CLUSTER_BREAK.LVT}else if(E<51089){if(E===51088)return f.CLUSTER_BREAK.LV}else if(51089<=E&&E<=51115)return f.CLUSTER_BREAK.LVT}else if(E<51117){if(E===51116)return f.CLUSTER_BREAK.LV}else if(E<51144){if(51117<=E&&E<=51143)return f.CLUSTER_BREAK.LVT}else if(E===51144)return f.CLUSTER_BREAK.LV}else if(E<51228){if(E<51173){if(E<51172){if(51145<=E&&E<=51171)return f.CLUSTER_BREAK.LVT}else if(E===51172)return f.CLUSTER_BREAK.LV}else if(E<51200){if(51173<=E&&E<=51199)return f.CLUSTER_BREAK.LVT}else if(E<51201){if(E===51200)return f.CLUSTER_BREAK.LV}else if(51201<=E&&E<=51227)return f.CLUSTER_BREAK.LVT}else if(E<51257){if(E<51229){if(E===51228)return f.CLUSTER_BREAK.LV}else if(E<51256){if(51229<=E&&E<=51255)return f.CLUSTER_BREAK.LVT}else if(E===51256)return f.CLUSTER_BREAK.LV}else if(E<51284){if(51257<=E&&E<=51283)return f.CLUSTER_BREAK.LVT}else if(E<51285){if(E===51284)return f.CLUSTER_BREAK.LV}else if(51285<=E&&E<=51311)return f.CLUSTER_BREAK.LVT}else if(E<52516){if(E<51901){if(E<51593){if(E<51452){if(E<51369){if(E<51340){if(E<51313){if(E===51312)return f.CLUSTER_BREAK.LV}else if(51313<=E&&E<=51339)return f.CLUSTER_BREAK.LVT}else if(E<51341){if(E===51340)return f.CLUSTER_BREAK.LV}else if(E<51368){if(51341<=E&&E<=51367)return f.CLUSTER_BREAK.LVT}else if(E===51368)return f.CLUSTER_BREAK.LV}else if(E<51397){if(E<51396){if(51369<=E&&E<=51395)return f.CLUSTER_BREAK.LVT}else if(E===51396)return f.CLUSTER_BREAK.LV}else if(E<51424){if(51397<=E&&E<=51423)return f.CLUSTER_BREAK.LVT}else if(E<51425){if(E===51424)return f.CLUSTER_BREAK.LV}else if(51425<=E&&E<=51451)return f.CLUSTER_BREAK.LVT}else if(E<51509){if(E<51480){if(E<51453){if(E===51452)return f.CLUSTER_BREAK.LV}else if(51453<=E&&E<=51479)return f.CLUSTER_BREAK.LVT}else if(E<51481){if(E===51480)return f.CLUSTER_BREAK.LV}else if(E<51508){if(51481<=E&&E<=51507)return f.CLUSTER_BREAK.LVT}else if(E===51508)return f.CLUSTER_BREAK.LV}else if(E<51564){if(E<51536){if(51509<=E&&E<=51535)return f.CLUSTER_BREAK.LVT}else if(E<51537){if(E===51536)return f.CLUSTER_BREAK.LV}else if(51537<=E&&E<=51563)return f.CLUSTER_BREAK.LVT}else if(E<51565){if(E===51564)return f.CLUSTER_BREAK.LV}else if(E<51592){if(51565<=E&&E<=51591)return f.CLUSTER_BREAK.LVT}else if(E===51592)return f.CLUSTER_BREAK.LV}else if(E<51760){if(E<51676){if(E<51621){if(E<51620){if(51593<=E&&E<=51619)return f.CLUSTER_BREAK.LVT}else if(E===51620)return f.CLUSTER_BREAK.LV}else if(E<51648){if(51621<=E&&E<=51647)return f.CLUSTER_BREAK.LVT}else if(E<51649){if(E===51648)return f.CLUSTER_BREAK.LV}else if(51649<=E&&E<=51675)return f.CLUSTER_BREAK.LVT}else if(E<51705){if(E<51677){if(E===51676)return f.CLUSTER_BREAK.LV}else if(E<51704){if(51677<=E&&E<=51703)return f.CLUSTER_BREAK.LVT}else if(E===51704)return f.CLUSTER_BREAK.LV}else if(E<51732){if(51705<=E&&E<=51731)return f.CLUSTER_BREAK.LVT}else if(E<51733){if(E===51732)return f.CLUSTER_BREAK.LV}else if(51733<=E&&E<=51759)return f.CLUSTER_BREAK.LVT}else if(E<51817){if(E<51788){if(E<51761){if(E===51760)return f.CLUSTER_BREAK.LV}else if(51761<=E&&E<=51787)return f.CLUSTER_BREAK.LVT}else if(E<51789){if(E===51788)return f.CLUSTER_BREAK.LV}else if(E<51816){if(51789<=E&&E<=51815)return f.CLUSTER_BREAK.LVT}else if(E===51816)return f.CLUSTER_BREAK.LV}else if(E<51872){if(E<51844){if(51817<=E&&E<=51843)return f.CLUSTER_BREAK.LVT}else if(E<51845){if(E===51844)return f.CLUSTER_BREAK.LV}else if(51845<=E&&E<=51871)return f.CLUSTER_BREAK.LVT}else if(E<51873){if(E===51872)return f.CLUSTER_BREAK.LV}else if(E<51900){if(51873<=E&&E<=51899)return f.CLUSTER_BREAK.LVT}else if(E===51900)return f.CLUSTER_BREAK.LV}else if(E<52208){if(E<52041){if(E<51984){if(E<51929){if(E<51928){if(51901<=E&&E<=51927)return f.CLUSTER_BREAK.LVT}else if(E===51928)return f.CLUSTER_BREAK.LV}else if(E<51956){if(51929<=E&&E<=51955)return f.CLUSTER_BREAK.LVT}else if(E<51957){if(E===51956)return f.CLUSTER_BREAK.LV}else if(51957<=E&&E<=51983)return f.CLUSTER_BREAK.LVT}else if(E<52012){if(E<51985){if(E===51984)return f.CLUSTER_BREAK.LV}else if(51985<=E&&E<=52011)return f.CLUSTER_BREAK.LVT}else if(E<52013){if(E===52012)return f.CLUSTER_BREAK.LV}else if(E<52040){if(52013<=E&&E<=52039)return f.CLUSTER_BREAK.LVT}else if(E===52040)return f.CLUSTER_BREAK.LV}else if(E<52124){if(E<52069){if(E<52068){if(52041<=E&&E<=52067)return f.CLUSTER_BREAK.LVT}else if(E===52068)return f.CLUSTER_BREAK.LV}else if(E<52096){if(52069<=E&&E<=52095)return f.CLUSTER_BREAK.LVT}else if(E<52097){if(E===52096)return f.CLUSTER_BREAK.LV}else if(52097<=E&&E<=52123)return f.CLUSTER_BREAK.LVT}else if(E<52153){if(E<52125){if(E===52124)return f.CLUSTER_BREAK.LV}else if(E<52152){if(52125<=E&&E<=52151)return f.CLUSTER_BREAK.LVT}else if(E===52152)return f.CLUSTER_BREAK.LV}else if(E<52180){if(52153<=E&&E<=52179)return f.CLUSTER_BREAK.LVT}else if(E<52181){if(E===52180)return f.CLUSTER_BREAK.LV}else if(52181<=E&&E<=52207)return f.CLUSTER_BREAK.LVT}else if(E<52349){if(E<52265){if(E<52236){if(E<52209){if(E===52208)return f.CLUSTER_BREAK.LV}else if(52209<=E&&E<=52235)return f.CLUSTER_BREAK.LVT}else if(E<52237){if(E===52236)return f.CLUSTER_BREAK.LV}else if(E<52264){if(52237<=E&&E<=52263)return f.CLUSTER_BREAK.LVT}else if(E===52264)return f.CLUSTER_BREAK.LV}else if(E<52320){if(E<52292){if(52265<=E&&E<=52291)return f.CLUSTER_BREAK.LVT}else if(E<52293){if(E===52292)return f.CLUSTER_BREAK.LV}else if(52293<=E&&E<=52319)return f.CLUSTER_BREAK.LVT}else if(E<52321){if(E===52320)return f.CLUSTER_BREAK.LV}else if(E<52348){if(52321<=E&&E<=52347)return f.CLUSTER_BREAK.LVT}else if(E===52348)return f.CLUSTER_BREAK.LV}else if(E<52432){if(E<52377){if(E<52376){if(52349<=E&&E<=52375)return f.CLUSTER_BREAK.LVT}else if(E===52376)return f.CLUSTER_BREAK.LV}else if(E<52404){if(52377<=E&&E<=52403)return f.CLUSTER_BREAK.LVT}else if(E<52405){if(E===52404)return f.CLUSTER_BREAK.LV}else if(52405<=E&&E<=52431)return f.CLUSTER_BREAK.LVT}else if(E<52461){if(E<52433){if(E===52432)return f.CLUSTER_BREAK.LV}else if(E<52460){if(52433<=E&&E<=52459)return f.CLUSTER_BREAK.LVT}else if(E===52460)return f.CLUSTER_BREAK.LV}else if(E<52488){if(52461<=E&&E<=52487)return f.CLUSTER_BREAK.LVT}else if(E<52489){if(E===52488)return f.CLUSTER_BREAK.LV}else if(52489<=E&&E<=52515)return f.CLUSTER_BREAK.LVT}else if(E<53105){if(E<52797){if(E<52656){if(E<52573){if(E<52544){if(E<52517){if(E===52516)return f.CLUSTER_BREAK.LV}else if(52517<=E&&E<=52543)return f.CLUSTER_BREAK.LVT}else if(E<52545){if(E===52544)return f.CLUSTER_BREAK.LV}else if(E<52572){if(52545<=E&&E<=52571)return f.CLUSTER_BREAK.LVT}else if(E===52572)return f.CLUSTER_BREAK.LV}else if(E<52601){if(E<52600){if(52573<=E&&E<=52599)return f.CLUSTER_BREAK.LVT}else if(E===52600)return f.CLUSTER_BREAK.LV}else if(E<52628){if(52601<=E&&E<=52627)return f.CLUSTER_BREAK.LVT}else if(E<52629){if(E===52628)return f.CLUSTER_BREAK.LV}else if(52629<=E&&E<=52655)return f.CLUSTER_BREAK.LVT}else if(E<52713){if(E<52684){if(E<52657){if(E===52656)return f.CLUSTER_BREAK.LV}else if(52657<=E&&E<=52683)return f.CLUSTER_BREAK.LVT}else if(E<52685){if(E===52684)return f.CLUSTER_BREAK.LV}else if(E<52712){if(52685<=E&&E<=52711)return f.CLUSTER_BREAK.LVT}else if(E===52712)return f.CLUSTER_BREAK.LV}else if(E<52768){if(E<52740){if(52713<=E&&E<=52739)return f.CLUSTER_BREAK.LVT}else if(E<52741){if(E===52740)return f.CLUSTER_BREAK.LV}else if(52741<=E&&E<=52767)return f.CLUSTER_BREAK.LVT}else if(E<52769){if(E===52768)return f.CLUSTER_BREAK.LV}else if(E<52796){if(52769<=E&&E<=52795)return f.CLUSTER_BREAK.LVT}else if(E===52796)return f.CLUSTER_BREAK.LV}else if(E<52964){if(E<52880){if(E<52825){if(E<52824){if(52797<=E&&E<=52823)return f.CLUSTER_BREAK.LVT}else if(E===52824)return f.CLUSTER_BREAK.LV}else if(E<52852){if(52825<=E&&E<=52851)return f.CLUSTER_BREAK.LVT}else if(E<52853){if(E===52852)return f.CLUSTER_BREAK.LV}else if(52853<=E&&E<=52879)return f.CLUSTER_BREAK.LVT}else if(E<52909){if(E<52881){if(E===52880)return f.CLUSTER_BREAK.LV}else if(E<52908){if(52881<=E&&E<=52907)return f.CLUSTER_BREAK.LVT}else if(E===52908)return f.CLUSTER_BREAK.LV}else if(E<52936){if(52909<=E&&E<=52935)return f.CLUSTER_BREAK.LVT}else if(E<52937){if(E===52936)return f.CLUSTER_BREAK.LV}else if(52937<=E&&E<=52963)return f.CLUSTER_BREAK.LVT}else if(E<53021){if(E<52992){if(E<52965){if(E===52964)return f.CLUSTER_BREAK.LV}else if(52965<=E&&E<=52991)return f.CLUSTER_BREAK.LVT}else if(E<52993){if(E===52992)return f.CLUSTER_BREAK.LV}else if(E<53020){if(52993<=E&&E<=53019)return f.CLUSTER_BREAK.LVT}else if(E===53020)return f.CLUSTER_BREAK.LV}else if(E<53076){if(E<53048){if(53021<=E&&E<=53047)return f.CLUSTER_BREAK.LVT}else if(E<53049){if(E===53048)return f.CLUSTER_BREAK.LV}else if(53049<=E&&E<=53075)return f.CLUSTER_BREAK.LVT}else if(E<53077){if(E===53076)return f.CLUSTER_BREAK.LV}else if(E<53104){if(53077<=E&&E<=53103)return f.CLUSTER_BREAK.LVT}else if(E===53104)return f.CLUSTER_BREAK.LV}else if(E<53412){if(E<53245){if(E<53188){if(E<53133){if(E<53132){if(53105<=E&&E<=53131)return f.CLUSTER_BREAK.LVT}else if(E===53132)return f.CLUSTER_BREAK.LV}else if(E<53160){if(53133<=E&&E<=53159)return f.CLUSTER_BREAK.LVT}else if(E<53161){if(E===53160)return f.CLUSTER_BREAK.LV}else if(53161<=E&&E<=53187)return f.CLUSTER_BREAK.LVT}else if(E<53216){if(E<53189){if(E===53188)return f.CLUSTER_BREAK.LV}else if(53189<=E&&E<=53215)return f.CLUSTER_BREAK.LVT}else if(E<53217){if(E===53216)return f.CLUSTER_BREAK.LV}else if(E<53244){if(53217<=E&&E<=53243)return f.CLUSTER_BREAK.LVT}else if(E===53244)return f.CLUSTER_BREAK.LV}else if(E<53328){if(E<53273){if(E<53272){if(53245<=E&&E<=53271)return f.CLUSTER_BREAK.LVT}else if(E===53272)return f.CLUSTER_BREAK.LV}else if(E<53300){if(53273<=E&&E<=53299)return f.CLUSTER_BREAK.LVT}else if(E<53301){if(E===53300)return f.CLUSTER_BREAK.LV}else if(53301<=E&&E<=53327)return f.CLUSTER_BREAK.LVT}else if(E<53357){if(E<53329){if(E===53328)return f.CLUSTER_BREAK.LV}else if(E<53356){if(53329<=E&&E<=53355)return f.CLUSTER_BREAK.LVT}else if(E===53356)return f.CLUSTER_BREAK.LV}else if(E<53384){if(53357<=E&&E<=53383)return f.CLUSTER_BREAK.LVT}else if(E<53385){if(E===53384)return f.CLUSTER_BREAK.LV}else if(53385<=E&&E<=53411)return f.CLUSTER_BREAK.LVT}else if(E<53553){if(E<53469){if(E<53440){if(E<53413){if(E===53412)return f.CLUSTER_BREAK.LV}else if(53413<=E&&E<=53439)return f.CLUSTER_BREAK.LVT}else if(E<53441){if(E===53440)return f.CLUSTER_BREAK.LV}else if(E<53468){if(53441<=E&&E<=53467)return f.CLUSTER_BREAK.LVT}else if(E===53468)return f.CLUSTER_BREAK.LV}else if(E<53524){if(E<53496){if(53469<=E&&E<=53495)return f.CLUSTER_BREAK.LVT}else if(E<53497){if(E===53496)return f.CLUSTER_BREAK.LV}else if(53497<=E&&E<=53523)return f.CLUSTER_BREAK.LVT}else if(E<53525){if(E===53524)return f.CLUSTER_BREAK.LV}else if(E<53552){if(53525<=E&&E<=53551)return f.CLUSTER_BREAK.LVT}else if(E===53552)return f.CLUSTER_BREAK.LV}else if(E<53636){if(E<53581){if(E<53580){if(53553<=E&&E<=53579)return f.CLUSTER_BREAK.LVT}else if(E===53580)return f.CLUSTER_BREAK.LV}else if(E<53608){if(53581<=E&&E<=53607)return f.CLUSTER_BREAK.LVT}else if(E<53609){if(E===53608)return f.CLUSTER_BREAK.LV}else if(53609<=E&&E<=53635)return f.CLUSTER_BREAK.LVT}else if(E<53665){if(E<53637){if(E===53636)return f.CLUSTER_BREAK.LV}else if(E<53664){if(53637<=E&&E<=53663)return f.CLUSTER_BREAK.LVT}else if(E===53664)return f.CLUSTER_BREAK.LV}else if(E<53692){if(53665<=E&&E<=53691)return f.CLUSTER_BREAK.LVT}else if(E<53693){if(E===53692)return f.CLUSTER_BREAK.LV}else if(53693<=E&&E<=53719)return f.CLUSTER_BREAK.LVT}else if(E<70459){if(E<54897){if(E<54308){if(E<54001){if(E<53860){if(E<53777){if(E<53748){if(E<53721){if(E===53720)return f.CLUSTER_BREAK.LV}else if(53721<=E&&E<=53747)return f.CLUSTER_BREAK.LVT}else if(E<53749){if(E===53748)return f.CLUSTER_BREAK.LV}else if(E<53776){if(53749<=E&&E<=53775)return f.CLUSTER_BREAK.LVT}else if(E===53776)return f.CLUSTER_BREAK.LV}else if(E<53805){if(E<53804){if(53777<=E&&E<=53803)return f.CLUSTER_BREAK.LVT}else if(E===53804)return f.CLUSTER_BREAK.LV}else if(E<53832){if(53805<=E&&E<=53831)return f.CLUSTER_BREAK.LVT}else if(E<53833){if(E===53832)return f.CLUSTER_BREAK.LV}else if(53833<=E&&E<=53859)return f.CLUSTER_BREAK.LVT}else if(E<53917){if(E<53888){if(E<53861){if(E===53860)return f.CLUSTER_BREAK.LV}else if(53861<=E&&E<=53887)return f.CLUSTER_BREAK.LVT}else if(E<53889){if(E===53888)return f.CLUSTER_BREAK.LV}else if(E<53916){if(53889<=E&&E<=53915)return f.CLUSTER_BREAK.LVT}else if(E===53916)return f.CLUSTER_BREAK.LV}else if(E<53972){if(E<53944){if(53917<=E&&E<=53943)return f.CLUSTER_BREAK.LVT}else if(E<53945){if(E===53944)return f.CLUSTER_BREAK.LV}else if(53945<=E&&E<=53971)return f.CLUSTER_BREAK.LVT}else if(E<53973){if(E===53972)return f.CLUSTER_BREAK.LV}else if(E<54e3){if(53973<=E&&E<=53999)return f.CLUSTER_BREAK.LVT}else if(E===54e3)return f.CLUSTER_BREAK.LV}else if(E<54141){if(E<54084){if(E<54029){if(E<54028){if(54001<=E&&E<=54027)return f.CLUSTER_BREAK.LVT}else if(E===54028)return f.CLUSTER_BREAK.LV}else if(E<54056){if(54029<=E&&E<=54055)return f.CLUSTER_BREAK.LVT}else if(E<54057){if(E===54056)return f.CLUSTER_BREAK.LV}else if(54057<=E&&E<=54083)return f.CLUSTER_BREAK.LVT}else if(E<54112){if(E<54085){if(E===54084)return f.CLUSTER_BREAK.LV}else if(54085<=E&&E<=54111)return f.CLUSTER_BREAK.LVT}else if(E<54113){if(E===54112)return f.CLUSTER_BREAK.LV}else if(E<54140){if(54113<=E&&E<=54139)return f.CLUSTER_BREAK.LVT}else if(E===54140)return f.CLUSTER_BREAK.LV}else if(E<54224){if(E<54169){if(E<54168){if(54141<=E&&E<=54167)return f.CLUSTER_BREAK.LVT}else if(E===54168)return f.CLUSTER_BREAK.LV}else if(E<54196){if(54169<=E&&E<=54195)return f.CLUSTER_BREAK.LVT}else if(E<54197){if(E===54196)return f.CLUSTER_BREAK.LV}else if(54197<=E&&E<=54223)return f.CLUSTER_BREAK.LVT}else if(E<54253){if(E<54225){if(E===54224)return f.CLUSTER_BREAK.LV}else if(E<54252){if(54225<=E&&E<=54251)return f.CLUSTER_BREAK.LVT}else if(E===54252)return f.CLUSTER_BREAK.LV}else if(E<54280){if(54253<=E&&E<=54279)return f.CLUSTER_BREAK.LVT}else if(E<54281){if(E===54280)return f.CLUSTER_BREAK.LV}else if(54281<=E&&E<=54307)return f.CLUSTER_BREAK.LVT}else if(E<54589){if(E<54448){if(E<54365){if(E<54336){if(E<54309){if(E===54308)return f.CLUSTER_BREAK.LV}else if(54309<=E&&E<=54335)return f.CLUSTER_BREAK.LVT}else if(E<54337){if(E===54336)return f.CLUSTER_BREAK.LV}else if(E<54364){if(54337<=E&&E<=54363)return f.CLUSTER_BREAK.LVT}else if(E===54364)return f.CLUSTER_BREAK.LV}else if(E<54393){if(E<54392){if(54365<=E&&E<=54391)return f.CLUSTER_BREAK.LVT}else if(E===54392)return f.CLUSTER_BREAK.LV}else if(E<54420){if(54393<=E&&E<=54419)return f.CLUSTER_BREAK.LVT}else if(E<54421){if(E===54420)return f.CLUSTER_BREAK.LV}else if(54421<=E&&E<=54447)return f.CLUSTER_BREAK.LVT}else if(E<54505){if(E<54476){if(E<54449){if(E===54448)return f.CLUSTER_BREAK.LV}else if(54449<=E&&E<=54475)return f.CLUSTER_BREAK.LVT}else if(E<54477){if(E===54476)return f.CLUSTER_BREAK.LV}else if(E<54504){if(54477<=E&&E<=54503)return f.CLUSTER_BREAK.LVT}else if(E===54504)return f.CLUSTER_BREAK.LV}else if(E<54560){if(E<54532){if(54505<=E&&E<=54531)return f.CLUSTER_BREAK.LVT}else if(E<54533){if(E===54532)return f.CLUSTER_BREAK.LV}else if(54533<=E&&E<=54559)return f.CLUSTER_BREAK.LVT}else if(E<54561){if(E===54560)return f.CLUSTER_BREAK.LV}else if(E<54588){if(54561<=E&&E<=54587)return f.CLUSTER_BREAK.LVT}else if(E===54588)return f.CLUSTER_BREAK.LV}else if(E<54756){if(E<54672){if(E<54617){if(E<54616){if(54589<=E&&E<=54615)return f.CLUSTER_BREAK.LVT}else if(E===54616)return f.CLUSTER_BREAK.LV}else if(E<54644){if(54617<=E&&E<=54643)return f.CLUSTER_BREAK.LVT}else if(E<54645){if(E===54644)return f.CLUSTER_BREAK.LV}else if(54645<=E&&E<=54671)return f.CLUSTER_BREAK.LVT}else if(E<54701){if(E<54673){if(E===54672)return f.CLUSTER_BREAK.LV}else if(E<54700){if(54673<=E&&E<=54699)return f.CLUSTER_BREAK.LVT}else if(E===54700)return f.CLUSTER_BREAK.LV}else if(E<54728){if(54701<=E&&E<=54727)return f.CLUSTER_BREAK.LVT}else if(E<54729){if(E===54728)return f.CLUSTER_BREAK.LV}else if(54729<=E&&E<=54755)return f.CLUSTER_BREAK.LVT}else if(E<54813){if(E<54784){if(E<54757){if(E===54756)return f.CLUSTER_BREAK.LV}else if(54757<=E&&E<=54783)return f.CLUSTER_BREAK.LVT}else if(E<54785){if(E===54784)return f.CLUSTER_BREAK.LV}else if(E<54812){if(54785<=E&&E<=54811)return f.CLUSTER_BREAK.LVT}else if(E===54812)return f.CLUSTER_BREAK.LV}else if(E<54868){if(E<54840){if(54813<=E&&E<=54839)return f.CLUSTER_BREAK.LVT}else if(E<54841){if(E===54840)return f.CLUSTER_BREAK.LV}else if(54841<=E&&E<=54867)return f.CLUSTER_BREAK.LVT}else if(E<54869){if(E===54868)return f.CLUSTER_BREAK.LV}else if(E<54896){if(54869<=E&&E<=54895)return f.CLUSTER_BREAK.LVT}else if(E===54896)return f.CLUSTER_BREAK.LV}else if(E<69632){if(E<55216){if(E<55037){if(E<54980){if(E<54925){if(E<54924){if(54897<=E&&E<=54923)return f.CLUSTER_BREAK.LVT}else if(E===54924)return f.CLUSTER_BREAK.LV}else if(E<54952){if(54925<=E&&E<=54951)return f.CLUSTER_BREAK.LVT}else if(E<54953){if(E===54952)return f.CLUSTER_BREAK.LV}else if(54953<=E&&E<=54979)return f.CLUSTER_BREAK.LVT}else if(E<55008){if(E<54981){if(E===54980)return f.CLUSTER_BREAK.LV}else if(54981<=E&&E<=55007)return f.CLUSTER_BREAK.LVT}else if(E<55009){if(E===55008)return f.CLUSTER_BREAK.LV}else if(E<55036){if(55009<=E&&E<=55035)return f.CLUSTER_BREAK.LVT}else if(E===55036)return f.CLUSTER_BREAK.LV}else if(E<55120){if(E<55065){if(E<55064){if(55037<=E&&E<=55063)return f.CLUSTER_BREAK.LVT}else if(E===55064)return f.CLUSTER_BREAK.LV}else if(E<55092){if(55065<=E&&E<=55091)return f.CLUSTER_BREAK.LVT}else if(E<55093){if(E===55092)return f.CLUSTER_BREAK.LV}else if(55093<=E&&E<=55119)return f.CLUSTER_BREAK.LVT}else if(E<55149){if(E<55121){if(E===55120)return f.CLUSTER_BREAK.LV}else if(E<55148){if(55121<=E&&E<=55147)return f.CLUSTER_BREAK.LVT}else if(E===55148)return f.CLUSTER_BREAK.LV}else if(E<55176){if(55149<=E&&E<=55175)return f.CLUSTER_BREAK.LVT}else if(E<55177){if(E===55176)return f.CLUSTER_BREAK.LV}else if(55177<=E&&E<=55203)return f.CLUSTER_BREAK.LVT}else if(E<68097){if(E<65279){if(E<64286){if(E<55243){if(55216<=E&&E<=55238)return f.CLUSTER_BREAK.V}else if(55243<=E&&E<=55291)return f.CLUSTER_BREAK.T}else if(E<65024){if(E===64286)return f.CLUSTER_BREAK.EXTEND}else if(E<65056){if(65024<=E&&E<=65039)return f.CLUSTER_BREAK.EXTEND}else if(65056<=E&&E<=65071)return f.CLUSTER_BREAK.EXTEND}else if(E<66045){if(E<65438){if(E===65279)return f.CLUSTER_BREAK.CONTROL}else if(E<65520){if(65438<=E&&E<=65439)return f.CLUSTER_BREAK.EXTEND}else if(65520<=E&&E<=65531)return f.CLUSTER_BREAK.CONTROL}else if(E<66272){if(E===66045)return f.CLUSTER_BREAK.EXTEND}else if(E<66422){if(E===66272)return f.CLUSTER_BREAK.EXTEND}else if(66422<=E&&E<=66426)return f.CLUSTER_BREAK.EXTEND}else if(E<68325){if(E<68108){if(E<68101){if(68097<=E&&E<=68099)return f.CLUSTER_BREAK.EXTEND}else if(68101<=E&&E<=68102)return f.CLUSTER_BREAK.EXTEND}else if(E<68152){if(68108<=E&&E<=68111)return f.CLUSTER_BREAK.EXTEND}else if(E<68159){if(68152<=E&&E<=68154)return f.CLUSTER_BREAK.EXTEND}else if(E===68159)return f.CLUSTER_BREAK.EXTEND}else if(E<69373){if(E<68900){if(68325<=E&&E<=68326)return f.CLUSTER_BREAK.EXTEND}else if(E<69291){if(68900<=E&&E<=68903)return f.CLUSTER_BREAK.EXTEND}else if(69291<=E&&E<=69292)return f.CLUSTER_BREAK.EXTEND}else if(E<69446){if(69373<=E&&E<=69375)return f.CLUSTER_BREAK.EXTEND}else if(E<69506){if(69446<=E&&E<=69456)return f.CLUSTER_BREAK.EXTEND}else if(69506<=E&&E<=69509)return f.CLUSTER_BREAK.EXTEND}else if(E<70016){if(E<69815){if(E<69747){if(E<69634){if(E===69632)return f.CLUSTER_BREAK.SPACINGMARK;if(E===69633)return f.CLUSTER_BREAK.EXTEND}else if(E<69688){if(E===69634)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<69744){if(69688<=E&&E<=69702)return f.CLUSTER_BREAK.EXTEND}else if(E===69744)return f.CLUSTER_BREAK.EXTEND}else if(E<69762){if(E<69759){if(69747<=E&&E<=69748)return f.CLUSTER_BREAK.EXTEND}else if(69759<=E&&E<=69761)return f.CLUSTER_BREAK.EXTEND}else if(E<69808){if(E===69762)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<69811){if(69808<=E&&E<=69810)return f.CLUSTER_BREAK.SPACINGMARK}else if(69811<=E&&E<=69814)return f.CLUSTER_BREAK.EXTEND}else if(E<69888)if(E<69821){if(E<69817){if(69815<=E&&E<=69816)return f.CLUSTER_BREAK.SPACINGMARK}else if(69817<=E&&E<=69818)return f.CLUSTER_BREAK.EXTEND}else if(E<69826){if(E===69821)return f.CLUSTER_BREAK.PREPEND}else{if(E===69826)return f.CLUSTER_BREAK.EXTEND;if(E===69837)return f.CLUSTER_BREAK.PREPEND}else if(E<69933){if(E<69927){if(69888<=E&&E<=69890)return f.CLUSTER_BREAK.EXTEND}else if(E<69932){if(69927<=E&&E<=69931)return f.CLUSTER_BREAK.EXTEND}else if(E===69932)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<69957){if(69933<=E&&E<=69940)return f.CLUSTER_BREAK.EXTEND}else if(E<70003){if(69957<=E&&E<=69958)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===70003)return f.CLUSTER_BREAK.EXTEND}else if(E<70194){if(E<70082){if(E<70067){if(E<70018){if(70016<=E&&E<=70017)return f.CLUSTER_BREAK.EXTEND}else if(E===70018)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<70070){if(70067<=E&&E<=70069)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<70079){if(70070<=E&&E<=70078)return f.CLUSTER_BREAK.EXTEND}else if(70079<=E&&E<=70080)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<70095){if(E<70089){if(70082<=E&&E<=70083)return f.CLUSTER_BREAK.PREPEND}else if(E<70094){if(70089<=E&&E<=70092)return f.CLUSTER_BREAK.EXTEND}else if(E===70094)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<70188){if(E===70095)return f.CLUSTER_BREAK.EXTEND}else if(E<70191){if(70188<=E&&E<=70190)return f.CLUSTER_BREAK.SPACINGMARK}else if(70191<=E&&E<=70193)return f.CLUSTER_BREAK.EXTEND}else if(E<70209){if(E<70197){if(E<70196){if(70194<=E&&E<=70195)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===70196)return f.CLUSTER_BREAK.EXTEND}else if(E<70198){if(E===70197)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<70206){if(70198<=E&&E<=70199)return f.CLUSTER_BREAK.EXTEND}else if(E===70206)return f.CLUSTER_BREAK.EXTEND}else if(E<70371){if(E<70367){if(E===70209)return f.CLUSTER_BREAK.EXTEND}else if(E<70368){if(E===70367)return f.CLUSTER_BREAK.EXTEND}else if(70368<=E&&E<=70370)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<70400){if(70371<=E&&E<=70378)return f.CLUSTER_BREAK.EXTEND}else if(E<70402){if(70400<=E&&E<=70401)return f.CLUSTER_BREAK.EXTEND}else if(70402<=E&&E<=70403)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<72343){if(E<71339){if(E<70841){if(E<70512){if(E<70471){if(E<70463){if(E<70462){if(70459<=E&&E<=70460)return f.CLUSTER_BREAK.EXTEND}else if(E===70462)return f.CLUSTER_BREAK.EXTEND}else if(E<70464){if(E===70463)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<70465){if(E===70464)return f.CLUSTER_BREAK.EXTEND}else if(70465<=E&&E<=70468)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<70487){if(E<70475){if(70471<=E&&E<=70472)return f.CLUSTER_BREAK.SPACINGMARK}else if(70475<=E&&E<=70477)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<70498){if(E===70487)return f.CLUSTER_BREAK.EXTEND}else if(E<70502){if(70498<=E&&E<=70499)return f.CLUSTER_BREAK.SPACINGMARK}else if(70502<=E&&E<=70508)return f.CLUSTER_BREAK.EXTEND}else if(E<70725){if(E<70712){if(E<70709){if(70512<=E&&E<=70516)return f.CLUSTER_BREAK.EXTEND}else if(70709<=E&&E<=70711)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<70720){if(70712<=E&&E<=70719)return f.CLUSTER_BREAK.EXTEND}else if(E<70722){if(70720<=E&&E<=70721)return f.CLUSTER_BREAK.SPACINGMARK}else if(70722<=E&&E<=70724)return f.CLUSTER_BREAK.EXTEND}else if(E<70832){if(E<70726){if(E===70725)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===70726||E===70750)return f.CLUSTER_BREAK.EXTEND}else if(E<70833){if(E===70832)return f.CLUSTER_BREAK.EXTEND}else if(E<70835){if(70833<=E&&E<=70834)return f.CLUSTER_BREAK.SPACINGMARK}else if(70835<=E&&E<=70840)return f.CLUSTER_BREAK.EXTEND}else if(E<71096){if(E<70847)if(E<70843){if(E===70841)return f.CLUSTER_BREAK.SPACINGMARK;if(E===70842)return f.CLUSTER_BREAK.EXTEND}else if(E<70845){if(70843<=E&&E<=70844)return f.CLUSTER_BREAK.SPACINGMARK}else{if(E===70845)return f.CLUSTER_BREAK.EXTEND;if(E===70846)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<71087){if(E<70849){if(70847<=E&&E<=70848)return f.CLUSTER_BREAK.EXTEND}else if(E<70850){if(E===70849)return f.CLUSTER_BREAK.SPACINGMARK}else if(70850<=E&&E<=70851)return f.CLUSTER_BREAK.EXTEND}else if(E<71088){if(E===71087)return f.CLUSTER_BREAK.EXTEND}else if(E<71090){if(71088<=E&&E<=71089)return f.CLUSTER_BREAK.SPACINGMARK}else if(71090<=E&&E<=71093)return f.CLUSTER_BREAK.EXTEND}else if(E<71216){if(E<71102){if(E<71100){if(71096<=E&&E<=71099)return f.CLUSTER_BREAK.SPACINGMARK}else if(71100<=E&&E<=71101)return f.CLUSTER_BREAK.EXTEND}else if(E<71103){if(E===71102)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<71132){if(71103<=E&&E<=71104)return f.CLUSTER_BREAK.EXTEND}else if(71132<=E&&E<=71133)return f.CLUSTER_BREAK.EXTEND}else if(E<71229){if(E<71219){if(71216<=E&&E<=71218)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<71227){if(71219<=E&&E<=71226)return f.CLUSTER_BREAK.EXTEND}else if(71227<=E&&E<=71228)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<71230){if(E===71229)return f.CLUSTER_BREAK.EXTEND}else if(E<71231){if(E===71230)return f.CLUSTER_BREAK.SPACINGMARK}else if(71231<=E&&E<=71232)return f.CLUSTER_BREAK.EXTEND}else if(E<71999)if(E<71463){if(E<71350){if(E<71341){if(E===71339)return f.CLUSTER_BREAK.EXTEND;if(E===71340)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<71342){if(E===71341)return f.CLUSTER_BREAK.EXTEND}else if(E<71344){if(71342<=E&&E<=71343)return f.CLUSTER_BREAK.SPACINGMARK}else if(71344<=E&&E<=71349)return f.CLUSTER_BREAK.EXTEND}else if(E<71453){if(E===71350)return f.CLUSTER_BREAK.SPACINGMARK;if(E===71351)return f.CLUSTER_BREAK.EXTEND}else if(E<71458){if(71453<=E&&E<=71455)return f.CLUSTER_BREAK.EXTEND}else if(E<71462){if(71458<=E&&E<=71461)return f.CLUSTER_BREAK.EXTEND}else if(E===71462)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<71984){if(E<71727){if(E<71724){if(71463<=E&&E<=71467)return f.CLUSTER_BREAK.EXTEND}else if(71724<=E&&E<=71726)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<71736){if(71727<=E&&E<=71735)return f.CLUSTER_BREAK.EXTEND}else if(E<71737){if(E===71736)return f.CLUSTER_BREAK.SPACINGMARK}else if(71737<=E&&E<=71738)return f.CLUSTER_BREAK.EXTEND}else if(E<71995){if(E<71985){if(E===71984)return f.CLUSTER_BREAK.EXTEND}else if(E<71991){if(71985<=E&&E<=71989)return f.CLUSTER_BREAK.SPACINGMARK}else if(71991<=E&&E<=71992)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<71997){if(71995<=E&&E<=71996)return f.CLUSTER_BREAK.EXTEND}else{if(E===71997)return f.CLUSTER_BREAK.SPACINGMARK;if(E===71998)return f.CLUSTER_BREAK.EXTEND}else if(E<72193)if(E<72145)if(E<72001){if(E===71999)return f.CLUSTER_BREAK.PREPEND;if(E===72e3)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<72002){if(E===72001)return f.CLUSTER_BREAK.PREPEND}else{if(E===72002)return f.CLUSTER_BREAK.SPACINGMARK;if(E===72003)return f.CLUSTER_BREAK.EXTEND}else if(E<72156){if(E<72148){if(72145<=E&&E<=72147)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<72154){if(72148<=E&&E<=72151)return f.CLUSTER_BREAK.EXTEND}else if(72154<=E&&E<=72155)return f.CLUSTER_BREAK.EXTEND}else if(E<72160){if(72156<=E&&E<=72159)return f.CLUSTER_BREAK.SPACINGMARK}else{if(E===72160)return f.CLUSTER_BREAK.EXTEND;if(E===72164)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<72263){if(E<72249){if(E<72243){if(72193<=E&&E<=72202)return f.CLUSTER_BREAK.EXTEND}else if(72243<=E&&E<=72248)return f.CLUSTER_BREAK.EXTEND}else if(E<72250){if(E===72249)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<72251){if(E===72250)return f.CLUSTER_BREAK.PREPEND}else if(72251<=E&&E<=72254)return f.CLUSTER_BREAK.EXTEND}else if(E<72281){if(E<72273){if(E===72263)return f.CLUSTER_BREAK.EXTEND}else if(E<72279){if(72273<=E&&E<=72278)return f.CLUSTER_BREAK.EXTEND}else if(72279<=E&&E<=72280)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<72324){if(72281<=E&&E<=72283)return f.CLUSTER_BREAK.EXTEND}else if(E<72330){if(72324<=E&&E<=72329)return f.CLUSTER_BREAK.PREPEND}else if(72330<=E&&E<=72342)return f.CLUSTER_BREAK.EXTEND}else if(E<94033){if(E<73104){if(E<72881){if(E<72766){if(E<72751){if(E<72344){if(E===72343)return f.CLUSTER_BREAK.SPACINGMARK}else if(72344<=E&&E<=72345)return f.CLUSTER_BREAK.EXTEND}else if(E<72752){if(E===72751)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<72760){if(72752<=E&&E<=72758)return f.CLUSTER_BREAK.EXTEND}else if(72760<=E&&E<=72765)return f.CLUSTER_BREAK.EXTEND}else if(E<72850){if(E===72766)return f.CLUSTER_BREAK.SPACINGMARK;if(E===72767)return f.CLUSTER_BREAK.EXTEND}else if(E<72873){if(72850<=E&&E<=72871)return f.CLUSTER_BREAK.EXTEND}else if(E<72874){if(E===72873)return f.CLUSTER_BREAK.SPACINGMARK}else if(72874<=E&&E<=72880)return f.CLUSTER_BREAK.EXTEND}else if(E<73018){if(E<72884){if(E<72882){if(E===72881)return f.CLUSTER_BREAK.SPACINGMARK}else if(72882<=E&&E<=72883)return f.CLUSTER_BREAK.EXTEND}else if(E<72885){if(E===72884)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<73009){if(72885<=E&&E<=72886)return f.CLUSTER_BREAK.EXTEND}else if(73009<=E&&E<=73014)return f.CLUSTER_BREAK.EXTEND}else if(E<73030){if(E<73020){if(E===73018)return f.CLUSTER_BREAK.EXTEND}else if(E<73023){if(73020<=E&&E<=73021)return f.CLUSTER_BREAK.EXTEND}else if(73023<=E&&E<=73029)return f.CLUSTER_BREAK.EXTEND}else if(E<73031){if(E===73030)return f.CLUSTER_BREAK.PREPEND}else if(E<73098){if(E===73031)return f.CLUSTER_BREAK.EXTEND}else if(73098<=E&&E<=73102)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<73526){if(E<73459)if(E<73109){if(E<73107){if(73104<=E&&E<=73105)return f.CLUSTER_BREAK.EXTEND}else if(73107<=E&&E<=73108)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<73110){if(E===73109)return f.CLUSTER_BREAK.EXTEND}else{if(E===73110)return f.CLUSTER_BREAK.SPACINGMARK;if(E===73111)return f.CLUSTER_BREAK.EXTEND}else if(E<73474){if(E<73461){if(73459<=E&&E<=73460)return f.CLUSTER_BREAK.EXTEND}else if(E<73472){if(73461<=E&&E<=73462)return f.CLUSTER_BREAK.SPACINGMARK}else if(73472<=E&&E<=73473)return f.CLUSTER_BREAK.EXTEND}else if(E<73475){if(E===73474)return f.CLUSTER_BREAK.PREPEND}else if(E<73524){if(E===73475)return f.CLUSTER_BREAK.SPACINGMARK}else if(73524<=E&&E<=73525)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<78896)if(E<73536){if(E<73534){if(73526<=E&&E<=73530)return f.CLUSTER_BREAK.EXTEND}else if(73534<=E&&E<=73535)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<73537){if(E===73536)return f.CLUSTER_BREAK.EXTEND}else{if(E===73537)return f.CLUSTER_BREAK.SPACINGMARK;if(E===73538)return f.CLUSTER_BREAK.EXTEND}else if(E<92912){if(E<78912){if(78896<=E&&E<=78911)return f.CLUSTER_BREAK.CONTROL}else if(E<78919){if(E===78912)return f.CLUSTER_BREAK.EXTEND}else if(78919<=E&&E<=78933)return f.CLUSTER_BREAK.EXTEND}else if(E<92976){if(92912<=E&&E<=92916)return f.CLUSTER_BREAK.EXTEND}else if(E<94031){if(92976<=E&&E<=92982)return f.CLUSTER_BREAK.EXTEND}else if(E===94031)return f.CLUSTER_BREAK.EXTEND}else if(E<121476){if(E<119143)if(E<113824){if(E<94180){if(E<94095){if(94033<=E&&E<=94087)return f.CLUSTER_BREAK.SPACINGMARK}else if(94095<=E&&E<=94098)return f.CLUSTER_BREAK.EXTEND}else if(E<94192){if(E===94180)return f.CLUSTER_BREAK.EXTEND}else if(E<113821){if(94192<=E&&E<=94193)return f.CLUSTER_BREAK.SPACINGMARK}else if(113821<=E&&E<=113822)return f.CLUSTER_BREAK.EXTEND}else if(E<118576){if(E<118528){if(113824<=E&&E<=113827)return f.CLUSTER_BREAK.CONTROL}else if(118528<=E&&E<=118573)return f.CLUSTER_BREAK.EXTEND}else if(E<119141){if(118576<=E&&E<=118598)return f.CLUSTER_BREAK.EXTEND}else{if(E===119141)return f.CLUSTER_BREAK.EXTEND;if(E===119142)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<119173){if(E<119150){if(E<119149){if(119143<=E&&E<=119145)return f.CLUSTER_BREAK.EXTEND}else if(E===119149)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<119155){if(119150<=E&&E<=119154)return f.CLUSTER_BREAK.EXTEND}else if(E<119163){if(119155<=E&&E<=119162)return f.CLUSTER_BREAK.CONTROL}else if(119163<=E&&E<=119170)return f.CLUSTER_BREAK.EXTEND}else if(E<121344){if(E<119210){if(119173<=E&&E<=119179)return f.CLUSTER_BREAK.EXTEND}else if(E<119362){if(119210<=E&&E<=119213)return f.CLUSTER_BREAK.EXTEND}else if(119362<=E&&E<=119364)return f.CLUSTER_BREAK.EXTEND}else if(E<121403){if(121344<=E&&E<=121398)return f.CLUSTER_BREAK.EXTEND}else if(E<121461){if(121403<=E&&E<=121452)return f.CLUSTER_BREAK.EXTEND}else if(E===121461)return f.CLUSTER_BREAK.EXTEND}else if(E<123628){if(E<122907){if(E<121505){if(E<121499){if(E===121476)return f.CLUSTER_BREAK.EXTEND}else if(121499<=E&&E<=121503)return f.CLUSTER_BREAK.EXTEND}else if(E<122880){if(121505<=E&&E<=121519)return f.CLUSTER_BREAK.EXTEND}else if(E<122888){if(122880<=E&&E<=122886)return f.CLUSTER_BREAK.EXTEND}else if(122888<=E&&E<=122904)return f.CLUSTER_BREAK.EXTEND}else if(E<123023){if(E<122915){if(122907<=E&&E<=122913)return f.CLUSTER_BREAK.EXTEND}else if(E<122918){if(122915<=E&&E<=122916)return f.CLUSTER_BREAK.EXTEND}else if(122918<=E&&E<=122922)return f.CLUSTER_BREAK.EXTEND}else if(E<123184){if(E===123023)return f.CLUSTER_BREAK.EXTEND}else if(E<123566){if(123184<=E&&E<=123190)return f.CLUSTER_BREAK.EXTEND}else if(E===123566)return f.CLUSTER_BREAK.EXTEND}else if(E<127995){if(E<125136){if(E<124140){if(123628<=E&&E<=123631)return f.CLUSTER_BREAK.EXTEND}else if(124140<=E&&E<=124143)return f.CLUSTER_BREAK.EXTEND}else if(E<125252){if(125136<=E&&E<=125142)return f.CLUSTER_BREAK.EXTEND}else if(E<127462){if(125252<=E&&E<=125258)return f.CLUSTER_BREAK.EXTEND}else if(127462<=E&&E<=127487)return f.CLUSTER_BREAK.REGIONAL_INDICATOR}else if(E<917632){if(E<917504){if(127995<=E&&E<=127999)return f.CLUSTER_BREAK.EXTEND}else if(E<917536){if(917504<=E&&E<=917535)return f.CLUSTER_BREAK.CONTROL}else if(917536<=E&&E<=917631)return f.CLUSTER_BREAK.EXTEND}else if(E<917760){if(917632<=E&&E<=917759)return f.CLUSTER_BREAK.CONTROL}else if(E<918e3){if(917760<=E&&E<=917999)return f.CLUSTER_BREAK.EXTEND}else if(918e3<=E&&E<=921599)return f.CLUSTER_BREAK.CONTROL;return f.CLUSTER_BREAK.OTHER}static getEmojiProperty(E){if(E<10160){if(E<9728){if(E<9e3){if(E<8482){if(E<8252){if(E===169||E===174)return f.EXTENDED_PICTOGRAPHIC}else if(E===8252||E===8265)return f.EXTENDED_PICTOGRAPHIC}else if(E<8596){if(E===8482||E===8505)return f.EXTENDED_PICTOGRAPHIC}else if(E<8617){if(8596<=E&&E<=8601)return f.EXTENDED_PICTOGRAPHIC}else if(E<8986){if(8617<=E&&E<=8618)return f.EXTENDED_PICTOGRAPHIC}else if(8986<=E&&E<=8987)return f.EXTENDED_PICTOGRAPHIC}else if(E<9410){if(E<9167){if(E===9e3||E===9096)return f.EXTENDED_PICTOGRAPHIC}else if(E<9193){if(E===9167)return f.EXTENDED_PICTOGRAPHIC}else if(E<9208){if(9193<=E&&E<=9203)return f.EXTENDED_PICTOGRAPHIC}else if(9208<=E&&E<=9210)return f.EXTENDED_PICTOGRAPHIC}else if(E<9654){if(E<9642){if(E===9410)return f.EXTENDED_PICTOGRAPHIC}else if(9642<=E&&E<=9643)return f.EXTENDED_PICTOGRAPHIC}else if(E<9664){if(E===9654)return f.EXTENDED_PICTOGRAPHIC}else if(E<9723){if(E===9664)return f.EXTENDED_PICTOGRAPHIC}else if(9723<=E&&E<=9726)return f.EXTENDED_PICTOGRAPHIC}else if(E<10035){if(E<10004){if(E<9748){if(E<9735){if(9728<=E&&E<=9733)return f.EXTENDED_PICTOGRAPHIC}else if(9735<=E&&E<=9746)return f.EXTENDED_PICTOGRAPHIC}else if(E<9872){if(9748<=E&&E<=9861)return f.EXTENDED_PICTOGRAPHIC}else if(E<9992){if(9872<=E&&E<=9989)return f.EXTENDED_PICTOGRAPHIC}else if(9992<=E&&E<=10002)return f.EXTENDED_PICTOGRAPHIC}else if(E<10013){if(E===10004||E===10006)return f.EXTENDED_PICTOGRAPHIC}else if(E<10017){if(E===10013)return f.EXTENDED_PICTOGRAPHIC}else if(E===10017||E===10024)return f.EXTENDED_PICTOGRAPHIC}else if(E<10067){if(E<10055){if(E<10052){if(10035<=E&&E<=10036)return f.EXTENDED_PICTOGRAPHIC}else if(E===10052)return f.EXTENDED_PICTOGRAPHIC}else if(E<10060){if(E===10055)return f.EXTENDED_PICTOGRAPHIC}else if(E===10060||E===10062)return f.EXTENDED_PICTOGRAPHIC}else if(E<10083){if(E<10071){if(10067<=E&&E<=10069)return f.EXTENDED_PICTOGRAPHIC}else if(E===10071)return f.EXTENDED_PICTOGRAPHIC}else if(E<10133){if(10083<=E&&E<=10087)return f.EXTENDED_PICTOGRAPHIC}else if(E<10145){if(10133<=E&&E<=10135)return f.EXTENDED_PICTOGRAPHIC}else if(E===10145)return f.EXTENDED_PICTOGRAPHIC}else if(E<127489){if(E<12951){if(E<11035){if(E<10548){if(E===10160||E===10175)return f.EXTENDED_PICTOGRAPHIC}else if(E<11013){if(10548<=E&&E<=10549)return f.EXTENDED_PICTOGRAPHIC}else if(11013<=E&&E<=11015)return f.EXTENDED_PICTOGRAPHIC}else if(E<11093){if(E<11088){if(11035<=E&&E<=11036)return f.EXTENDED_PICTOGRAPHIC}else if(E===11088)return f.EXTENDED_PICTOGRAPHIC}else if(E<12336){if(E===11093)return f.EXTENDED_PICTOGRAPHIC}else if(E===12336||E===12349)return f.EXTENDED_PICTOGRAPHIC}else if(E<127340){if(E<126976){if(E===12951||E===12953)return f.EXTENDED_PICTOGRAPHIC}else if(E<127245){if(126976<=E&&E<=127231)return f.EXTENDED_PICTOGRAPHIC}else if(E<127279){if(127245<=E&&E<=127247)return f.EXTENDED_PICTOGRAPHIC}else if(E===127279)return f.EXTENDED_PICTOGRAPHIC}else if(E<127374){if(E<127358){if(127340<=E&&E<=127345)return f.EXTENDED_PICTOGRAPHIC}else if(127358<=E&&E<=127359)return f.EXTENDED_PICTOGRAPHIC}else if(E<127377){if(E===127374)return f.EXTENDED_PICTOGRAPHIC}else if(E<127405){if(127377<=E&&E<=127386)return f.EXTENDED_PICTOGRAPHIC}else if(127405<=E&&E<=127461)return f.EXTENDED_PICTOGRAPHIC}else if(E<128981){if(E<127561){if(E<127535){if(E<127514){if(127489<=E&&E<=127503)return f.EXTENDED_PICTOGRAPHIC}else if(E===127514)return f.EXTENDED_PICTOGRAPHIC}else if(E<127538){if(E===127535)return f.EXTENDED_PICTOGRAPHIC}else if(E<127548){if(127538<=E&&E<=127546)return f.EXTENDED_PICTOGRAPHIC}else if(127548<=E&&E<=127551)return f.EXTENDED_PICTOGRAPHIC}else if(E<128326){if(E<128e3){if(127561<=E&&E<=127994)return f.EXTENDED_PICTOGRAPHIC}else if(128e3<=E&&E<=128317)return f.EXTENDED_PICTOGRAPHIC}else if(E<128640){if(128326<=E&&E<=128591)return f.EXTENDED_PICTOGRAPHIC}else if(E<128884){if(128640<=E&&E<=128767)return f.EXTENDED_PICTOGRAPHIC}else if(128884<=E&&E<=128895)return f.EXTENDED_PICTOGRAPHIC}else if(E<129198){if(E<129096){if(E<129036){if(128981<=E&&E<=129023)return f.EXTENDED_PICTOGRAPHIC}else if(129036<=E&&E<=129039)return f.EXTENDED_PICTOGRAPHIC}else if(E<129114){if(129096<=E&&E<=129103)return f.EXTENDED_PICTOGRAPHIC}else if(E<129160){if(129114<=E&&E<=129119)return f.EXTENDED_PICTOGRAPHIC}else if(129160<=E&&E<=129167)return f.EXTENDED_PICTOGRAPHIC}else if(E<129340){if(E<129292){if(129198<=E&&E<=129279)return f.EXTENDED_PICTOGRAPHIC}else if(129292<=E&&E<=129338)return f.EXTENDED_PICTOGRAPHIC}else if(E<129351){if(129340<=E&&E<=129349)return f.EXTENDED_PICTOGRAPHIC}else if(E<130048){if(129351<=E&&E<=129791)return f.EXTENDED_PICTOGRAPHIC}else if(130048<=E&&E<=131069)return f.EXTENDED_PICTOGRAPHIC;return f.CLUSTER_BREAK.OTHER}};b.default=C});var p=l(e=>{"use strict";var Y=e&&e.__importDefault||function(x){return x&&x.__esModule?x:{default:x}};Object.defineProperty(e,"__esModule",{value:!0});var $=Y(g());e.default=$.default});var v=J(p()),c=new v.default;globalThis.Intl=globalThis.Intl||{};globalThis.Intl.Segmenter=globalThis.Intl.Segmenter||class{constructor(){}segment=c.iterateGraphemes};
+
diff --git a/yarn.lock b/yarn.lock
index e6976c269..111e800f6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -19,12 +19,15 @@
     jsonpointer "^5.0.0"
     leven "^3.1.0"
 
-"@atproto/api@0.1.3":
-  version "0.1.3"
-  resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.1.3.tgz#4aa9ea7caad624a7eda7d22e03f076e4b0fb68fb"
-  integrity sha512-jEtE0Afxnkvth7/dZKYx9Gv1IpO2Jlmb8KzgRVPnyYyolI2GI4VTNs7mxxO/44cs8vKu2PN2zW+64XuaIY1JBA==
+"@atproto/api@*", "@atproto/api@0.2.0":
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.2.0.tgz#4a60f8f1de91105ad93526d69abcf011bbeaa3be"
+  integrity sha512-AntqYOVrMalBJapnNBV0akh/PWcsKdWq8zfuvv8hZW/jwOkJTVPTRFOP2OHJFcfz4WezytX43ml/L2kSG9z4+Q==
   dependencies:
+    "@atproto/common-web" "*"
+    "@atproto/uri" "*"
     "@atproto/xrpc" "*"
+    tlds "^1.234.0"
     typed-emitter "^2.1.0"
 
 "@atproto/auth@*":
@@ -37,6 +40,15 @@
     "@ucans/core" "0.11.0"
     uint8arrays "3.0.0"
 
+"@atproto/common-web@*":
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.1.0.tgz#5529fa66f9533aa00cfd13f0a25757df7b26bd3d"
+  integrity sha512-qD6xF60hvH+cP++fk/mt+0S9cxs94KsK+rNWypNlgnlp7r9By4ltXwtDSR/DNTA8mwDeularUno4VbTd2IWIzA==
+  dependencies:
+    multiformats "^9.6.4"
+    uint8arrays "3.0.0"
+    zod "^3.14.2"
+
 "@atproto/common@*":
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.1.1.tgz#ec33a3b4995c91d3ad2e90fc4cdbc65284ceff84"
@@ -47,7 +59,17 @@
     pino "^8.6.1"
     zod "^3.14.2"
 
-"@atproto/crypto@*":
+"@atproto/common@0.1.0":
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.1.0.tgz#4216a8fef5b985ab62ac21252a0f8ca0f4a0f210"
+  integrity sha512-OB5tWE2R19jwiMIs2IjQieH5KTUuMb98XGCn9h3xuu6NanwjlmbCYMv08fMYwIp3UQ6jcq//84cDT3Bu6fJD+A==
+  dependencies:
+    "@ipld/dag-cbor" "^7.0.3"
+    multiformats "^9.6.4"
+    pino "^8.6.1"
+    zod "^3.14.2"
+
+"@atproto/crypto@*", "@atproto/crypto@0.1.0":
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/@atproto/crypto/-/crypto-0.1.0.tgz#bc73a479f9dbe06fa025301c182d7f7ab01bc568"
   integrity sha512-9xgFEPtsCiJEPt9o3HtJT30IdFTGw5cQRSJVIy5CFhqBA4vDLcdXiRDLCjkzHEVbtNCsHUW6CrlfOgbeLPcmcg==
@@ -68,14 +90,14 @@
     axios "^0.24.0"
     did-resolver "^4.0.0"
 
-"@atproto/handle@*":
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/@atproto/handle/-/handle-0.0.1.tgz#783f88aaef1f57920deb61da8d72e5191cd9d515"
-  integrity sha512-foWqpzyVufo6/LxHeqBqoz9KhoLIGpIQ3zqYXlJWX4YD6OlFq3RfrGYbJrqHIiHhe1xgm6GIgEax8V6QIEbATA==
+"@atproto/identifier@*":
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/@atproto/identifier/-/identifier-0.1.0.tgz#6b600c8a3da08d9a7d5eab076f8b7064457dde75"
+  integrity sha512-3LV7+4E6S0k8Rru7NBkyDF6Zf6NHVUXVS9d4l9fiXWMC49ghZMjq0vPmz80xjG1rRuFdJFbpRf4ApFciGxLIyQ==
   dependencies:
-    "@sideway/address" "^5.0.0"
+    "@atproto/common-web" "*"
 
-"@atproto/lexicon@*", "@atproto/lexicon@^0.0.4":
+"@atproto/lexicon@*":
   version "0.0.4"
   resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.0.4.tgz#f0a6688ad54adb2ec4a8d1f11fcbf45e96203c4b"
   integrity sha512-00lqIKJetVlxQzNmEhrFzZeT9k+zGPBsHwtYpG7rH4vZ211i5WiDkmQcBwwFs2g/qCBt+nVq0dlgl3JhCLJXQg==
@@ -89,20 +111,21 @@
   resolved "https://registry.yarnpkg.com/@atproto/nsid/-/nsid-0.0.1.tgz#0cdc00cefe8f0b1385f352b9f57b3ad37fff09a4"
   integrity sha512-t5M6/CzWBVYoBbIvfKDpqPj/+ZmyoK9ydZSStcTXosJ27XXwOPhz0VDUGKK2SM9G5Y7TPes8S5KTAU0UdVYFCw==
 
-"@atproto/pds@^0.0.3":
-  version "0.0.3"
-  resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.0.3.tgz#118a1d51687664f085f8e1c19ae3ac1646dc69b2"
-  integrity sha512-l5iGJNyQs73V/mQWkcg4NXNGbnfXfv+Yg3g8nqwtun409iAAeYZimA1Tt0JV/hyq+Oz3antG0VvLQQEtNVUpVQ==
+"@atproto/pds@^0.1.0":
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.1.0.tgz#8014269c12a322b14618e0991c534979a4b145d7"
+  integrity sha512-f1KPONxim674owWcTsR8S5r57+b7evg+zy+jkcTX00BB0fO6PchDL6sTQQc1x3u2QZArHDSUUUgoHt4IWwsfkw==
   dependencies:
+    "@atproto/api" "*"
     "@atproto/common" "*"
     "@atproto/crypto" "*"
     "@atproto/did-resolver" "*"
-    "@atproto/handle" "*"
+    "@atproto/identifier" "*"
     "@atproto/lexicon" "*"
-    "@atproto/plc" "*"
     "@atproto/repo" "*"
     "@atproto/uri" "*"
     "@atproto/xrpc-server" "*"
+    "@did-plc/lib" "^0.0.1"
     better-sqlite3 "^7.6.2"
     bytes "^3.1.2"
     cors "^2.8.5"
@@ -126,29 +149,6 @@
     typed-emitter "^2.1.0"
     uint8arrays "3.0.0"
 
-"@atproto/plc@*":
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/@atproto/plc/-/plc-0.0.1.tgz#713de881fd2b803a0f1afbee57735de8382a8ed3"
-  integrity sha512-9JM027ioAb6rG+2F/p89DJlIXBOH85rGWXFcG3dImZJ8SalFqRZ0/7gtdFN387IZ/HNAWRmmFaAxMEmJ9NgKpQ==
-  dependencies:
-    "@atproto/common" "*"
-    "@atproto/crypto" "*"
-    "@ipld/dag-cbor" "^7.0.3"
-    async-mutex "^0.4.0"
-    axios "^0.27.2"
-    better-sqlite3 "^7.6.2"
-    cors "^2.8.5"
-    dotenv "^16.0.2"
-    express "^4.17.2"
-    express-async-errors "^3.1.1"
-    http-terminator "^3.2.0"
-    kysely "^0.22.0"
-    pg "^8.8.0"
-    pino "^8.6.1"
-    pino-http "^8.2.1"
-    uint8arrays "3.0.0"
-    zod "^3.14.2"
-
 "@atproto/repo@*":
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/@atproto/repo/-/repo-0.0.1.tgz#41c63943a7e6a0942fc3e721c05d8c836c2fcfc2"
@@ -180,7 +180,7 @@
     mime-types "^2.1.35"
     zod "^3.14.2"
 
-"@atproto/xrpc@*", "@atproto/xrpc@^0.0.4":
+"@atproto/xrpc@*":
   version "0.0.4"
   resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.0.4.tgz#d7dd45cdb21e29b9715ca30eb18320548f293413"
   integrity sha512-Hxh+GgZx21Zvlb2RMlSlJDd3r3GR0vAS6OOZPW2xzWiVHsetb9ZlFB6D0AeAPj2R+U2UUkmdUR8G3U/nkgnQFA==
@@ -1314,6 +1314,13 @@
   resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
   integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
 
+"@cspotcode/source-map-support@^0.8.0":
+  version "0.8.1"
+  resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
+  integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==
+  dependencies:
+    "@jridgewell/trace-mapping" "0.3.9"
+
 "@csstools/normalize.css@*":
   version "12.0.0"
   resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-12.0.0.tgz#a9583a75c3f150667771f30b60d9f059473e62c4"
@@ -1425,6 +1432,38 @@
   resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.1.1.tgz#c9c61d9fe5ca5ac664e1153bb0aa0eba1c6d6308"
   integrity sha512-jwx+WCqszn53YHOfvFMJJRd/B2GqkCBt+1MJSG6o5/s8+ytHMvDZXsJgUEWLk12UnLd7HYKac4BYU5i/Ron1Cw==
 
+"@did-plc/lib@*", "@did-plc/lib@^0.0.1":
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/@did-plc/lib/-/lib-0.0.1.tgz#5fd78c71901168ac05c5650af3a376c76461991c"
+  integrity sha512-RkY5w9DbYMco3SjeepqIiMveqz35exjlVDipCs2gz9AXF4/cp9hvmrp9zUWEw2vny+FjV8vGEN7QpaXWaO6nhg==
+  dependencies:
+    "@atproto/common" "0.1.0"
+    "@atproto/crypto" "0.1.0"
+    "@ipld/dag-cbor" "^7.0.3"
+    axios "^1.3.4"
+    multiformats "^9.6.4"
+    uint8arrays "3.0.0"
+    zod "^3.14.2"
+
+"@did-plc/server@^0.0.1":
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/@did-plc/server/-/server-0.0.1.tgz#8d1ba701f3b2b952b7c8fe03ef3118bb0cba077c"
+  integrity sha512-GtxxHcOrOQ6fNI1ufq3Zqjc2PtWqPZOdsuzlwtxiH9XibUGwDkb0GmaBHyU5GiOxOKZEW1GspZ8mreBA6XOlTQ==
+  dependencies:
+    "@atproto/common" "0.1.0"
+    "@atproto/crypto" "0.1.0"
+    "@did-plc/lib" "*"
+    axios "^1.3.4"
+    cors "^2.8.5"
+    express "^4.18.2"
+    express-async-errors "^3.1.1"
+    http-terminator "^3.2.0"
+    kysely "^0.23.4"
+    multiformats "^9.6.4"
+    pg "^8.9.0"
+    pino "^8.11.0"
+    pino-http "^8.3.3"
+
 "@discoveryjs/json-ext@^0.5.0":
   version "0.5.7"
   resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
@@ -1953,11 +1992,6 @@
   resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.2.tgz#6fc464307cbe3c8ca5064549b806360d84457b04"
   integrity sha512-9anpBMM9mEgZN4wr2v8wHJI2/u5TnnggewRN6OlvXTTnuVyoY19X6rOv9XTqKRw6dcGKwZsBi8n0kDE2I5i4VA==
 
-"@hapi/hoek@^10.0.0":
-  version "10.0.1"
-  resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-10.0.1.tgz#ee9da297fabc557e1c040a0f44ee89c266ccc306"
-  integrity sha512-CvlW7jmOhWzuqOqiJQ3rQVLMcREh0eel4IBnxDx2FAcK8g7qoJRQK4L1CPBASoCY6y8e6zuCy3f2g+HWdkzcMw==
-
 "@hapi/hoek@^9.0.0":
   version "9.3.0"
   resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb"
@@ -2459,7 +2493,7 @@
     "@jridgewell/sourcemap-codec" "^1.4.10"
     "@jridgewell/trace-mapping" "^0.3.9"
 
-"@jridgewell/resolve-uri@3.1.0":
+"@jridgewell/resolve-uri@3.1.0", "@jridgewell/resolve-uri@^3.0.3":
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
   integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
@@ -2482,6 +2516,14 @@
   resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
   integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
 
+"@jridgewell/trace-mapping@0.3.9":
+  version "0.3.9"
+  resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
+  integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==
+  dependencies:
+    "@jridgewell/resolve-uri" "^3.0.3"
+    "@jridgewell/sourcemap-codec" "^1.4.10"
+
 "@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.15", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9":
   version "0.3.17"
   resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985"
@@ -2644,10 +2686,10 @@
   dependencies:
     serve-static "^1.13.1"
 
-"@react-native-community/cli-doctor@^10.1.1":
-  version "10.2.1"
-  resolved "https://registry.yarnpkg.com/@react-native-community/cli-doctor/-/cli-doctor-10.2.1.tgz#b6b7a3f0f9cef1a05f1adc6393eb29c6f8f2972c"
-  integrity sha512-IwhdSD+mtgWdxg2eMr0fpkn08XN7r70DC1riGSmqK/DXNyWBzIZlCkDN+/TwlaUEsiFk6LQTjgCiqZSMpmDrsg==
+"@react-native-community/cli-doctor@^10.2.0":
+  version "10.2.2"
+  resolved "https://registry.yarnpkg.com/@react-native-community/cli-doctor/-/cli-doctor-10.2.2.tgz#b1893604fa9fc8971064e7c00042350f96868bfe"
+  integrity sha512-49Ep2aQOF0PkbAR/TcyMjOm9XwBa8VQr+/Zzf4SJeYwiYLCT1NZRAVAVjYRXl0xqvq5S5mAGZZShS4AQl4WsZw==
   dependencies:
     "@react-native-community/cli-config" "^10.1.1"
     "@react-native-community/cli-platform-ios" "^10.2.1"
@@ -2666,7 +2708,7 @@
     sudo-prompt "^9.0.0"
     wcwidth "^1.0.1"
 
-"@react-native-community/cli-hermes@^10.1.3":
+"@react-native-community/cli-hermes@^10.2.0":
   version "10.2.0"
   resolved "https://registry.yarnpkg.com/@react-native-community/cli-hermes/-/cli-hermes-10.2.0.tgz#cc252f435b149f74260bc918ce22fdf58033a87e"
   integrity sha512-urfmvNeR8IiO/Sd92UU3xPO+/qI2lwCWQnxOkWaU/i2EITFekE47MD6MZrfVulRVYRi5cuaFqKZO/ccOdOB/vQ==
@@ -2677,18 +2719,7 @@
     hermes-profile-transformer "^0.0.6"
     ip "^1.1.5"
 
-"@react-native-community/cli-platform-android@10.1.3":
-  version "10.1.3"
-  resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-android/-/cli-platform-android-10.1.3.tgz#8380799cd4d3f9a0ca568b0f5b4ae9e462ce3669"
-  integrity sha512-8YZEpBL6yd9l4CIoFcLOgrV8x2GDujdqrdWrNsNERDAbsiFwqAQvfjyyb57GAZVuEPEJCoqUlGlMCwOh3XQb9A==
-  dependencies:
-    "@react-native-community/cli-tools" "^10.1.1"
-    chalk "^4.1.2"
-    execa "^1.0.0"
-    glob "^7.1.3"
-    logkitty "^0.7.1"
-
-"@react-native-community/cli-platform-android@^10.2.0":
+"@react-native-community/cli-platform-android@10.2.0", "@react-native-community/cli-platform-android@^10.2.0":
   version "10.2.0"
   resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-android/-/cli-platform-android-10.2.0.tgz#0bc689270a5f1d9aaf9e723181d43ca4dbfffdef"
   integrity sha512-CBenYwGxwFdObZTn1lgxWtMGA5ms2G/ALQhkS+XTAD7KHDrCxFF9yT/fnAjFZKM6vX/1TqGI1RflruXih3kAhw==
@@ -2699,14 +2730,15 @@
     glob "^7.1.3"
     logkitty "^0.7.1"
 
-"@react-native-community/cli-platform-ios@10.1.1":
-  version "10.1.1"
-  resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-ios/-/cli-platform-ios-10.1.1.tgz#39ed6810117d8e7330d3aa4d85818fb6ae358785"
-  integrity sha512-EB9/L8j1LqrqyfJtLRixU+d8FIP6Pr83rEgUgXgya/u8wk3h/bvX70w+Ff2skwjdPLr5dLUQ/n5KFX4r3bsNmA==
+"@react-native-community/cli-platform-ios@10.2.0":
+  version "10.2.0"
+  resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-ios/-/cli-platform-ios-10.2.0.tgz#be21c0e3bbf17358d540cc23e5556bf679f6322e"
+  integrity sha512-hIPK3iL/mL+0ChXmQ9uqqzNOKA48H+TAzg+hrxQLll/6dNMxDeK9/wZpktcsh8w+CyhqzKqVernGcQs7tPeKGw==
   dependencies:
     "@react-native-community/cli-tools" "^10.1.1"
     chalk "^4.1.2"
     execa "^1.0.0"
+    fast-xml-parser "^4.0.12"
     glob "^7.1.3"
     ora "^5.4.1"
 
@@ -2722,21 +2754,21 @@
     glob "^7.1.3"
     ora "^5.4.1"
 
-"@react-native-community/cli-plugin-metro@^10.1.1":
-  version "10.2.0"
-  resolved "https://registry.yarnpkg.com/@react-native-community/cli-plugin-metro/-/cli-plugin-metro-10.2.0.tgz#83cabbc04c80f7e94f88ed998b72c7d572c6f094"
-  integrity sha512-9eiJrKYuauEDkQLCrjJUh7tS9T0oaMQqVUSSSuyDG6du7HQcfaR4mSf21wK75jvhKiwcQLpsFmMdctAb+0v+Cg==
+"@react-native-community/cli-plugin-metro@^10.2.0":
+  version "10.2.2"
+  resolved "https://registry.yarnpkg.com/@react-native-community/cli-plugin-metro/-/cli-plugin-metro-10.2.2.tgz#766914e3c8007dfe52b253544c4f6cd8549919ac"
+  integrity sha512-sTGjZlD3OGqbF9v1ajwUIXhGmjw9NyJ/14Lo0sg7xH8Pv4qUd5ZvQ6+DWYrQn3IKFUMfGFWYyL81ovLuPylrpw==
   dependencies:
     "@react-native-community/cli-server-api" "^10.1.1"
     "@react-native-community/cli-tools" "^10.1.1"
     chalk "^4.1.2"
     execa "^1.0.0"
-    metro "0.73.8"
-    metro-config "0.73.8"
-    metro-core "0.73.8"
-    metro-react-native-babel-transformer "0.73.8"
-    metro-resolver "0.73.8"
-    metro-runtime "0.73.8"
+    metro "0.73.9"
+    metro-config "0.73.9"
+    metro-core "0.73.9"
+    metro-react-native-babel-transformer "0.73.9"
+    metro-resolver "0.73.9"
+    metro-runtime "0.73.9"
     readline "^1.3.0"
 
 "@react-native-community/cli-server-api@^10.1.1":
@@ -2776,17 +2808,17 @@
   dependencies:
     joi "^17.2.1"
 
-"@react-native-community/cli@10.1.3":
-  version "10.1.3"
-  resolved "https://registry.yarnpkg.com/@react-native-community/cli/-/cli-10.1.3.tgz#ad610c46da9fc7c717272024ec757dc646726506"
-  integrity sha512-kzh6bYLGN1q1q0IiczKSP1LTrovFeVzppYRTKohPI9VdyZwp7b5JOgaQMB/Ijtwm3MxBDrZgV9AveH/eUmUcKQ==
+"@react-native-community/cli@10.2.0":
+  version "10.2.0"
+  resolved "https://registry.yarnpkg.com/@react-native-community/cli/-/cli-10.2.0.tgz#bcb65bb3dcb03b0fc4e49619d51e12d23396b301"
+  integrity sha512-QH7AFBz5FX2zTZRH/o3XehHrZ0aZZEL5Sh+23nSEFgSj3bLFfvjjZhuoiRSAo7iiBdvAoXrfxQ8TXgg4Xf/7fw==
   dependencies:
     "@react-native-community/cli-clean" "^10.1.1"
     "@react-native-community/cli-config" "^10.1.1"
     "@react-native-community/cli-debugger-ui" "^10.0.0"
-    "@react-native-community/cli-doctor" "^10.1.1"
-    "@react-native-community/cli-hermes" "^10.1.3"
-    "@react-native-community/cli-plugin-metro" "^10.1.1"
+    "@react-native-community/cli-doctor" "^10.2.0"
+    "@react-native-community/cli-hermes" "^10.2.0"
+    "@react-native-community/cli-plugin-metro" "^10.2.0"
     "@react-native-community/cli-server-api" "^10.1.1"
     "@react-native-community/cli-tools" "^10.1.1"
     "@react-native-community/cli-types" "^10.0.0"
@@ -3019,13 +3051,6 @@
   dependencies:
     "@hapi/hoek" "^9.0.0"
 
-"@sideway/address@^5.0.0":
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/@sideway/address/-/address-5.0.0.tgz#015f191a4a29e2b2f9ad1aabe7465c3088241536"
-  integrity sha512-IEZ3Gi972M1yubSPhcpzpVTT/Vb46F9L0W+K/GhqvWv6aAvVbNNVsYFekXWEemHHFfTVrxFcURrzsPGPPKkxKQ==
-  dependencies:
-    "@hapi/hoek" "^10.0.0"
-
 "@sideway/formula@^3.0.1":
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f"
@@ -3314,6 +3339,26 @@
   resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
   integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
 
+"@tsconfig/node10@^1.0.7":
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2"
+  integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==
+
+"@tsconfig/node12@^1.0.7":
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d"
+  integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==
+
+"@tsconfig/node14@^1.0.0":
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1"
+  integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==
+
+"@tsconfig/node16@^1.0.2":
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e"
+  integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==
+
 "@tsconfig/react-native@^2.0.3":
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/@tsconfig/react-native/-/react-native-2.0.3.tgz#79ad8efc6d3729152da6cb23725b6c364a7349b2"
@@ -4098,7 +4143,7 @@ acorn-walk@^7.0.0, acorn-walk@^7.1.1:
   resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
   integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
 
-acorn-walk@^8.0.2:
+acorn-walk@^8.0.2, acorn-walk@^8.1.1:
   version "8.2.0"
   resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
   integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
@@ -4108,7 +4153,7 @@ acorn@^7.0.0, acorn@^7.1.1:
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
   integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
 
-acorn@^8.1.0, acorn@^8.2.4, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.0, acorn@^8.8.1:
+acorn@^8.1.0, acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.0, acorn@^8.8.1:
   version "8.8.2"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
   integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
@@ -4280,6 +4325,11 @@ arg@4.1.0:
   resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.0.tgz#583c518199419e0037abb74062c37f8519e575f0"
   integrity sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==
 
+arg@^4.1.0:
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
+  integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
+
 arg@^5.0.2:
   version "5.0.2"
   resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c"
@@ -4449,13 +4499,6 @@ async-limiter@~1.0.0:
   resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
   integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
 
-async-mutex@^0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.4.0.tgz#ae8048cd4d04ace94347507504b3cf15e631c25f"
-  integrity sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA==
-  dependencies:
-    tslib "^2.4.0"
-
 async@^3.2.2, async@^3.2.3:
   version "3.2.4"
   resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c"
@@ -4515,13 +4558,14 @@ axios@^0.24.0:
   dependencies:
     follow-redirects "^1.14.4"
 
-axios@^0.27.2:
-  version "0.27.2"
-  resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972"
-  integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==
+axios@^1.3.4:
+  version "1.3.4"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.4.tgz#f5760cefd9cfb51fd2481acf88c05f67c4523024"
+  integrity sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==
   dependencies:
-    follow-redirects "^1.14.9"
+    follow-redirects "^1.15.0"
     form-data "^4.0.0"
+    proxy-from-env "^1.1.0"
 
 axobject-query@^3.1.1:
   version "3.1.1"
@@ -4704,10 +4748,10 @@ babel-preset-current-node-syntax@^1.0.0:
     "@babel/plugin-syntax-optional-chaining" "^7.8.3"
     "@babel/plugin-syntax-top-level-await" "^7.8.3"
 
-babel-preset-expo@~9.3.0:
-  version "9.3.0"
-  resolved "https://registry.yarnpkg.com/babel-preset-expo/-/babel-preset-expo-9.3.0.tgz#51cb3c6e22126bcc14d17322d2f2dfb418e71222"
-  integrity sha512-cIz+5TVBkcZgtfpTyFPo1peswr2dvQj2VIwdj5vY37/zESsYBHfaZ+u/A11yb1WnuZHcYD/ZoSLNwmWr20jp4Q==
+babel-preset-expo@~9.3.1:
+  version "9.3.1"
+  resolved "https://registry.yarnpkg.com/babel-preset-expo/-/babel-preset-expo-9.3.1.tgz#b31ddfce0d7ed1d848705e7178c1eb4ae4be9db0"
+  integrity sha512-1JL4T7q3uXu9FeJhLXDAKhFbWs75Qj2pixA60eR2ROzE9LnrKxm2g42OfcArS4vJcPj2NzcOdPpMI9/ZgF8i8Q==
   dependencies:
     "@babel/plugin-proposal-decorators" "^7.12.9"
     "@babel/plugin-proposal-object-rest-spread" "^7.12.13"
@@ -4715,7 +4759,7 @@ babel-preset-expo@~9.3.0:
     "@babel/preset-env" "^7.20.0"
     babel-plugin-module-resolver "^4.1.0"
     babel-plugin-react-native-web "~0.18.10"
-    metro-react-native-babel-preset "0.73.7"
+    metro-react-native-babel-preset "0.73.8"
 
 babel-preset-fbjs@^3.4.0:
   version "3.4.0"
@@ -5760,6 +5804,11 @@ create-react-class@^15.7.0:
     loose-envify "^1.3.1"
     object-assign "^4.1.1"
 
+create-require@^1.1.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
+  integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
+
 crelt@^1.0.0:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.5.tgz#57c0d52af8c859e354bace1883eb2e1eb182bb94"
@@ -6387,6 +6436,11 @@ diff-sequences@^29.4.3:
   resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2"
   integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==
 
+diff@^4.0.1:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
+  integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
+
 dir-glob@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
@@ -6540,7 +6594,7 @@ dotenv@^10.0.0:
   resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81"
   integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==
 
-dotenv@^16.0.0, dotenv@^16.0.2, dotenv@^16.0.3:
+dotenv@^16.0.0, dotenv@^16.0.3:
   version "16.0.3"
   resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07"
   integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==
@@ -6800,16 +6854,16 @@ escape-html@~1.0.3:
   resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
   integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
 
-escape-string-regexp@2.0.0, escape-string-regexp@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
-  integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
-
 escape-string-regexp@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
   integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
 
+escape-string-regexp@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
+  integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
+
 escape-string-regexp@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
@@ -7375,10 +7429,10 @@ expo-modules-autolinking@1.1.2:
     find-up "^5.0.0"
     fs-extra "^9.1.0"
 
-expo-modules-core@1.2.5:
-  version "1.2.5"
-  resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-1.2.5.tgz#3f9166f4c32c68ab8ef3e120c70ce9890b711650"
-  integrity sha512-5pXNlLHNKLayOusAFMbqr27gjgymHuKuWl/Dtbw2MjoyJY1MZCGD2nIJxd1TTcfnyxNxLg6OQmgkyqoBUFqBuw==
+expo-modules-core@1.2.6:
+  version "1.2.6"
+  resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-1.2.6.tgz#921abc8031fe0e5474ee48905071902b9627d051"
+  integrity sha512-vyleKepkP8F6L+D55B/E4FbZ8x9pdy3yw/mdbGBkDkrmo2gmeMjOM1mKLSszOkLIqet05O7Wy8m0FZHZTo0VBg==
   dependencies:
     compare-versions "^3.4.0"
     invariant "^2.2.4"
@@ -7411,17 +7465,17 @@ expo-updates-interface@~0.9.0:
   resolved "https://registry.yarnpkg.com/expo-updates-interface/-/expo-updates-interface-0.9.1.tgz#e81308d551ed5a4c35c8770ac61434f6ca749610"
   integrity sha512-wk88LLhseQ7LJvxdN7BTKiryyqALxnrvr+lyHK3/prg76Yy0EGi2Q/oE/rtFyyZ1JmQDRbO/5pdX0EE6QqVQXQ==
 
-expo@~48.0.0-beta.2:
-  version "48.0.7"
-  resolved "https://registry.yarnpkg.com/expo/-/expo-48.0.7.tgz#7900bfda316d25127ed9c412daa31db66dc4a869"
-  integrity sha512-4sPW+HWm03z72FKIG9IddwEhF9+RlAUsTh8pnsoZjZbXALVikmV3QjD4zp/Dkt9YuiCAnJN1VBaT2AlhbYk2Rg==
+expo@~48.0.9:
+  version "48.0.9"
+  resolved "https://registry.yarnpkg.com/expo/-/expo-48.0.9.tgz#7beaecc09e0c364a2c152a0b8bd71060b2d37186"
+  integrity sha512-RlYpJSny4g3G2sqAfx1taaT7QFEw2cIfYLlZWmguA6EQSCviaeaQU1m4tvVXU1jIXb/w8jqer18XIq56VuECfg==
   dependencies:
     "@babel/runtime" "^7.20.0"
     "@expo/cli" "0.6.2"
     "@expo/config" "8.0.2"
     "@expo/config-plugins" "6.0.1"
     "@expo/vector-icons" "^13.0.0"
-    babel-preset-expo "~9.3.0"
+    babel-preset-expo "~9.3.1"
     cross-spawn "^6.0.5"
     expo-application "~5.1.1"
     expo-asset "~8.9.1"
@@ -7430,7 +7484,7 @@ expo@~48.0.0-beta.2:
     expo-font "~11.1.1"
     expo-keep-awake "~12.0.1"
     expo-modules-autolinking "1.1.2"
-    expo-modules-core "1.2.5"
+    expo-modules-core "1.2.6"
     fbemitter "^3.0.0"
     getenv "^1.0.0"
     invariant "^2.2.4"
@@ -7444,7 +7498,7 @@ express-async-errors@^3.1.1:
   resolved "https://registry.yarnpkg.com/express-async-errors/-/express-async-errors-3.1.1.tgz#6053236d61d21ddef4892d6bd1d736889fc9da41"
   integrity sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==
 
-express@^4.17.2, express@^4.17.3:
+express@^4.17.2, express@^4.17.3, express@^4.18.2:
   version "4.18.2"
   resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59"
   integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==
@@ -7573,6 +7627,11 @@ fast-redact@^3.1.1:
   resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.2.tgz#d58e69e9084ce9fa4c1a6fa98a3e1ecf5d7839aa"
   integrity sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==
 
+fast-text-encoding@^1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867"
+  integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==
+
 fast-xml-parser@^4.0.12:
   version "4.1.3"
   resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.1.3.tgz#0254ad0d4d27f07e6b48254b068c0c137488dd97"
@@ -7817,7 +7876,7 @@ flow-parser@^0.185.0:
   resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.185.2.tgz#cb7ee57f77377d6c5d69a469e980f6332a15e492"
   integrity sha512-2hJ5ACYeJCzNtiVULov6pljKOLygy0zddoqSI1fFetM+XRPpRshFdGEijtqlamA1XwyZ+7rhryI6FQFzvtLWUQ==
 
-follow-redirects@^1.0.0, follow-redirects@^1.14.4, follow-redirects@^1.14.9:
+follow-redirects@^1.0.0, follow-redirects@^1.14.4, follow-redirects@^1.15.0:
   version "1.15.2"
   resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
   integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
@@ -8235,6 +8294,11 @@ grapheme-splitter@^1.0.4:
   resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
   integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
 
+graphemer@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
+  integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
+
 graphql-tag@^2.10.1:
   version "2.12.6"
   resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1"
@@ -8726,7 +8790,7 @@ interpret@^3.1.1:
   resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4"
   integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==
 
-invariant@*, invariant@2.2.4, invariant@^2.2.4:
+invariant@*, invariant@^2.2.4:
   version "2.2.4"
   resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
   integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
@@ -9509,7 +9573,7 @@ jest-environment-node@^29.2.1, jest-environment-node@^29.5.0:
     jest-mock "^29.5.0"
     jest-util "^29.5.0"
 
-jest-expo@^48.0.0-beta.2:
+jest-expo@^48.0.2:
   version "48.0.2"
   resolved "https://registry.yarnpkg.com/jest-expo/-/jest-expo-48.0.2.tgz#eedab424e29e9bec2cf17a2fe1a653096ec82b04"
   integrity sha512-hxppv3I3/WgtswladHpPlcEHCv+5/6OG8nOuR3VqtS0h7ZJYuyQCMpXbsKZiA4R/sT4fHS0BUj9BBsdhrk/zXg==
@@ -10504,6 +10568,11 @@ kysely@^0.22.0:
   resolved "https://registry.yarnpkg.com/kysely/-/kysely-0.22.0.tgz#8aac53942da3cadc604d7d154a746d983fe8f7b9"
   integrity sha512-ZE3qWtnqLOalodzfK5QUEcm7AEulhxsPNuKaGFsC3XiqO92vMLm+mAHk/NnbSIOtC4RmGm0nsv700i8KDp1gfQ==
 
+kysely@^0.23.4:
+  version "0.23.5"
+  resolved "https://registry.yarnpkg.com/kysely/-/kysely-0.23.5.tgz#60c63d94e1c42cc0411be8aaa688a0f27405f514"
+  integrity sha512-TH+b56pVXQq0tsyooYLeNfV11j6ih7D50dyN8tkM0e7ndiUH28Nziojiog3qRFlmEj9XePYdZUrNJ2079Qjdow==
+
 lande@^1.0.10:
   version "1.0.10"
   resolved "https://registry.yarnpkg.com/lande/-/lande-1.0.10.tgz#1f6c6542e628338eb18def22edd1038f5fce9e7a"
@@ -10802,7 +10871,7 @@ make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0:
   dependencies:
     semver "^6.0.0"
 
-make-error@^1.3.6:
+make-error@^1.1.1, make-error@^1.3.6:
   version "1.3.6"
   resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
   integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
@@ -10931,16 +11000,6 @@ methods@~1.1.2:
   resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
   integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
 
-metro-babel-transformer@0.73.7:
-  version "0.73.7"
-  resolved "https://registry.yarnpkg.com/metro-babel-transformer/-/metro-babel-transformer-0.73.7.tgz#561ffa0336eb6d7d112e7128e957114c729fdb71"
-  integrity sha512-s7UVkwovGTEXYEQrv5hcmSBbFJ9s9lhCRNMScn4Itgj3UMdqRr9lU8DXKEFlJ7osgRxN6n5+eXqcvhE4B1H1VQ==
-  dependencies:
-    "@babel/core" "^7.20.0"
-    hermes-parser "0.8.0"
-    metro-source-map "0.73.7"
-    nullthrows "^1.1.1"
-
 metro-babel-transformer@0.73.8:
   version "0.73.8"
   resolved "https://registry.yarnpkg.com/metro-babel-transformer/-/metro-babel-transformer-0.73.8.tgz#521374cb9234ba126f3f8d63588db5901308b4ed"
@@ -10951,43 +11010,53 @@ metro-babel-transformer@0.73.8:
     metro-source-map "0.73.8"
     nullthrows "^1.1.1"
 
-metro-cache-key@0.73.8:
-  version "0.73.8"
-  resolved "https://registry.yarnpkg.com/metro-cache-key/-/metro-cache-key-0.73.8.tgz#afc9f63454edbd9d207544445a66e8a4e119462d"
-  integrity sha512-VzFGu4kJGIkLjyDgVoM2ZxIHlMdCZWMqVIux9N+EeyMVMvGXTiXW8eGROgxzDhVjyR58IjfMsYpRCKz5dR+2ew==
+metro-babel-transformer@0.73.9:
+  version "0.73.9"
+  resolved "https://registry.yarnpkg.com/metro-babel-transformer/-/metro-babel-transformer-0.73.9.tgz#bec8aaaf1bbdc2e469fde586fde455f8b2a83073"
+  integrity sha512-DlYwg9wwYIZTHtic7dyD4BP0SDftoltZ3clma76nHu43blMWsCnrImHeHsAVne3XsQ+RJaSRxhN5nkG2VyVHwA==
+  dependencies:
+    "@babel/core" "^7.20.0"
+    hermes-parser "0.8.0"
+    metro-source-map "0.73.9"
+    nullthrows "^1.1.1"
 
-metro-cache@0.73.8:
-  version "0.73.8"
-  resolved "https://registry.yarnpkg.com/metro-cache/-/metro-cache-0.73.8.tgz#85e2d7f7c7c74d1f942b7ecd168f7aceb987d883"
-  integrity sha512-/uFbTIw813Rvb8kSAIHvax9gWl41dtgjY2SpJLNIBLdQ6oFZ3CVo3ahZIiEZOrCeHl9xfGn5tmvNb8CEFa/Q5w==
+metro-cache-key@0.73.9:
+  version "0.73.9"
+  resolved "https://registry.yarnpkg.com/metro-cache-key/-/metro-cache-key-0.73.9.tgz#7d8c441a3b7150f7b201273087ef3cf7d3435d9f"
+  integrity sha512-uJg+6Al7UoGIuGfoxqPBy6y1Ewq7Y8/YapGYIDh6sohInwt/kYKnPZgLDYHIPvY2deORnQ/2CYo4tOeBTnhCXQ==
+
+metro-cache@0.73.9:
+  version "0.73.9"
+  resolved "https://registry.yarnpkg.com/metro-cache/-/metro-cache-0.73.9.tgz#773c2df6ba53434e58ccbe421b0c54e6da8d2890"
+  integrity sha512-upiRxY8rrQkUWj7ieACD6tna7xXuXdu2ZqrheksT79ePI0aN/t0memf6WcyUtJUMHZetke3j+ppELNvlmp3tOw==
   dependencies:
-    metro-core "0.73.8"
+    metro-core "0.73.9"
     rimraf "^3.0.2"
 
-metro-config@0.73.8:
-  version "0.73.8"
-  resolved "https://registry.yarnpkg.com/metro-config/-/metro-config-0.73.8.tgz#8f6c22c94528919635c6688ed8d2ad8a10c70b27"
-  integrity sha512-sAYq+llL6ZAfro64U99ske8HcKKswxX4wIZbll9niBKG7TkWm7tfMY1jO687XEmE4683rHncZeBRav9pLngIzg==
+metro-config@0.73.9:
+  version "0.73.9"
+  resolved "https://registry.yarnpkg.com/metro-config/-/metro-config-0.73.9.tgz#6b43c70681bdd6b00f44400fc76dddbe53374500"
+  integrity sha512-NiWl1nkYtjqecDmw77tbRbXnzIAwdO6DXGZTuKSkH+H/c1NKq1eizO8Fe+NQyFtwR9YLqn8Q0WN1nmkwM1j8CA==
   dependencies:
     cosmiconfig "^5.0.5"
     jest-validate "^26.5.2"
-    metro "0.73.8"
-    metro-cache "0.73.8"
-    metro-core "0.73.8"
-    metro-runtime "0.73.8"
+    metro "0.73.9"
+    metro-cache "0.73.9"
+    metro-core "0.73.9"
+    metro-runtime "0.73.9"
 
-metro-core@0.73.8:
-  version "0.73.8"
-  resolved "https://registry.yarnpkg.com/metro-core/-/metro-core-0.73.8.tgz#a31ba7d7bfe3f4c2ac2c7a2493aa4229ecad701e"
-  integrity sha512-Aew4dthbZf8bRRjlYGL3cnai3+LKYTf6mc7YS2xLQRWtgGZ1b/H8nQtBvXZpfRYFcS84UeEQ10vwIf5eR3qPdQ==
+metro-core@0.73.9:
+  version "0.73.9"
+  resolved "https://registry.yarnpkg.com/metro-core/-/metro-core-0.73.9.tgz#410c5c0aeae840536c10039f68098fdab3da568e"
+  integrity sha512-1NTs0IErlKcFTfYyRT3ljdgrISWpl1nys+gaHkXapzTSpvtX9F1NQNn5cgAuE+XIuTJhbsCdfIJiM2JXbrJQaQ==
   dependencies:
     lodash.throttle "^4.1.1"
-    metro-resolver "0.73.8"
+    metro-resolver "0.73.9"
 
-metro-file-map@0.73.8:
-  version "0.73.8"
-  resolved "https://registry.yarnpkg.com/metro-file-map/-/metro-file-map-0.73.8.tgz#88d666e7764e1b0adf5fd634d91e97e3135d2db7"
-  integrity sha512-CM552hUO9om02jJdLszOCIDADKNaaeVz8CjYXItndvgr5jmFlQYAR+UMvaDzeT8oYdAV1DXAljma2CS2UBymPg==
+metro-file-map@0.73.9:
+  version "0.73.9"
+  resolved "https://registry.yarnpkg.com/metro-file-map/-/metro-file-map-0.73.9.tgz#09c04a8e8ef1eaa6ecb2b9cb8cb53bb0fa0167ec"
+  integrity sha512-R/Wg3HYeQhYY3ehWtfedw8V0ne4lpufG7a21L3GWer8tafnC9pmjoCKEbJz9XZkVj9i1FtxE7UTbrtZNeIILxQ==
   dependencies:
     abort-controller "^3.0.0"
     anymatch "^3.0.3"
@@ -11005,39 +11074,39 @@ metro-file-map@0.73.8:
   optionalDependencies:
     fsevents "^2.3.2"
 
-metro-hermes-compiler@0.73.8:
-  version "0.73.8"
-  resolved "https://registry.yarnpkg.com/metro-hermes-compiler/-/metro-hermes-compiler-0.73.8.tgz#c522e2c97afc8bdc249755d88146a75720bc2498"
-  integrity sha512-2d7t+TEoQLk+jyXgBykmAtPPJK2B46DB3qUYIMKDFDDaKzCljrojyVuGgQq6SM1f95fe6HDAQ3K9ihTjeB90yw==
+metro-hermes-compiler@0.73.9:
+  version "0.73.9"
+  resolved "https://registry.yarnpkg.com/metro-hermes-compiler/-/metro-hermes-compiler-0.73.9.tgz#6f473e67e8f76066066f00e2e0ecce865f7d445d"
+  integrity sha512-5B3vXIwQkZMSh3DQQY23XpTCpX9kPLqZbA3rDuAcbGW0tzC3f8dCenkyBb0GcCzyTDncJeot/A7oVCVK6zapwg==
 
-metro-inspector-proxy@0.73.8:
-  version "0.73.8"
-  resolved "https://registry.yarnpkg.com/metro-inspector-proxy/-/metro-inspector-proxy-0.73.8.tgz#67d5aadfc33fe97f61c716eb168db4bd5d0e3c96"
-  integrity sha512-F0QxwDTox0TDeXVRN7ZmI7BknBjPDVKQ1ZeKznFBiMa0SXiD1kzoksfpDbZ6hTEKrhVM9Ep0YQmC7avwZouOnA==
+metro-inspector-proxy@0.73.9:
+  version "0.73.9"
+  resolved "https://registry.yarnpkg.com/metro-inspector-proxy/-/metro-inspector-proxy-0.73.9.tgz#8e11cd300adf3f904f1f5afe28b198312cdcd8c2"
+  integrity sha512-B3WrWZnlYhtTrv0IaX3aUAhi2qVILPAZQzb5paO1e+xrz4YZHk9c7dXv7qe7B/IQ132e3w46y3AL7rFo90qVjA==
   dependencies:
     connect "^3.6.5"
     debug "^2.2.0"
     ws "^7.5.1"
     yargs "^17.5.1"
 
-metro-minify-terser@0.73.8:
-  version "0.73.8"
-  resolved "https://registry.yarnpkg.com/metro-minify-terser/-/metro-minify-terser-0.73.8.tgz#a0fe857d6aaf99cba3a2aef59ee06ac409682c6b"
-  integrity sha512-pnagyXAoMPhihWrHRIWqCxrP6EJ8Hfugv5RXBb6HbOANmwajn2uQuzeu18+dXaN1yPoDCMCgpg/UA4ibFN5jtQ==
+metro-minify-terser@0.73.9:
+  version "0.73.9"
+  resolved "https://registry.yarnpkg.com/metro-minify-terser/-/metro-minify-terser-0.73.9.tgz#301aef2e106b0802f7a14ef0f2b4883b20c80018"
+  integrity sha512-MTGPu2qV5qtzPJ2SqH6s58awHDtZ4jd7lmmLR+7TXDwtZDjIBA0YVfI0Zak2Haby2SqoNKrhhUns/b4dPAQAVg==
   dependencies:
     terser "^5.15.0"
 
-metro-minify-uglify@0.73.8:
-  version "0.73.8"
-  resolved "https://registry.yarnpkg.com/metro-minify-uglify/-/metro-minify-uglify-0.73.8.tgz#b2e2430014c340479db4fc393a2ea4c5bad75ecd"
-  integrity sha512-9wZqKfraVfmtMXdOzRyan+6r1woQXqqa4KeXfVh7+Mxl+5+J0Lmw6EvTrWawsaOEpvpn32q9MfoHC1d8plDJwA==
+metro-minify-uglify@0.73.9:
+  version "0.73.9"
+  resolved "https://registry.yarnpkg.com/metro-minify-uglify/-/metro-minify-uglify-0.73.9.tgz#cf4f8c19b688deea103905689ec736c2f2acd733"
+  integrity sha512-gzxD/7WjYcnCNGiFJaA26z34rjOp+c/Ft++194Wg91lYep3TeWQ0CnH8t2HRS7AYDHU81SGWgvD3U7WV0g4LGA==
   dependencies:
     uglify-es "^3.1.9"
 
-metro-react-native-babel-preset@0.73.7:
-  version "0.73.7"
-  resolved "https://registry.yarnpkg.com/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.73.7.tgz#78e1ce448aa9a5cf3651c0ebe73cb225465211b4"
-  integrity sha512-RKcmRZREjJCzHKP+JhC9QTCohkeb3xa/DtqHU14U5KWzJHdC0mMrkTZYNXhV0cryxsaVKVEw5873KhbZyZHMVw==
+metro-react-native-babel-preset@0.73.8, metro-react-native-babel-preset@^0.73.7:
+  version "0.73.8"
+  resolved "https://registry.yarnpkg.com/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.73.8.tgz#04908f264f5d99c944ae20b5b11f659431328431"
+  integrity sha512-spNrcQJTbQntEIqJnCA6yL4S+dzV9fXCk7U+Rm7yJasZ4o4Frn7jP23isu7FlZIp1Azx1+6SbP7SgQM+IP5JgQ==
   dependencies:
     "@babel/core" "^7.20.0"
     "@babel/plugin-proposal-async-generator-functions" "^7.0.0"
@@ -11078,10 +11147,10 @@ metro-react-native-babel-preset@0.73.7:
     "@babel/template" "^7.0.0"
     react-refresh "^0.4.0"
 
-metro-react-native-babel-preset@0.73.8, metro-react-native-babel-preset@^0.73.7:
-  version "0.73.8"
-  resolved "https://registry.yarnpkg.com/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.73.8.tgz#04908f264f5d99c944ae20b5b11f659431328431"
-  integrity sha512-spNrcQJTbQntEIqJnCA6yL4S+dzV9fXCk7U+Rm7yJasZ4o4Frn7jP23isu7FlZIp1Azx1+6SbP7SgQM+IP5JgQ==
+metro-react-native-babel-preset@0.73.9:
+  version "0.73.9"
+  resolved "https://registry.yarnpkg.com/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.73.9.tgz#ef54637dd20f025197beb49e71309a9c539e73e2"
+  integrity sha512-AoD7v132iYDV4K78yN2OLgTPwtAKn0XlD2pOhzyBxiI8PeXzozhbKyPV7zUOJUPETj+pcEVfuYj5ZN/8+bhbCw==
   dependencies:
     "@babel/core" "^7.20.0"
     "@babel/plugin-proposal-async-generator-functions" "^7.0.0"
@@ -11122,19 +11191,6 @@ metro-react-native-babel-preset@0.73.8, metro-react-native-babel-preset@^0.73.7:
     "@babel/template" "^7.0.0"
     react-refresh "^0.4.0"
 
-metro-react-native-babel-transformer@0.73.7:
-  version "0.73.7"
-  resolved "https://registry.yarnpkg.com/metro-react-native-babel-transformer/-/metro-react-native-babel-transformer-0.73.7.tgz#a92055fd564cd403255cc34f925c5e99ce457565"
-  integrity sha512-73HW8betjX+VPm3iqsMBe8F/F2Tt+hONO6YJwcF7FonTqQYW1oTz0dOp0dClZGfHUXxpJBz6Vuo7J6TpdzDD+w==
-  dependencies:
-    "@babel/core" "^7.20.0"
-    babel-preset-fbjs "^3.4.0"
-    hermes-parser "0.8.0"
-    metro-babel-transformer "0.73.7"
-    metro-react-native-babel-preset "0.73.7"
-    metro-source-map "0.73.7"
-    nullthrows "^1.1.1"
-
 metro-react-native-babel-transformer@0.73.8:
   version "0.73.8"
   resolved "https://registry.yarnpkg.com/metro-react-native-babel-transformer/-/metro-react-native-babel-transformer-0.73.8.tgz#cbcd4b243216878431dc4311ce46f02a928e3991"
@@ -11148,20 +11204,25 @@ metro-react-native-babel-transformer@0.73.8:
     metro-source-map "0.73.8"
     nullthrows "^1.1.1"
 
-metro-resolver@0.73.8:
-  version "0.73.8"
-  resolved "https://registry.yarnpkg.com/metro-resolver/-/metro-resolver-0.73.8.tgz#65cc158575d130363296f66a33257c7971228640"
-  integrity sha512-GiBWont7/OgAftkkj2TiEp+Gf1PYZUk8xV4MbtnQjIKyy3MlGY3GbpMQ1BHih9GUQqlF0n9jsUlC2K5P0almXQ==
+metro-react-native-babel-transformer@0.73.9:
+  version "0.73.9"
+  resolved "https://registry.yarnpkg.com/metro-react-native-babel-transformer/-/metro-react-native-babel-transformer-0.73.9.tgz#4f4f0cfa5119bab8b53e722fabaf90687d0cbff0"
+  integrity sha512-DSdrEHuQ22ixY7DyipyKkIcqhOJrt5s6h6X7BYJCP9AMUfXOwLe2biY3BcgJz5GOXv8/Akry4vTCvQscVS1otQ==
   dependencies:
-    absolute-path "^0.0.0"
+    "@babel/core" "^7.20.0"
+    babel-preset-fbjs "^3.4.0"
+    hermes-parser "0.8.0"
+    metro-babel-transformer "0.73.9"
+    metro-react-native-babel-preset "0.73.9"
+    metro-source-map "0.73.9"
+    nullthrows "^1.1.1"
 
-metro-runtime@0.73.7:
-  version "0.73.7"
-  resolved "https://registry.yarnpkg.com/metro-runtime/-/metro-runtime-0.73.7.tgz#9f3a7f3ff668c1a87370650e32b47d8f6329fd1e"
-  integrity sha512-2fxRGrF8FyrwwHY0TCitdUljzutfW6CWEpdvPilfrs8p0PI5X8xOWg8ficeYtw+DldHtHIAL2phT59PqzHTyVA==
+metro-resolver@0.73.9:
+  version "0.73.9"
+  resolved "https://registry.yarnpkg.com/metro-resolver/-/metro-resolver-0.73.9.tgz#f3cf77e6c7606a34aa81bad40edb856aad671cf3"
+  integrity sha512-Ej3wAPOeNRPDnJmkK0zk7vJ33iU07n+oPhpcf5L0NFkWneMmSM2bflMPibI86UjzZGmRfn0AhGhs8yGeBwQ/Xg==
   dependencies:
-    "@babel/runtime" "^7.0.0"
-    react-refresh "^0.4.0"
+    absolute-path "^0.0.0"
 
 metro-runtime@0.73.8:
   version "0.73.8"
@@ -11171,19 +11232,13 @@ metro-runtime@0.73.8:
     "@babel/runtime" "^7.0.0"
     react-refresh "^0.4.0"
 
-metro-source-map@0.73.7:
-  version "0.73.7"
-  resolved "https://registry.yarnpkg.com/metro-source-map/-/metro-source-map-0.73.7.tgz#8e9f850a72d60ea7ace05b984f981c8ec843e7a0"
-  integrity sha512-gbC/lfUN52TtQhEsTTA+987MaFUpQlufuCI05blLGLosDcFCsARikHsxa65Gtslm/rG2MqvFLiPA5hviONNv9g==
+metro-runtime@0.73.9:
+  version "0.73.9"
+  resolved "https://registry.yarnpkg.com/metro-runtime/-/metro-runtime-0.73.9.tgz#0b24c0b066b8629ee855a6e5035b65061fef60d5"
+  integrity sha512-d5Hs83FpKB9r8q8Vb95+fa6ESpwysmPr4lL1I2rM2qXAFiO7OAPT9Bc23WmXgidkBtD0uUFdB2lG+H1ATz8rZg==
   dependencies:
-    "@babel/traverse" "^7.20.0"
-    "@babel/types" "^7.20.0"
-    invariant "^2.2.4"
-    metro-symbolicate "0.73.7"
-    nullthrows "^1.1.1"
-    ob1 "0.73.7"
-    source-map "^0.5.6"
-    vlq "^1.0.0"
+    "@babel/runtime" "^7.0.0"
+    react-refresh "^0.4.0"
 
 metro-source-map@0.73.8:
   version "0.73.8"
@@ -11199,16 +11254,18 @@ metro-source-map@0.73.8:
     source-map "^0.5.6"
     vlq "^1.0.0"
 
-metro-symbolicate@0.73.7:
-  version "0.73.7"
-  resolved "https://registry.yarnpkg.com/metro-symbolicate/-/metro-symbolicate-0.73.7.tgz#40e4cda81f8030b86afe391b5e686a0b06822b0a"
-  integrity sha512-571ThWmX5o8yGNzoXjlcdhmXqpByHU/bSZtWKhtgV2TyIAzYCYt4hawJAS5+/qDazUvjHdm8BbdqFUheM0EKNQ==
+metro-source-map@0.73.9:
+  version "0.73.9"
+  resolved "https://registry.yarnpkg.com/metro-source-map/-/metro-source-map-0.73.9.tgz#89ca41f6346aeb12f7f23496fa363e520adafebe"
+  integrity sha512-l4VZKzdqafipriETYR6lsrwtavCF1+CMhCOY9XbyWeTrpGSNgJQgdeJpttzEZTHQQTLR0csQo0nD1ef3zEP6IQ==
   dependencies:
+    "@babel/traverse" "^7.20.0"
+    "@babel/types" "^7.20.0"
     invariant "^2.2.4"
-    metro-source-map "0.73.7"
+    metro-symbolicate "0.73.9"
     nullthrows "^1.1.1"
+    ob1 "0.73.9"
     source-map "^0.5.6"
-    through2 "^2.0.1"
     vlq "^1.0.0"
 
 metro-symbolicate@0.73.8:
@@ -11223,10 +11280,22 @@ metro-symbolicate@0.73.8:
     through2 "^2.0.1"
     vlq "^1.0.0"
 
-metro-transform-plugins@0.73.8:
-  version "0.73.8"
-  resolved "https://registry.yarnpkg.com/metro-transform-plugins/-/metro-transform-plugins-0.73.8.tgz#07be7fd94a448ea1b245ab02ce7d277d757f9a32"
-  integrity sha512-IxjlnB5eA49M0WfvPEzvRikK3Rr6bECUUfcZt/rWpSphq/mttgyLYcHQ+VTZZl0zHolC3cTLwgoDod4IIJBn1A==
+metro-symbolicate@0.73.9:
+  version "0.73.9"
+  resolved "https://registry.yarnpkg.com/metro-symbolicate/-/metro-symbolicate-0.73.9.tgz#cb452299a36e5b86b2826e7426d51221635c48bf"
+  integrity sha512-4TUOwxRHHqbEHxRqRJ3wZY5TA8xq7AHMtXrXcjegMH9FscgYztsrIG9aNBUBS+VLB6g1qc6BYbfIgoAnLjCDyw==
+  dependencies:
+    invariant "^2.2.4"
+    metro-source-map "0.73.9"
+    nullthrows "^1.1.1"
+    source-map "^0.5.6"
+    through2 "^2.0.1"
+    vlq "^1.0.0"
+
+metro-transform-plugins@0.73.9:
+  version "0.73.9"
+  resolved "https://registry.yarnpkg.com/metro-transform-plugins/-/metro-transform-plugins-0.73.9.tgz#9fffbe1b24269e3d114286fa681abc570072d9b8"
+  integrity sha512-r9NeiqMngmooX2VOKLJVQrMuV7PAydbqst5bFhdVBPcFpZkxxqyzjzo+kzrszGy2UpSQBZr2P1L6OMjLHwQwfQ==
   dependencies:
     "@babel/core" "^7.20.0"
     "@babel/generator" "^7.20.0"
@@ -11234,29 +11303,29 @@ metro-transform-plugins@0.73.8:
     "@babel/traverse" "^7.20.0"
     nullthrows "^1.1.1"
 
-metro-transform-worker@0.73.8:
-  version "0.73.8"
-  resolved "https://registry.yarnpkg.com/metro-transform-worker/-/metro-transform-worker-0.73.8.tgz#701a006c2b4d93f1bb24802f3f2834c963153db9"
-  integrity sha512-B8kR6lmcvyG4UFSF2QDfr/eEnWJvg0ZadooF8Dg6m/3JSm9OAqfSoC0YrWqAuvtWImNDnbeKWN7/+ns44Hv6tg==
+metro-transform-worker@0.73.9:
+  version "0.73.9"
+  resolved "https://registry.yarnpkg.com/metro-transform-worker/-/metro-transform-worker-0.73.9.tgz#30384cef2d5e35a4abe91b15bf1a8344f5720441"
+  integrity sha512-Rq4b489sIaTUENA+WCvtu9yvlT/C6zFMWhU4sq+97W29Zj0mPBjdk+qGT5n1ZBgtBIJzZWt1KxeYuc17f4aYtQ==
   dependencies:
     "@babel/core" "^7.20.0"
     "@babel/generator" "^7.20.0"
     "@babel/parser" "^7.20.0"
     "@babel/types" "^7.20.0"
     babel-preset-fbjs "^3.4.0"
-    metro "0.73.8"
-    metro-babel-transformer "0.73.8"
-    metro-cache "0.73.8"
-    metro-cache-key "0.73.8"
-    metro-hermes-compiler "0.73.8"
-    metro-source-map "0.73.8"
-    metro-transform-plugins "0.73.8"
+    metro "0.73.9"
+    metro-babel-transformer "0.73.9"
+    metro-cache "0.73.9"
+    metro-cache-key "0.73.9"
+    metro-hermes-compiler "0.73.9"
+    metro-source-map "0.73.9"
+    metro-transform-plugins "0.73.9"
     nullthrows "^1.1.1"
 
-metro@0.73.8:
-  version "0.73.8"
-  resolved "https://registry.yarnpkg.com/metro/-/metro-0.73.8.tgz#25f014e4064eb34a4833c316e0a9094528061a8c"
-  integrity sha512-2EMJME9w5x7Uzn+DnQ4hzWr33u/aASaOBGdpf4lxbrlk6/vl4UBfX1sru6KU535qc/0Z1BMt4Vq9qsP3ZGFmWg==
+metro@0.73.9:
+  version "0.73.9"
+  resolved "https://registry.yarnpkg.com/metro/-/metro-0.73.9.tgz#150e69a6735fab0bcb4f6ee97fd1efc65b3ec36f"
+  integrity sha512-BlYbPmTF60hpetyNdKhdvi57dSqutb+/oK0u3ni4emIh78PiI0axGo7RfdsZ/mn3saASXc94tDbpC5yn7+NpEg==
   dependencies:
     "@babel/code-frame" "^7.0.0"
     "@babel/core" "^7.20.0"
@@ -11280,23 +11349,23 @@ metro@0.73.8:
     invariant "^2.2.4"
     jest-worker "^27.2.0"
     lodash.throttle "^4.1.1"
-    metro-babel-transformer "0.73.8"
-    metro-cache "0.73.8"
-    metro-cache-key "0.73.8"
-    metro-config "0.73.8"
-    metro-core "0.73.8"
-    metro-file-map "0.73.8"
-    metro-hermes-compiler "0.73.8"
-    metro-inspector-proxy "0.73.8"
-    metro-minify-terser "0.73.8"
-    metro-minify-uglify "0.73.8"
-    metro-react-native-babel-preset "0.73.8"
-    metro-resolver "0.73.8"
-    metro-runtime "0.73.8"
-    metro-source-map "0.73.8"
-    metro-symbolicate "0.73.8"
-    metro-transform-plugins "0.73.8"
-    metro-transform-worker "0.73.8"
+    metro-babel-transformer "0.73.9"
+    metro-cache "0.73.9"
+    metro-cache-key "0.73.9"
+    metro-config "0.73.9"
+    metro-core "0.73.9"
+    metro-file-map "0.73.9"
+    metro-hermes-compiler "0.73.9"
+    metro-inspector-proxy "0.73.9"
+    metro-minify-terser "0.73.9"
+    metro-minify-uglify "0.73.9"
+    metro-react-native-babel-preset "0.73.9"
+    metro-resolver "0.73.9"
+    metro-runtime "0.73.9"
+    metro-source-map "0.73.9"
+    metro-symbolicate "0.73.9"
+    metro-transform-plugins "0.73.9"
+    metro-transform-worker "0.73.9"
     mime-types "^2.1.27"
     node-fetch "^2.2.0"
     nullthrows "^1.1.1"
@@ -11796,16 +11865,16 @@ nwsapi@^2.2.0, nwsapi@^2.2.2:
   resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0"
   integrity sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==
 
-ob1@0.73.7:
-  version "0.73.7"
-  resolved "https://registry.yarnpkg.com/ob1/-/ob1-0.73.7.tgz#14c9b6ddc26cf99144f59eb542d7ae956e6b3192"
-  integrity sha512-DfelfvR843KADhSUATGGhuepVMRcf5VQX+6MQLy5AW0BKDLlO7Usj6YZeAAZP7P86QwsoTxB0RXCFiA7t6S1IQ==
-
 ob1@0.73.8:
   version "0.73.8"
   resolved "https://registry.yarnpkg.com/ob1/-/ob1-0.73.8.tgz#c569f1a15ce2d04da6fd70293ad44b5a93b11978"
   integrity sha512-1F7j+jzD+edS6ohQP7Vg5f3yiIk5i3x1uLrNIHOmLHWzWK1t3zrDpjnoXghccdVlsU+UjbyURnDynm4p0GgXeA==
 
+ob1@0.73.9:
+  version "0.73.9"
+  resolved "https://registry.yarnpkg.com/ob1/-/ob1-0.73.9.tgz#d5677a0dd3e2f16ad84231278d79424436c38c59"
+  integrity sha512-kHOzCOFXmAM26fy7V/YuXNKne2TyRiXbFAvPBIbuedJCZZWQZHLdPzMeXJI4Egt6IcfDttRzN3jQ90wOwq1iNw==
+
 object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@@ -12358,7 +12427,7 @@ pg-types@^2.1.0:
     postgres-date "~1.0.4"
     postgres-interval "^1.1.0"
 
-pg@^8.8.0:
+pg@^8.8.0, pg@^8.9.0:
   version "8.10.0"
   resolved "https://registry.yarnpkg.com/pg/-/pg-8.10.0.tgz#5b8379c9b4a36451d110fc8cd98fc325fe62ad24"
   integrity sha512-ke7o7qSTMb47iwzOSaZMfeR7xToFdkE71ifIipOAAaLIM0DYzfOAXlgFFmYUIE2BcJtvnVlGCID84ZzCegE8CQ==
@@ -12423,7 +12492,7 @@ pino-abstract-transport@v1.0.0:
     readable-stream "^4.0.0"
     split2 "^4.0.0"
 
-pino-http@^8.2.1:
+pino-http@^8.2.1, pino-http@^8.3.3:
   version "8.3.3"
   resolved "https://registry.yarnpkg.com/pino-http/-/pino-http-8.3.3.tgz#2b140e734bfc6babe0df272a43bb8f36f2b525c0"
   integrity sha512-p4umsNIXXVu95HD2C8wie/vXH7db5iGRpc+yj1/ZQ3sRtTQLXNjoS6Be5+eI+rQbqCRxen/7k/KSN+qiZubGDw==
@@ -12438,7 +12507,7 @@ pino-std-serializers@^6.0.0:
   resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.1.0.tgz#307490fd426eefc95e06067e85d8558603e8e844"
   integrity sha512-KO0m2f1HkrPe9S0ldjx7za9BJjeHqBku5Ch8JyxETxT8dEFGz1PwgrHaOQupVYitpzbFSYm7nnljxD8dik2c+g==
 
-pino@^8.0.0, pino@^8.6.1:
+pino@^8.0.0, pino@^8.11.0, pino@^8.6.1:
   version "8.11.0"
   resolved "https://registry.yarnpkg.com/pino/-/pino-8.11.0.tgz#2a91f454106b13e708a66c74ebc1c2ab7ab38498"
   integrity sha512-Z2eKSvlrl2rH8p5eveNUnTdd4AjJk8tAsLkHYZQKGHP4WTh2Gi1cOSOs3eWPqaj+niS3gj4UkoreoaWgF3ZWYg==
@@ -13406,6 +13475,11 @@ proxy-addr@~2.0.7:
     forwarded "0.2.0"
     ipaddr.js "1.9.1"
 
+proxy-from-env@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
+  integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+
 pseudomap@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
@@ -13451,13 +13525,6 @@ qs@6.11.0:
   dependencies:
     side-channel "^1.0.4"
 
-qs@^6.5.1:
-  version "6.11.1"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.1.tgz#6c29dff97f0c0060765911ba65cbc9764186109f"
-  integrity sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==
-  dependencies:
-    side-channel "^1.0.4"
-
 query-string@^7.1.3:
   version "7.1.3"
   resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.3.tgz#a1cf90e994abb113a325804a972d98276fe02328"
@@ -13699,7 +13766,7 @@ react-native-get-random-values@^1.8.0:
   dependencies:
     fast-base64-decode "^1.0.0"
 
-react-native-gradle-plugin@^0.71.15:
+react-native-gradle-plugin@^0.71.16:
   version "0.71.16"
   resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.71.16.tgz#822bb0c680e03b5df5aa65f2e5ffc2bc2930854a"
   integrity sha512-H2BjG2zk7B7Wii9sXvd9qhCVRQYDAHSWdMw9tscmZBqSP62DkIWEQSk4/B2GhQ4aK9ydVXgtqR6tBeg3yy8TSA==
@@ -13804,13 +13871,6 @@ react-native-web-linear-gradient@^1.1.2:
   resolved "https://registry.yarnpkg.com/react-native-web-linear-gradient/-/react-native-web-linear-gradient-1.1.2.tgz#33f85f7085a0bb5ffa5106faf02ed105b92a9ed7"
   integrity sha512-SmUnpwT49CEe78pXvIvYf72Es8Pv+ZYKCnEOgb2zAKpEUDMo0+xElfRJhwt5nfI8krJ5WbFPKnoDgD0uUjAN1A==
 
-react-native-web-webview@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/react-native-web-webview/-/react-native-web-webview-1.0.2.tgz#c215efa70c17589f2c8d640b1f1dc669b18c6e02"
-  integrity sha512-oNAYNuqUqeqTuAAdIejzDqvUtYA+k5lrvhUYmASdUznZNmyIaoQFA6OKoA4K9F3wdMvark42vUXkUWIp875ewg==
-  dependencies:
-    qs "^6.5.1"
-
 react-native-web@^0.18.11:
   version "0.18.12"
   resolved "https://registry.yarnpkg.com/react-native-web/-/react-native-web-0.18.12.tgz#d4bb3a783ece2514ba0508d7805b09c0a98f5a8e"
@@ -13824,30 +13884,15 @@ react-native-web@^0.18.11:
     postcss-value-parser "^4.2.0"
     styleq "^0.1.2"
 
-react-native-webview@11.26.0:
-  version "11.26.0"
-  resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-11.26.0.tgz#e524992876fe4a79e69905f0fab8949b470e9f16"
-  integrity sha512-4T4CKRm8xlaQDz9h/bCMPGAvtkesrhkRWqCX9FDJEzBToaVUIsV0ZOqtC4w/JSnCtFKKYiaC1ReJtCGv+4mFeQ==
-  dependencies:
-    escape-string-regexp "2.0.0"
-    invariant "2.2.4"
-
-react-native-youtube-iframe@^2.2.2:
-  version "2.2.2"
-  resolved "https://registry.yarnpkg.com/react-native-youtube-iframe/-/react-native-youtube-iframe-2.2.2.tgz#ade1a2e4ead3d539fbb80463f45b59ff1b510b55"
-  integrity sha512-og2KW21kCwAHKcnWoyWWBYC6J2Xtqjjwpghhoy9G6zfwZkr8Ej27BbQIAKM/TheJJUZ5/YUrqsgqAdnFYDx5TQ==
-  dependencies:
-    events "^3.2.0"
-
-react-native@0.71.3:
-  version "0.71.3"
-  resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.71.3.tgz#0faab799c49e61ba12df9e6525c3ac7d595d673c"
-  integrity sha512-RYJXCcQGa4NTfKiPgl92eRDUuQ6JGDnHqFEzRwJSqEx9lWvlvRRIebstJfurzPDKLQWQrvITR7aI7e09E25mLw==
+react-native@0.71.4:
+  version "0.71.4"
+  resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.71.4.tgz#f03f600efe68f745d19454ab17f9c1a9ef304790"
+  integrity sha512-3hSYqvWrOdKhpV3HpEKp1/CkWx8Sr/N/miCrmUIAsVTSJUR7JW0VvIsrV9urDhUj/s6v2WF4n7qIEEJsmTCrPw==
   dependencies:
     "@jest/create-cache-key-function" "^29.2.1"
-    "@react-native-community/cli" "10.1.3"
-    "@react-native-community/cli-platform-android" "10.1.3"
-    "@react-native-community/cli-platform-ios" "10.1.1"
+    "@react-native-community/cli" "10.2.0"
+    "@react-native-community/cli-platform-android" "10.2.0"
+    "@react-native-community/cli-platform-ios" "10.2.0"
     "@react-native/assets" "1.0.0"
     "@react-native/normalize-color" "2.1.0"
     "@react-native/polyfills" "2.0.0"
@@ -13860,16 +13905,16 @@ react-native@0.71.3:
     jest-environment-node "^29.2.1"
     jsc-android "^250231.0.0"
     memoize-one "^5.0.0"
-    metro-react-native-babel-transformer "0.73.7"
-    metro-runtime "0.73.7"
-    metro-source-map "0.73.7"
+    metro-react-native-babel-transformer "0.73.8"
+    metro-runtime "0.73.8"
+    metro-source-map "0.73.8"
     mkdirp "^0.5.1"
     nullthrows "^1.1.1"
     pretty-format "^26.5.2"
     promise "^8.3.0"
     react-devtools-core "^4.26.1"
     react-native-codegen "^0.71.5"
-    react-native-gradle-plugin "^0.71.15"
+    react-native-gradle-plugin "^0.71.16"
     react-refresh "^0.4.0"
     react-shallow-renderer "^16.15.0"
     regenerator-runtime "^0.13.2"
@@ -15771,6 +15816,25 @@ ts-interface-checker@^0.1.9:
   resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699"
   integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
 
+ts-node@^10.9.1:
+  version "10.9.1"
+  resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b"
+  integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==
+  dependencies:
+    "@cspotcode/source-map-support" "^0.8.0"
+    "@tsconfig/node10" "^1.0.7"
+    "@tsconfig/node12" "^1.0.7"
+    "@tsconfig/node14" "^1.0.0"
+    "@tsconfig/node16" "^1.0.2"
+    acorn "^8.4.1"
+    acorn-walk "^8.1.1"
+    arg "^4.1.0"
+    create-require "^1.1.0"
+    diff "^4.0.1"
+    make-error "^1.1.1"
+    v8-compile-cache-lib "^3.0.1"
+    yn "3.1.1"
+
 tsconfig-paths@^3.14.1:
   version "3.14.2"
   resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088"
@@ -16164,6 +16228,11 @@ uuid@^9.0.0:
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
   integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==
 
+v8-compile-cache-lib@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
+  integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
+
 v8-to-istanbul@^8.1.0:
   version "8.1.1"
   resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz#77b752fd3975e31bbcef938f85e9bd1c7a8d60ed"
@@ -16990,6 +17059,11 @@ yargs@^17.3.1, yargs@^17.5.1:
     y18n "^5.0.5"
     yargs-parser "^21.1.1"
 
+yn@3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
+  integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
+
 yocto-queue@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"