about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.detoxrc.js86
-rw-r--r--.eslintrc.js1
-rw-r--r--__e2e__/config.yml2
-rw-r--r--__e2e__/flows/composer-self-label.yml30
-rw-r--r--__e2e__/flows/composer.yml87
-rw-r--r--__e2e__/flows/create-account.yml37
-rw-r--r--__e2e__/flows/curate-lists.yml208
-rw-r--r--__e2e__/flows/home-screen.yml63
-rw-r--r--__e2e__/flows/login.yml26
-rw-r--r--__e2e__/flows/mod-lists.yml45
-rw-r--r--__e2e__/flows/profile-screen-edit.yml119
-rw-r--r--__e2e__/flows/profile-screen.yml37
-rw-r--r--__e2e__/flows/search-screen.yml22
-rw-r--r--__e2e__/flows/thread-muting.yml82
-rw-r--r--__e2e__/flows/thread-screen.yml84
-rw-r--r--__e2e__/jest.config.js12
-rw-r--r--__e2e__/perf-test.yml (renamed from __e2e__/maestro/scroll.yaml)2
-rw-r--r--__e2e__/setupApp.yml11
-rw-r--r--__e2e__/setupServer.js5
-rw-r--r--__e2e__/tests/composer.test.ts109
-rw-r--r--__e2e__/tests/create-account.test.ts39
-rw-r--r--__e2e__/tests/curate-lists.test.ts213
-rw-r--r--__e2e__/tests/home-screen.test.ts110
-rw-r--r--__e2e__/tests/invite-codes.test.skip.ts47
-rw-r--r--__e2e__/tests/login.test.ts23
-rw-r--r--__e2e__/tests/merge-feed.test.skip.ts163
-rw-r--r--__e2e__/tests/mod-lists.test.ts189
-rw-r--r--__e2e__/tests/profile-screen.test.ts196
-rw-r--r--__e2e__/tests/search-screen.test.ts25
-rw-r--r--__e2e__/tests/self-labeling.test.ts36
-rw-r--r--__e2e__/tests/shell.test.skip.ts33
-rw-r--r--__e2e__/tests/thread-muting.test.ts103
-rw-r--r--__e2e__/tests/thread-screen.test.ts131
-rw-r--r--__e2e__/util.ts141
-rw-r--r--docs/build.md9
-rw-r--r--docs/testing.md14
-rw-r--r--jest/test-pds.ts3
-rw-r--r--package.json9
-rw-r--r--src/view/com/testing/TestCtrls.e2e.tsx11
-rw-r--r--src/view/com/util/Toast.e2e.tsx1
-rw-r--r--yarn.lock48
41 files changed, 882 insertions, 1730 deletions
diff --git a/.detoxrc.js b/.detoxrc.js
deleted file mode 100644
index 906620430..000000000
--- a/.detoxrc.js
+++ /dev/null
@@ -1,86 +0,0 @@
-/** @type {Detox.DetoxConfig} */
-module.exports = {
-  testRunner: {
-    args: {
-      $0: 'jest',
-      config: '__e2e__/jest.config.js',
-    },
-    jest: {
-      setupTimeout: 120000,
-    },
-  },
-  apps: {
-    'ios.debug': {
-      type: 'ios.app',
-      binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/bluesky.app',
-      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/bluesky.app',
-      build:
-        'xcodebuild -workspace ios/Bluesky.xcworkspace -scheme Bluesky -configuration Release -sdk iphonesimulator -derivedDataPath ios/build',
-    },
-    'android.debug': {
-      type: 'android.apk',
-      binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
-      build:
-        'cd android ; ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug ; cd -',
-      reversePorts: [8081],
-    },
-    'android.release': {
-      type: 'android.apk',
-      binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
-      build:
-        'cd android ; ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release ; cd -',
-    },
-  },
-  devices: {
-    simulator: {
-      type: 'ios.simulator',
-      device: {
-        type: 'iPhone 15 Pro',
-      },
-    },
-    attached: {
-      type: 'android.attached',
-      device: {
-        adbName: '.*',
-      },
-    },
-    emulator: {
-      type: 'android.emulator',
-      device: {
-        avdName: 'Pixel_3a_API_30_x86',
-      },
-    },
-  },
-  configurations: {
-    'ios.sim.debug': {
-      device: 'simulator',
-      app: 'ios.debug',
-    },
-    'ios.sim.release': {
-      device: 'simulator',
-      app: 'ios.release',
-    },
-    'android.att.debug': {
-      device: 'attached',
-      app: 'android.debug',
-    },
-    'android.att.release': {
-      device: 'attached',
-      app: 'android.release',
-    },
-    'android.emu.debug': {
-      device: 'emulator',
-      app: 'android.debug',
-    },
-    'android.emu.release': {
-      device: 'emulator',
-      app: 'android.release',
-    },
-  },
-}
diff --git a/.eslintrc.js b/.eslintrc.js
index 29136d5dd..eb7ad04b1 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -9,7 +9,6 @@ module.exports = {
   parser: '@typescript-eslint/parser',
   plugins: [
     '@typescript-eslint',
-    'detox',
     'react',
     'lingui',
     'simple-import-sort',
diff --git a/__e2e__/config.yml b/__e2e__/config.yml
new file mode 100644
index 000000000..b36b0ef60
--- /dev/null
+++ b/__e2e__/config.yml
@@ -0,0 +1,2 @@
+flows:
+  - "flows/*"
\ No newline at end of file
diff --git a/__e2e__/flows/composer-self-label.yml b/__e2e__/flows/composer-self-label.yml
new file mode 100644
index 000000000..cc38b1d99
--- /dev/null
+++ b/__e2e__/flows/composer-self-label.yml
@@ -0,0 +1,30 @@
+appId: xyz.blueskyweb.app
+---
+- runScript:
+    file: ../setupServer.js
+    env:
+        SERVER_PATH: ?users
+- runFlow:
+    file: ../setupApp.yml
+- tapOn:
+    id: "e2eSignInAlice"
+
+# Post an image with the porn label
+- tapOn:
+    id: "composeFAB"
+- inputText: "Post with an image"
+- tapOn:
+    id: "openGalleryBtn"
+- tapOn:
+    id: "labelsBtn"
+- tapOn:
+    label: "Tap on porn"
+    point: 78%,67%
+- tapOn:
+    label: "Tap on confirm"
+    point: 51%,92%
+- tapOn:
+    id: "composerPublishBtn"
+- tapOn:
+    id: "e2eRefreshHome"
+- assertVisible: "Adult Content"
\ No newline at end of file
diff --git a/__e2e__/flows/composer.yml b/__e2e__/flows/composer.yml
new file mode 100644
index 000000000..f6d760ea5
--- /dev/null
+++ b/__e2e__/flows/composer.yml
@@ -0,0 +1,87 @@
+appId: xyz.blueskyweb.app
+---
+- runScript:
+    file: ../setupServer.js
+    env:
+        SERVER_PATH: ?users
+- runFlow:
+    file: ../setupApp.yml
+- tapOn:
+    id: "e2eSignInAlice"
+- tapOn:
+    id: "composeFAB"
+- inputText: "Post text only"
+- tapOn:
+    id: "composerPublishBtn"
+- assertVisible:
+    id: "composeFAB"
+- tapOn:
+    id: "composeFAB"
+- inputText: "Post with an image"
+- tapOn:
+    id: "openGalleryBtn"
+- tapOn:
+    id: "composerPublishBtn"
+- assertVisible:
+    id: "composeFAB"
+- tapOn:
+    id: "composeFAB"
+- inputText: "Post with a https://example.com link card"
+- tapOn:
+    id: "composerPublishBtn"
+- assertVisible:
+    id: "composeFAB"
+- tapOn:
+    id: "e2eRefreshHome"
+- tapOn:
+    id: "replyBtn"
+- inputText: "Reply text only"
+- tapOn:
+    id: "composerPublishBtn"
+- assertVisible:
+    id: "composeFAB"
+- tapOn:
+    id: "replyBtn"
+- inputText: "Reply with an image"
+- tapOn:
+    id: "openGalleryBtn"
+- tapOn:
+    id: "composerPublishBtn"
+- assertVisible:
+    id: "composeFAB"
+- tapOn:
+    id: "replyBtn"
+- inputText: "Reply with a https://example.com link card"
+- tapOn:
+    id: "composerPublishBtn"
+- assertVisible:
+    id: "composeFAB"
+- tapOn:
+    id: "repostBtn"
+- tapOn:
+    id: "quoteBtn"
+- inputText: "QP text only"   
+- tapOn:
+    id: "composerPublishBtn"
+- assertVisible:
+    id: "composeFAB"
+- tapOn:
+    id: "repostBtn"
+- tapOn:
+    id: "quoteBtn"
+- inputText: "QP with an image"
+- tapOn:
+    id: "openGalleryBtn"
+- tapOn:
+    id: "composerPublishBtn"
+- assertVisible:
+    id: "composeFAB"
+- tapOn:
+    id: "repostBtn"
+- tapOn:
+    id: "quoteBtn"
+- inputText: "QP with a https://example.com link card"
+- tapOn:
+    id: "composerPublishBtn"
+- assertVisible:
+    id: "composeFAB"
diff --git a/__e2e__/flows/create-account.yml b/__e2e__/flows/create-account.yml
new file mode 100644
index 000000000..99ac1371a
--- /dev/null
+++ b/__e2e__/flows/create-account.yml
@@ -0,0 +1,37 @@
+appId: xyz.blueskyweb.app
+---
+- runScript:
+    file: ../setupServer.js
+    env:
+        SERVER_PATH: ""
+- runFlow:
+    file: ../setupApp.yml
+- tapOn:
+    id: "e2eOpenLoggedOutView"
+- tapOn:
+    id: "createAccountButton"
+- tapOn:
+    id: "selectServiceButton"
+- tapOn:
+    id: "customSelectBtn"
+- tapOn:
+    id: "customServerTextInput"
+- inputText: "http://localhost:3000"
+- pressKey: Enter
+- tapOn:
+    id: "doneBtn"
+- tapOn:
+    id: "emailInput"
+- inputText: "example@test.com"
+- tapOn:
+    id: "passwordInput"
+- inputText: "hunter2"
+- pressKey: Enter
+- tapOn:
+    id: "nextBtn"
+- tapOn:
+     id: "handleInput"
+- inputText: "e2e-test"
+- tapOn:
+    id: "nextBtn"
+
diff --git a/__e2e__/flows/curate-lists.yml b/__e2e__/flows/curate-lists.yml
new file mode 100644
index 000000000..35f4f800d
--- /dev/null
+++ b/__e2e__/flows/curate-lists.yml
@@ -0,0 +1,208 @@
+appId: xyz.blueskyweb.app
+---
+- runScript:
+    file: ../setupServer.js
+    env:
+        SERVER_PATH: "?users&follows&posts"
+- runFlow:
+    file: ../setupApp.yml
+- tapOn:
+    id: "e2eSignInAlice"
+
+- tapOn:
+    label: "Create a curate list"
+    id: "e2eGotoLists"
+- tapOn:
+    id: "newUserListBtn"
+- assertVisible:
+    id: "createOrEditListModal"
+- tapOn:
+    id: "editNameInput"
+- inputText: "Good Ppl"
+- tapOn:
+    id: "editDescriptionInput"
+- inputText: "They good"
+- tapOn: "Save"
+- tapOn: "Save"
+- assertNotVisible:
+    id: "createOrEditListModal"
+- tapOn: "About"
+- assertVisible: "Good Ppl"
+- assertVisible: "They good"
+
+- tapOn:
+    label: "Edit display name and description via the edit curatelist modal"
+    point: "90%,9%"
+- tapOn: "Edit list details"
+- assertVisible:
+    id: "createOrEditListModal"
+- tapOn:
+    id: "editNameInput"
+- eraseText
+- inputText: "Bad Ppl"
+- hideKeyboard
+- tapOn:
+    id: "editDescriptionInput"
+- eraseText
+- inputText: "They bad"
+- tapOn: "Save"
+- tapOn: "Save"
+- assertNotVisible:
+    id: "createOrEditListModal"
+- assertVisible: Bad Ppl
+- assertVisible: They bad
+
+- tapOn:
+    label: "Remove description via the edit curatelist modal"
+    point: "90%,9%"
+- tapOn: "Edit list details"
+- assertVisible:
+    id: "createOrEditListModal"
+- tapOn:
+    id: "editDescriptionInput"
+- eraseText
+- tapOn: "Save"
+- tapOn: "Save"
+- assertNotVisible:
+    id: "createOrEditListModal"
+- assertNotVisible:
+    id: "listDescription"
+
+- tapOn:
+    label: "Delete the curatelist"
+    point: "90%,9%"
+- tapOn: "Delete List"
+- tapOn:
+    id: "confirmBtn"
+- assertVisible:
+    id: "listsEmpty"
+
+- tapOn:
+    label: "Create a new curatelist"
+    id: "e2eGotoLists"
+- tapOn:
+    id: "newUserListBtn"
+- assertVisible:
+    id: "createOrEditListModal"
+- tapOn:
+    id: "editNameInput"
+- inputText: "Good Ppl"
+- tapOn:
+    id: "editDescriptionInput"
+- inputText: "They good"
+- tapOn: "Save"
+- tapOn: "Save"
+- assertNotVisible:
+    id: "createOrEditListModal"
+- tapOn: "About"
+- assertVisible: "Good Ppl"
+- assertVisible: "They good"
+- tapOn: "About"
+
+- tapOn:
+    label: "Adds users on curatelists from the list"
+    id: "addUserBtn"
+- assertVisible:
+    id: "listAddUserModal"
+- tapOn:
+    id: "searchInput"
+- inputText: "b"
+- pressKey: Enter
+- tapOn:
+    id: "user-bob.test-addBtn"
+- tapOn:
+    id: "doneBtn"
+- assertNotVisible:
+    id: "listAddUserModal"
+- assertVisible:
+    id: "user-bob.test"
+
+- tapOn: "Posts"
+- assertVisible:
+    label: "Shows posts by the users in the list"
+    id: "feedItem-by-bob.test"
+
+- tapOn:
+    label: "Pins the list"
+    id: "pinBtn"
+- tapOn:
+    id: "e2eGotoHome"
+- tapOn: "Good Ppl"
+- assertVisible:
+    id: "feedItem-by-bob.test"
+- tapOn:
+    id: "bottomBarFeedsBtn"
+- tapOn:
+    id: "saved-feed-Good Ppl"
+- assertVisible:
+    id: "feedItem-by-bob.test"
+- tapOn:
+    id: "unpinBtn"
+- tapOn:
+    id: "bottomBarHomeBtn"
+- assertNotVisible:
+    id: "homeScreenFeedTabs-Good Ppl"
+- tapOn:
+    id: "e2eGotoLists"
+- tapOn:
+    id: "list-Good Ppl"
+
+- tapOn: "About"
+- assertVisible:
+    label: "Removes users on curatelists from the list"
+    id: "user-bob.test"
+- tapOn:
+    point: "90%,43%"
+- assertVisible:
+    id: "userAddRemoveListsModal"
+- tapOn:
+    id: "user-bob.test-addBtn"
+- tapOn:
+    id: "doneBtn"
+- assertNotVisible:
+    id: "userAddRemoveListsModal"
+
+- tapOn:
+    label: "Shows the curatelist on my profile"
+    id: "bottomBarProfileBtn"
+- swipe:
+    from:
+        id: "profilePager-selector"
+    direction: LEFT
+- tapOn:
+    id: "profilePager-selector-5"
+- tapOn:
+    id: "list-Good Ppl"
+
+- tapOn:
+    label: "Adds and removes users on curatelists from the profile"
+    id: "bottomBarSearchBtn"
+- tapOn:
+    id: "searchTextInput"
+- inputText: "bob"
+- tapOn:
+    id: "searchAutoCompleteResult-bob.test"
+- assertVisible:
+    id: "profileView"
+- tapOn:
+    id: "profileHeaderDropdownBtn"
+- tapOn: "Add to Lists"
+- assertVisible:
+    id: "userAddRemoveListsModal"
+- tapOn:
+    id: "user-bob.test-addBtn"
+- tapOn:
+    id: "doneBtn"
+- assertNotVisible:
+    id: "userAddRemoveListsModal"
+- tapOn:
+    id: "profileHeaderDropdownBtn"
+- tapOn: "Add to Lists"
+- assertVisible:
+    id: "userAddRemoveListsModal"
+- tapOn:
+    id: "user-bob.test-addBtn"
+- tapOn:
+    id: "doneBtn"
+- assertNotVisible:
+    id: "userAddRemoveListsModal"
diff --git a/__e2e__/flows/home-screen.yml b/__e2e__/flows/home-screen.yml
new file mode 100644
index 000000000..69a1fe37f
--- /dev/null
+++ b/__e2e__/flows/home-screen.yml
@@ -0,0 +1,63 @@
+appId: xyz.blueskyweb.app
+---
+- runScript:
+    file: ../setupServer.js
+    env:
+        SERVER_PATH: ?users&follows&posts&feeds
+- runFlow:
+    file: ../setupApp.yml
+- tapOn:
+    id: "e2eSignInAlice"
+
+- tapOn:
+    label: "Can go to feeds page using feeds button in tab bar"
+    text: "Feeds ✨"
+- assertVisible: "Discover New Feeds"
+
+- tapOn:
+    label: "Feeds button disappears after pinning a feed"
+    id: "bottomBarProfileBtn"
+- swipe:
+    from:
+        id: "profilePager-selector"
+    direction: LEFT
+- tapOn:
+    id: "profilePager-selector-4"
+- tapOn:
+    id: "feed-alice-favs"
+- tapOn: "Pin to Home"
+- tapOn:
+    id: "bottomBarHomeBtn"
+- assertNotVisible: "Feeds ✨"
+
+- tapOn:
+    label: "Can like posts"
+    id: "likeBtn"
+- assertVisible:
+    id: "likeCount"
+    text: "1"
+- tapOn:
+    id: "likeBtn"
+- assertNotVisible:
+    id: "likeCount"
+
+- tapOn:
+    label: "Can repost posts"
+    id: "repostBtn"
+- tapOn: "Repost"
+- assertVisible:
+    id: "repostCount"
+    text: "1"
+- tapOn:
+    id: "repostBtn"
+- tapOn: "Undo repost"
+- assertNotVisible:
+    id: "repostCount"
+
+- tapOn:
+    label: "Can delete posts"
+    id: "postDropdownBtn"
+    childOf:
+        id: "feedItem-by-alice.test"
+- tapOn: "Delete post"
+- tapOn: "Delete"
diff --git a/__e2e__/flows/login.yml b/__e2e__/flows/login.yml
new file mode 100644
index 000000000..f1001f78d
--- /dev/null
+++ b/__e2e__/flows/login.yml
@@ -0,0 +1,26 @@
+appId: xyz.blueskyweb.app
+---
+- runScript:
+    file: ../setupServer.js
+    env:
+        SERVER_PATH: "?users"
+- runFlow:
+    file: ../setupApp.yml
+- tapOn:
+    id: "e2eOpenLoggedOutView"
+- tapOn: "Sign in"
+- tapOn: 
+    id: "selectServiceButton"
+- tapOn: "Custom"
+- tapOn:
+    id: "customServerTextInput"
+- inputText: "http://localhost:3000"
+- tapOn: "Done"
+- tapOn:
+    id: "loginUsernameInput"
+- inputText: "Alice"
+- tapOn:
+    id: "loginPasswordInput"
+- inputText: "hunter2"
+- pressKey: Enter
+- assertVisible: "Following"
\ No newline at end of file
diff --git a/__e2e__/flows/mod-lists.yml b/__e2e__/flows/mod-lists.yml
new file mode 100644
index 000000000..75ee100a8
--- /dev/null
+++ b/__e2e__/flows/mod-lists.yml
@@ -0,0 +1,45 @@
+appId: xyz.blueskyweb.app
+---
+- runScript:
+    file: ../setupServer.js
+    env:
+        SERVER_PATH: "?users&follows&labels"
+- runFlow:
+    file: ../setupApp.yml
+- tapOn:
+    id: "e2eSignInAlice"
+
+# create a modlist
+- tapOn:
+    id: "e2eGotoModeration"
+- tapOn:
+    id: "moderationlistsBtn"
+- tapOn: "New"
+- tapOn:
+    id: "editNameInput"
+- inputText: "Muted Users"
+- tapOn:
+    id: "editDescriptionInput"
+- inputText: "Shhh"
+- tapOn: "Save"
+- tapOn: "Save"
+
+# view modlist
+- assertVisible: "Muted Users"
+- assertVisible: "Shhh"
+
+# toggle mute subscription
+- tapOn:
+    point: "70%,9%"
+- tapOn: "Mute accounts"
+- tapOn: "Mute list"
+- tapOn: "Unmute"
+
+# toggle block subscription
+- tapOn:
+    point: "70%,9%"
+- tapOn: "Block accounts"
+- tapOn: "Block list"
+- tapOn: "Unblock"
+ 
+ # the rest of the behaviors are tested in curate-lists.yml
\ No newline at end of file
diff --git a/__e2e__/flows/profile-screen-edit.yml b/__e2e__/flows/profile-screen-edit.yml
new file mode 100644
index 000000000..602cc6688
--- /dev/null
+++ b/__e2e__/flows/profile-screen-edit.yml
@@ -0,0 +1,119 @@
+appId: xyz.blueskyweb.app
+---
+- runScript:
+    file: ../setupServer.js
+    env:
+        SERVER_PATH: "?users&posts&feeds"
+- runFlow:
+    file: ../setupApp.yml
+- tapOn:
+    id: "e2eSignInAlice"
+
+
+# Navigate to my profile
+- tapOn:
+    id: "bottomBarProfileBtn"
+
+# Can see feeds
+- swipe:
+    from:
+        id: "profilePager-selector"
+    direction: LEFT
+- tapOn:
+    id: "profilePager-selector-4"
+- assertVisible: 
+    id: "feed-alice-favs"
+- swipe:
+    from:
+        id: "profilePager-selector"
+    direction: RIGHT
+- tapOn:
+    id: "profilePager-selector-0"
+
+# Open and close edit profile modal
+- tapOn:
+    id: "profileHeaderEditProfileButton"
+- assertVisible:
+    id: "editProfileModal"
+- tapOn:
+    id: "editProfileCancelBtn"
+- assertNotVisible:
+    id: "editProfileModal"
+
+# Edit display name and description via the edit profile modal
+- tapOn:
+    id: "profileHeaderEditProfileButton"
+- assertVisible:
+    id: "editProfileModal"
+- tapOn:
+    id: "editProfileDisplayNameInput"
+- eraseText
+- inputText: "Alicia"
+- tapOn:
+    id: "editProfileDescriptionInput"
+- eraseText
+- inputText: "One cool hacker"
+- tapOn: "Description"
+- tapOn:
+    id: "editProfileSaveBtn"
+- assertNotVisible:
+    id: "editProfileModal"
+- assertVisible: "Alicia"
+- assertVisible: "One cool hacker"
+
+# Remove display name and description via the edit profile modal
+- tapOn:
+    id: "profileHeaderEditProfileButton"
+- assertVisible:
+    id: "editProfileModal"
+- tapOn:
+    id: "editProfileDisplayNameInput"
+- eraseText
+- tapOn:
+    id: "editProfileDescriptionInput"
+- eraseText
+- tapOn: "Description"
+- tapOn:
+    id: "editProfileSaveBtn"
+- assertNotVisible:
+    id: "editProfileModal"
+- assertVisible: "alice.test"
+- assertNotVisible: "One cool hacker"
+
+# Set avi and banner via the edit profile modal
+- assertVisible:
+    id: "userBannerFallback"
+- tapOn:
+    id: "profileHeaderEditProfileButton"
+- assertVisible:
+    id: "editProfileModal"
+- tapOn:
+    id: "changeBannerBtn"
+- tapOn: "Upload from Library"
+- tapOn:
+    id: "changeAvatarBtn"
+- tapOn: "Upload from Library"
+- tapOn:
+    id: "editProfileSaveBtn"
+- assertNotVisible:
+    id: "editProfileModal"
+- assertVisible:
+    id: "userBannerImage"
+
+# # Remove avi and banner via the edit profile modal
+- tapOn:
+    id: "profileHeaderEditProfileButton"
+- assertVisible:
+    id: "editProfileModal"
+- tapOn:
+    id: "changeBannerBtn"
+- tapOn: "Remove Banner"
+- tapOn:
+    id: "changeAvatarBtn"
+- tapOn: "Remove Avatar"
+- tapOn:
+    id: "editProfileSaveBtn"
+- assertNotVisible:
+    id: "editProfileModal"
+- assertVisible:
+    id: "userBannerFallback"
diff --git a/__e2e__/flows/profile-screen.yml b/__e2e__/flows/profile-screen.yml
new file mode 100644
index 000000000..7d2d43dee
--- /dev/null
+++ b/__e2e__/flows/profile-screen.yml
@@ -0,0 +1,37 @@
+appId: xyz.blueskyweb.app
+---
+- runScript:
+    file: ../setupServer.js
+    env:
+        SERVER_PATH: "?users&posts&feeds"
+- runFlow:
+    file: ../setupApp.yml
+- tapOn:
+    id: "e2eSignInAlice"
+
+# Navigate to another user profile
+- tapOn:
+    id: "bottomBarSearchBtn"
+- tapOn:
+    id: "searchTextInput"
+- inputText: "b"
+- tapOn:
+    id: "searchAutoCompleteResult-bob.test"
+- assertVisible:
+    id: "profileView"
+
+# Can follow/unfollow another user
+- tapOn:
+    id: "followBtn"
+- tapOn:
+    id: "unfollowBtn"
+
+# Can mute/unmute another user
+- tapOn:
+    id: "profileHeaderDropdownBtn"
+- tapOn: "Mute Account"
+- assertVisible: "Account Muted"
+- tapOn:
+    id: "profileHeaderDropdownBtn"
+- tapOn: "Unmute Account"
+- assertNotVisible: "Account Muted"
\ No newline at end of file
diff --git a/__e2e__/flows/search-screen.yml b/__e2e__/flows/search-screen.yml
new file mode 100644
index 000000000..0d31d03fb
--- /dev/null
+++ b/__e2e__/flows/search-screen.yml
@@ -0,0 +1,22 @@
+appId: xyz.blueskyweb.app
+---
+- runScript:
+    file: ../setupServer.js
+    env:
+        SERVER_PATH: "?users"
+- runFlow:
+    file: ../setupApp.yml
+- tapOn:
+    id: "e2eSignInAlice"
+
+# Navigate to another user profile via autocomplete
+- tapOn:
+    id: "bottomBarSearchBtn"
+- tapOn:
+    id: "searchTextInput"
+- inputText: "b"
+- tapOn:
+    id: "searchAutoCompleteResult-bob.test"
+- assertVisible:
+    id: "profileView"
+
diff --git a/__e2e__/flows/thread-muting.yml b/__e2e__/flows/thread-muting.yml
new file mode 100644
index 000000000..316389a79
--- /dev/null
+++ b/__e2e__/flows/thread-muting.yml
@@ -0,0 +1,82 @@
+appId: xyz.blueskyweb.app
+---
+- runScript:
+    file: ../setupServer.js
+    env:
+        SERVER_PATH: "?users&follows"
+- runFlow:
+    file: ../setupApp.yml
+
+
+# Login, create a thread, and log out
+- tapOn:
+    id: "e2eSignInAlice"
+- tapOn:
+    id: "composeFAB"
+- inputText: "Test thread"
+- tapOn:
+    id: "composerPublishBtn"
+
+# Login, reply to the thread, and log out
+- tapOn:
+    id: "e2eSignInBob"
+- tapOn:
+    id: "replyBtn"
+- inputText: "Reply 1"
+- tapOn:
+    id: "composerPublishBtn"
+
+# Login, confirm notification exists, mute thread, and log out
+- tapOn:
+    id: "e2eSignInAlice"
+- tapOn:
+    id: "bottomBarNotificationsBtn"
+- assertVisible:
+    id: "feedItem-by-bob.test"
+- tapOn:
+    id: "feedItem-by-bob.test"
+- tapOn:
+    id: "postDropdownBtn"
+    childOf:
+        id: "postThreadItem-by-bob.test"
+- tapOn: "Mute thread"
+
+# Login, reply to the thread twice, and log out
+- tapOn:
+    id: "e2eSignInBob"
+- tapOn:
+    id: "bottomBarProfileBtn"
+- tapOn:
+    id: "profilePager-selector-1"
+- tapOn:
+    id: "replyBtn"
+- inputText: "Reply 2"
+- tapOn:
+    id: "composerPublishBtn"
+- tapOn:
+    id: "replyBtn"
+- inputText: "Reply 3"
+- tapOn:
+    id: "composerPublishBtn"
+
+
+# Login, confirm notifications dont exist, unmute the thread, confirm notifications exist
+- tapOn:
+    id: "e2eSignInAlice"
+- tapOn:
+    id: "bottomBarNotificationsBtn"
+- assertNotVisible:
+    id: "feedItem-by-bob.test"
+- tapOn:
+    id: "bottomBarHomeBtn"
+- tapOn:
+    id: "postDropdownBtn"
+- tapOn: "Unmute thread"
+- tapOn:
+    id: "bottomBarNotificationsBtn"
+- swipe:
+    from:
+        id: "notifsFeed"
+    direction: DOWN
+- assertVisible:
+    id: "feedItem-by-bob.test"
diff --git a/__e2e__/flows/thread-screen.yml b/__e2e__/flows/thread-screen.yml
new file mode 100644
index 000000000..22f71345d
--- /dev/null
+++ b/__e2e__/flows/thread-screen.yml
@@ -0,0 +1,84 @@
+appId: xyz.blueskyweb.app
+---
+- runScript:
+    file: ../setupServer.js
+    env:
+        SERVER_PATH: "?users&follows&thread"
+- runFlow:
+    file: ../setupApp.yml
+- tapOn:
+    id: "e2eSignInAlice"
+
+
+# Navigate to thread
+- tapOn: "Thread root"
+- assertVisible: "Thread reply"
+
+# Can like the root post
+- tapOn:
+    id: "likeBtn"
+    childOf:
+        id: "postThreadItem-by-bob.test"
+- assertVisible:
+    id: "likeCount-expanded"
+- tapOn:
+    id: "likeBtn"
+    childOf:
+        id: "postThreadItem-by-bob.test"
+- assertNotVisible:
+    id: "likeCount-expanded"
+
+# Can like a reply post
+- tapOn:
+    id: "likeBtn"
+    childOf:
+        id: "postThreadItem-by-carla.test"
+- assertVisible:
+    id: "likeCount"
+    childOf:
+        id: "postThreadItem-by-carla.test"
+- tapOn:
+    id: "likeBtn"
+    childOf:
+        id: "postThreadItem-by-carla.test"
+- assertNotVisible:
+    id: "likeCount"
+    childOf:
+        id: "postThreadItem-by-carla.test"
+
+# Can repost the root post
+- tapOn:
+    id: "repostBtn"
+    childOf:
+        id: "postThreadItem-by-bob.test"
+- tapOn: "Repost"
+- assertVisible:
+    id: "repostCount-expanded"
+- tapOn:
+    id: "repostBtn"
+    childOf:
+        id: "postThreadItem-by-bob.test"
+- tapOn: "Undo repost"
+- assertNotVisible:
+    id: "repostCount-expanded"
+
+
+# Can repost a reply post
+- tapOn:
+    id: "repostBtn"
+    childOf:
+        id: "postThreadItem-by-carla.test"
+- tapOn: "Repost"
+- assertVisible:
+    id: "repostCount"
+    childOf:
+        id: "postThreadItem-by-carla.test"
+- tapOn:
+    id: "repostBtn"
+    childOf:
+        id: "postThreadItem-by-carla.test"
+- tapOn: "Undo repost"
+- assertNotVisible:
+    id: "repostCount"
+    childOf:
+        id: "postThreadItem-by-carla.test"
diff --git a/__e2e__/jest.config.js b/__e2e__/jest.config.js
deleted file mode 100644
index 80c2ad5b3..000000000
--- a/__e2e__/jest.config.js
+++ /dev/null
@@ -1,12 +0,0 @@
-/** @type {import('@jest/types').Config.InitialOptions} */
-module.exports = {
-  rootDir: '..',
-  testMatch: ['<rootDir>/__e2e__/**/*.test.ts'],
-  testTimeout: 120000,
-  maxWorkers: 1,
-  globalSetup: 'detox/runners/jest/globalSetup',
-  globalTeardown: 'detox/runners/jest/globalTeardown',
-  reporters: ['detox/runners/jest/reporter'],
-  testEnvironment: 'detox/runners/jest/testEnvironment',
-  verbose: true,
-}
diff --git a/__e2e__/maestro/scroll.yaml b/__e2e__/perf-test.yml
index 2d32793eb..7a7b7a18c 100644
--- a/__e2e__/maestro/scroll.yaml
+++ b/__e2e__/perf-test.yml
@@ -1,3 +1,4 @@
+
 # flow.yaml
 
 appId: xyz.blueskyweb.app
@@ -74,4 +75,3 @@ appId: xyz.blueskyweb.app
 - "scroll"
 - "scroll"
 - "scroll"
-
diff --git a/__e2e__/setupApp.yml b/__e2e__/setupApp.yml
new file mode 100644
index 000000000..8c3ffd2d3
--- /dev/null
+++ b/__e2e__/setupApp.yml
@@ -0,0 +1,11 @@
+appId: xyz.blueskyweb.app
+---
+- launchApp:
+   appId: "xyz.blueskyweb.app"
+   clearState: true
+- waitForAnimationToEnd
+- tapOn: "http://localhost:8081"
+- waitForAnimationToEnd
+- swipe:
+   from: "Bluesky"
+   direction: DOWN
diff --git a/__e2e__/setupServer.js b/__e2e__/setupServer.js
new file mode 100644
index 000000000..7b1fb9574
--- /dev/null
+++ b/__e2e__/setupServer.js
@@ -0,0 +1,5 @@
+// eslint-disable-next-line
+http.post('http://localhost:1986/' + SERVER_PATH, {
+  headers: {'Content-Type': 'text/plain'},
+  body: '',
+})
diff --git a/__e2e__/tests/composer.test.ts b/__e2e__/tests/composer.test.ts
deleted file mode 100644
index 06781410f..000000000
--- a/__e2e__/tests/composer.test.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-/* eslint-env detox/detox */
-
-import {beforeAll, describe, it} from '@jest/globals'
-import {expect} from 'detox'
-
-import {createServer, loginAsAlice, openApp, sleep} from '../util'
-
-describe('Composer', () => {
-  beforeAll(async () => {
-    await createServer('?users')
-    await openApp({
-      permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
-    })
-  })
-
-  it('Login', async () => {
-    await loginAsAlice()
-    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('composerPublishBtn')).tap()
-    await expect(element(by.id('composeFAB'))).toBeVisible()
-  })
-
-  it('Reply text only', async () => {
-    await element(by.id('e2eRefreshHome')).tap()
-
-    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('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('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
deleted file mode 100644
index 9c56c914e..000000000
--- a/__e2e__/tests/create-account.test.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-/* eslint-env detox/detox */
-
-import {describe, beforeAll, it} from '@jest/globals'
-import {expect} from 'detox'
-import {openApp, createServer} from '../util'
-
-describe('Create account', () => {
-  let service: string
-  beforeAll(async () => {
-    service = await createServer('')
-    await openApp({permissions: {notifications: 'YES'}})
-  })
-
-  it('I can create a new account', async () => {
-    await element(by.id('e2eOpenLoggedOutView')).tap()
-
-    await element(by.id('createAccountButton')).tap()
-    await device.takeScreenshot('1- opened create account screen')
-    await element(by.id('selectServiceButton')).tap()
-    await device.takeScreenshot('2- selected other server')
-    await element(by.id('customSelectBtn')).tap()
-    await element(by.id('customServerTextInput')).typeText(service)
-    await element(by.id('customServerTextInput')).tapReturnKey()
-    await element(by.id('doneBtn')).tap()
-    await device.takeScreenshot('3- input test server URL')
-    await element(by.id('emailInput')).typeText('example@test.com')
-    await element(by.id('passwordInput')).typeText('hunter2')
-    await device.takeScreenshot('4- entered account details')
-
-    await element(by.id('nextBtn')).tap()
-
-    await element(by.id('handleInput')).typeText('e2e-test')
-    await device.takeScreenshot('5- entered handle')
-
-    await element(by.id('nextBtn')).tap()
-
-    await expect(element(by.id('onboardingInterests'))).toBeVisible()
-  })
-})
diff --git a/__e2e__/tests/curate-lists.test.ts b/__e2e__/tests/curate-lists.test.ts
deleted file mode 100644
index 635357b8d..000000000
--- a/__e2e__/tests/curate-lists.test.ts
+++ /dev/null
@@ -1,213 +0,0 @@
-/* eslint-env detox/detox */
-
-import {beforeAll, describe, it} from '@jest/globals'
-import {expect} from 'detox'
-
-import {createServer, loginAsAlice, loginAsBob, openApp, sleep} from '../util'
-
-describe('Curate lists', () => {
-  beforeAll(async () => {
-    await createServer('?users&follows&posts')
-    await openApp({
-      permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
-    })
-  })
-
-  it('Login and create a curatelists', async () => {
-    await loginAsAlice()
-    await element(by.id('e2eGotoLists')).tap()
-    await element(by.id('newUserListBtn')).tap()
-    await expect(element(by.id('createOrEditListModal'))).toBeVisible()
-    await element(by.id('editNameInput')).typeText('Good Ppl')
-    await element(by.id('editDescriptionInput')).typeText('They good')
-    await element(by.id('saveBtn')).tap()
-    await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
-    await element(by.text('About')).tap()
-    await expect(element(by.id('headerTitle'))).toHaveText('Good Ppl')
-    await expect(element(by.id('listDescription'))).toHaveText('They good')
-  })
-
-  it('Edit display name and description via the edit curatelist modal', async () => {
-    await element(by.id('headerDropdownBtn')).tap()
-    await element(by.text('Edit list details')).tap()
-    await expect(element(by.id('createOrEditListModal'))).toBeVisible()
-    await element(by.id('editNameInput')).clearText()
-    await element(by.id('editNameInput')).typeText('Bad Ppl')
-    await element(by.id('editDescriptionInput')).clearText()
-    await element(by.id('editDescriptionInput')).typeText('They bad')
-    await element(by.id('saveBtn')).tap()
-    await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
-    await expect(element(by.id('headerTitle'))).toHaveText('Bad Ppl')
-    await expect(element(by.id('listDescription'))).toHaveText('They bad')
-    // have to wait for the toast to clear
-    await waitFor(element(by.id('headerDropdownBtn')))
-      .toBeVisible()
-      .withTimeout(5000)
-  })
-
-  it('Remove description via the edit curatelist modal', async () => {
-    await element(by.id('headerDropdownBtn')).tap()
-    await element(by.text('Edit list details')).tap()
-    await expect(element(by.id('createOrEditListModal'))).toBeVisible()
-    await element(by.id('editDescriptionInput')).clearText()
-    await element(by.id('saveBtn')).tap()
-    await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
-    await expect(element(by.id('listDescription'))).not.toBeVisible()
-    // have to wait for the toast to clear
-    await waitFor(element(by.id('headerDropdownBtn')))
-      .toBeVisible()
-      .withTimeout(5000)
-  })
-
-  it('Set avi via the edit curatelist modal', async () => {
-    await expect(element(by.id('userAvatarFallback'))).toExist()
-    await element(by.id('headerDropdownBtn')).tap()
-    await element(by.text('Edit list details')).tap()
-    await expect(element(by.id('createOrEditListModal'))).toBeVisible()
-    await element(by.id('changeAvatarBtn')).tap()
-    await element(by.text('Upload from Library')).tap()
-    await sleep(3e3)
-    await element(by.id('saveBtn')).tap()
-    await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
-    await expect(element(by.id('userAvatarImage'))).toExist()
-    // have to wait for the toast to clear
-    await waitFor(element(by.id('headerDropdownBtn')))
-      .toBeVisible()
-      .withTimeout(5000)
-  })
-
-  it('Remove avi via the edit curatelist modal', async () => {
-    await expect(element(by.id('userAvatarImage'))).toExist()
-    await element(by.id('headerDropdownBtn')).tap()
-    await element(by.text('Edit list details')).tap()
-    await expect(element(by.id('createOrEditListModal'))).toBeVisible()
-    await element(by.id('changeAvatarBtn')).tap()
-    await element(by.text('Remove Avatar')).tap()
-    await element(by.id('saveBtn')).tap()
-    await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
-    await expect(element(by.id('userAvatarFallback'))).toExist()
-    // have to wait for the toast to clear
-    await waitFor(element(by.id('headerDropdownBtn')))
-      .toBeVisible()
-      .withTimeout(5000)
-  })
-
-  it('Delete the curatelist', async () => {
-    await element(by.id('headerDropdownBtn')).tap()
-    await element(by.text('Delete List')).tap()
-    await element(by.id('confirmBtn')).tap()
-    await expect(element(by.id('listsEmpty'))).toBeVisible()
-  })
-
-  it('Create a new curatelist', async () => {
-    await element(by.id('e2eGotoLists')).tap()
-    await element(by.id('newUserListBtn')).tap()
-    await expect(element(by.id('createOrEditListModal'))).toBeVisible()
-    await element(by.id('editNameInput')).typeText('Good Ppl')
-    await element(by.id('editDescriptionInput')).typeText('They good')
-    await element(by.id('saveBtn')).tap()
-    await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
-    await element(by.text('About')).tap()
-    await expect(element(by.id('headerTitle'))).toHaveText('Good Ppl')
-    await expect(element(by.id('listDescription'))).toHaveText('They good')
-  })
-
-  it('Adds users on curatelists from the list', async () => {
-    await element(by.text('About')).tap()
-    await element(by.id('addUserBtn')).tap()
-    await expect(element(by.id('listAddUserModal'))).toBeVisible()
-    await element(by.id('searchInput')).typeText('b')
-    await waitFor(element(by.id('user-bob.test-addBtn')))
-      .toBeVisible()
-      .withTimeout(5000)
-    await element(by.id('user-bob.test-addBtn')).tap()
-    await element(by.id('doneBtn')).tap()
-    await expect(element(by.id('listAddUserModal'))).not.toBeVisible()
-    await expect(element(by.id('user-bob.test'))).toBeVisible()
-  })
-
-  it('Shows posts by the users in the list', async () => {
-    await element(by.text('Posts')).tap()
-    await expect(element(by.id('feedItem-by-bob.test'))).toBeVisible()
-  })
-
-  it('Pins the list', async () => {
-    await expect(element(by.id('pinBtn'))).toBeVisible()
-    await element(by.id('pinBtn')).tap()
-    await element(by.id('e2eGotoHome')).tap()
-    await element(by.id('homeScreenFeedTabs-Good Ppl')).tap()
-    await expect(element(by.id('feedItem-by-bob.test'))).toBeVisible()
-
-    await element(by.id('bottomBarFeedsBtn')).tap()
-    await element(by.id('saved-feed-Good Ppl')).tap()
-    await expect(element(by.id('feedItem-by-bob.test'))).toBeVisible()
-
-    await element(by.id('unpinBtn')).tap()
-    await element(by.id('bottomBarHomeBtn')).tap()
-    await expect(
-      element(by.id('homeScreenFeedTabs-Good Ppl')),
-    ).not.toBeVisible()
-
-    await element(by.id('e2eGotoLists')).tap()
-    await element(by.id('list-Good Ppl')).tap()
-  })
-
-  it('Removes users on curatelists from the list', async () => {
-    await element(by.text('About')).tap()
-    await expect(element(by.id('user-bob.test'))).toBeVisible()
-    await element(by.id('user-bob.test-editBtn')).tap()
-    await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
-    await element(by.id('user-bob.test-addBtn')).tap()
-    await element(by.id('doneBtn')).tap()
-    await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
-  })
-
-  it('Shows the curatelist on my profile', async () => {
-    await element(by.id('bottomBarProfileBtn')).tap()
-    await element(by.id('profilePager-selector')).swipe('left')
-    await element(by.id('profilePager-selector-5')).tap()
-    await element(by.id('list-Good Ppl')).tap()
-  })
-
-  it('Adds and removes users on curatelists from the profile', 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()
-
-    await element(by.id('profileHeaderDropdownBtn')).tap()
-    await element(by.text('Add to Lists')).tap()
-    await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
-    await element(by.id('user-bob.test-addBtn')).tap()
-    await element(by.id('doneBtn')).tap()
-    await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
-
-    await element(by.id('profileHeaderDropdownBtn')).tap()
-    await element(by.text('Add to Lists')).tap()
-    await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
-    await element(by.id('user-bob.test-addBtn')).tap()
-    await element(by.id('doneBtn')).tap()
-    await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
-  })
-
-  it('Can report a user list', async () => {
-    await element(by.id('e2eGotoSettings')).tap()
-    await element(by.id('signOutBtn')).tap()
-    await loginAsBob()
-    await element(by.id('bottomBarSearchBtn')).tap()
-    await element(by.id('searchTextInput')).typeText('alice')
-    await element(by.id('searchAutoCompleteResult-alice.test')).tap()
-    await element(by.id('profilePager-selector')).swipe('left')
-    await element(by.id('profilePager-selector-3')).tap()
-    await element(by.id('list-Good Ppl')).tap()
-    await element(by.id('headerDropdownBtn')).tap()
-    await element(by.text('Report List')).tap()
-    await expect(element(by.id('reportModal'))).toBeVisible()
-    await expect(element(by.text('Report List'))).toBeVisible()
-    await element(
-      by.id('reportReasonRadios-com.atproto.moderation.defs#reasonRude'),
-    ).tap()
-    await element(by.id('sendReportBtn')).tap()
-    await expect(element(by.id('reportModal'))).not.toBeVisible()
-  })
-})
diff --git a/__e2e__/tests/home-screen.test.ts b/__e2e__/tests/home-screen.test.ts
deleted file mode 100644
index b594c4697..000000000
--- a/__e2e__/tests/home-screen.test.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-/* eslint-env detox/detox */
-
-import {beforeAll, describe, it} from '@jest/globals'
-import {expect} from 'detox'
-
-import {createServer, loginAsAlice, openApp} from '../util'
-
-describe('Home screen', () => {
-  beforeAll(async () => {
-    await createServer('?users&follows&posts&feeds')
-    await openApp({permissions: {notifications: 'YES'}})
-  })
-
-  it('Login', async () => {
-    await loginAsAlice()
-    await element(by.id('homeScreenFeedTabs-Following')).tap()
-  })
-
-  it('Can go to feeds page using feeds button in tab bar', async () => {
-    await element(by.id('homeScreenFeedTabs-Feeds ✨')).tap()
-    await expect(element(by.text('Discover New Feeds'))).toBeVisible()
-  })
-
-  it('Feeds button disappears after pinning a feed', async () => {
-    await element(by.id('bottomBarProfileBtn')).tap()
-    await element(by.id('profilePager-selector')).swipe('left')
-    await element(by.id('profilePager-selector-4')).tap()
-    await element(by.id('feed-alice-favs')).tap()
-    await element(by.id('pinBtn')).tap()
-    await element(by.id('bottomBarHomeBtn')).tap()
-    await expect(
-      element(by.id('homeScreenFeedTabs-Feeds ✨')),
-    ).not.toBeVisible()
-  })
-
-  it('Can like posts', async () => {
-    const carlaPosts = by.id('feedItem-by-carla.test')
-    await expect(
-      element(by.id('likeCount').withAncestor(carlaPosts)).atIndex(0),
-    ).not.toExist()
-    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),
-    ).not.toExist()
-  })
-
-  it('Can repost posts', async () => {
-    const carlaPosts = by.id('feedItem-by-carla.test')
-    await expect(
-      element(by.id('repostCount').withAncestor(carlaPosts)).atIndex(0),
-    ).not.toExist()
-    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),
-    ).not.toExist()
-  })
-
-  // TODO skipping because the test env PDS isnt setup correctly to handle the report -prf
-  // 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.text('Report post')).tap()
-  //   await element(by.id('com.atproto.moderation.defs#reasonSpam')).tap()
-  //   await element(by.id('sendReportBtn')).tap()
-  // })
-
-  it('Can swipe between feeds', async () => {
-    await element(by.id('homeScreen')).swipe('left', 'fast', 0.75)
-    await expect(element(by.id('customFeedPage'))).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-alice-favs')).tap()
-    await expect(element(by.id('customFeedPage'))).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.text('Delete post')).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/invite-codes.test.skip.ts b/__e2e__/tests/invite-codes.test.skip.ts
deleted file mode 100644
index 9f00f0525..000000000
--- a/__e2e__/tests/invite-codes.test.skip.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-/* eslint-env detox/detox */
-
-import {beforeAll, describe, it} from '@jest/globals'
-import {expect} from 'detox'
-
-import {createServer, loginAsAlice, openApp} from '../util'
-
-describe('invite-codes', () => {
-  let service: string
-  let inviteCode = ''
-  beforeAll(async () => {
-    service = await createServer('?users&invite')
-    await openApp({permissions: {notifications: 'YES'}})
-  })
-
-  it('I can fetch invite codes', async () => {
-    await loginAsAlice()
-    await element(by.id('e2eOpenInviteCodesModal')).tap()
-    await expect(element(by.id('inviteCodesModal'))).toBeVisible()
-    const attrs = await element(by.id('inviteCode-0-code')).getAttributes()
-    inviteCode = attrs.text
-    await element(by.id('closeBtn')).tap()
-    await element(by.id('e2eSignOut')).tap()
-  })
-
-  it('I can create a new account with the invite code', async () => {
-    await element(by.id('e2eOpenLoggedOutView')).tap()
-    await element(by.id('createAccountButton')).tap()
-    await device.takeScreenshot('1- opened create account screen')
-    await element(by.id('selectServiceButton')).tap()
-    await device.takeScreenshot('2- selected other server')
-    await element(by.id('customSelectBtn')).tap()
-    await element(by.id('customServerTextInput')).typeText(service)
-    await element(by.id('customServerTextInput')).tapReturnKey()
-    await element(by.id('doneBtn')).tap()
-    await device.takeScreenshot('3- input test server URL')
-    await element(by.id('inviteCodeInput')).typeText(inviteCode)
-    await element(by.id('emailInput')).typeText('example@test.com')
-    await element(by.id('passwordInput')).typeText('hunter2')
-    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('onboardingInterests'))).toBeVisible()
-  })
-})
diff --git a/__e2e__/tests/login.test.ts b/__e2e__/tests/login.test.ts
deleted file mode 100644
index b4cedef6c..000000000
--- a/__e2e__/tests/login.test.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/* eslint-env detox/detox */
-
-import {describe, beforeAll, it} from '@jest/globals'
-import {expect} from '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 element(by.id('e2eOpenLoggedOutView')).tap()
-
-    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/merge-feed.test.skip.ts b/__e2e__/tests/merge-feed.test.skip.ts
deleted file mode 100644
index 4a8b3cbce..000000000
--- a/__e2e__/tests/merge-feed.test.skip.ts
+++ /dev/null
@@ -1,163 +0,0 @@
-/* eslint-env detox/detox */
-
-import {describe, beforeAll, it} from '@jest/globals'
-import {expect} from 'detox'
-import {openApp, loginAsAlice, createServer} from '../util'
-
-describe('Mergefeed', () => {
-  beforeAll(async () => {
-    await createServer('?mergefeed')
-    await openApp({permissions: {notifications: 'YES'}})
-  })
-
-  it('Login', async () => {
-    await element(by.id('e2eOpenLoggedOutView')).tap()
-    await loginAsAlice()
-    await element(by.id('e2eToggleMergefeed')).tap()
-    await element(by.id('bottomBarFeedsBtn')).tap()
-    await element(by.id('feed-alice-favs-toggleSave')).tap()
-    await element(by.id('e2eGotoHome')).tap()
-  })
-
-  it('Sees the expected mix of posts with default filters', async () => {
-    await element(by.id('followingFeedPage-feed-flatlist')).swipe(
-      'down',
-      'slow',
-      1,
-      0.5,
-      0.5,
-    )
-    // followed users
-    await expect(
-      element(
-        by.id('postText').withAncestor(by.id('feedItem-by-carla.test')),
-      ).atIndex(0),
-    ).toHaveText('Post 9')
-    await expect(
-      element(
-        by.id('postText').withAncestor(by.id('feedItem-by-bob.test')),
-      ).atIndex(0),
-    ).toHaveText('Post 9')
-    await element(by.id('followingFeedPage-feed-flatlist')).swipe(
-      'up',
-      'fast',
-      1,
-      0.5,
-      0.5,
-    )
-    // feed users
-    await expect(
-      element(
-        by.id('postText').withAncestor(by.id('feedItem-by-dan.test')),
-      ).atIndex(0),
-    ).toHaveText('Post 0')
-  })
-
-  it('Sees the expected mix of posts with replies disabled', async () => {
-    await element(by.id('followingFeedPage-feed-flatlist')).swipe(
-      'down',
-      'fast',
-      1,
-      0.5,
-      0.5,
-    )
-    await element(by.id('followingFeedPage-feed-flatlist')).swipe(
-      'down',
-      'fast',
-      1,
-      0.5,
-      0.5,
-    )
-    await element(by.id('viewHeaderHomeFeedPrefsBtn')).tap()
-    await element(by.id('toggleRepliesBtn')).tap()
-    await element(by.id('confirmBtn')).tap()
-    await element(by.id('followingFeedPage-feed-flatlist')).swipe(
-      'down',
-      'slow',
-      1,
-      0.5,
-      0.5,
-    )
-
-    // followed users
-    await expect(
-      element(
-        by.id('postText').withAncestor(by.id('feedItem-by-carla.test')),
-      ).atIndex(0),
-    ).toHaveText('Post 9')
-    await expect(
-      element(
-        by.id('postText').withAncestor(by.id('feedItem-by-bob.test')),
-      ).atIndex(0),
-    ).toHaveText('Post 9')
-    await element(by.id('followingFeedPage-feed-flatlist')).swipe(
-      'up',
-      'fast',
-      1,
-      0.5,
-      0.5,
-    )
-
-    // feed users
-    await expect(
-      element(
-        by.id('postText').withAncestor(by.id('feedItem-by-dan.test')),
-      ).atIndex(0),
-    ).toHaveText('Post 0')
-  })
-
-  it('Sees the expected mix of posts with no follows', async () => {
-    await element(by.id('followingFeedPage-feed-flatlist')).swipe(
-      'down',
-      'fast',
-      1,
-      0.5,
-      0.5,
-    )
-
-    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()
-    await element(by.id('unfollowBtn')).tap()
-    await element(by.id('profileHeaderBackBtn')).tap()
-
-    // have to wait for the toast to clear
-    await waitFor(element(by.id('searchTextInputClearBtn')))
-      .toBeVisible()
-      .withTimeout(5000)
-    await element(by.id('searchTextInputClearBtn')).tap()
-    await element(by.id('searchTextInput')).typeText('carla')
-    await element(by.id('searchAutoCompleteResult-carla.test')).tap()
-    await expect(element(by.id('profileView'))).toBeVisible()
-    await element(by.id('unfollowBtn')).tap()
-    await element(by.id('profileHeaderBackBtn')).tap()
-
-    await element(by.id('bottomBarHomeBtn')).tap()
-    await element(by.id('followingFeedPage-feed-flatlist')).swipe(
-      'down',
-      'slow',
-      1,
-      0.5,
-      0.5,
-    )
-    await element(by.id('followingFeedPage-feed-flatlist')).swipe(
-      'down',
-      'slow',
-      1,
-      0.5,
-      0.5,
-    )
-
-    // followed users NOT present
-    await expect(element(by.id('feedItem-by-carla.test'))).not.toExist()
-    await expect(element(by.id('feedItem-by-bob.test'))).not.toExist()
-
-    // feed users
-    await expect(
-      element(
-        by.id('postText').withAncestor(by.id('feedItem-by-dan.test')),
-      ).atIndex(0),
-    ).toHaveText('Post 0')
-  })
-})
diff --git a/__e2e__/tests/mod-lists.test.ts b/__e2e__/tests/mod-lists.test.ts
deleted file mode 100644
index c3d4149e0..000000000
--- a/__e2e__/tests/mod-lists.test.ts
+++ /dev/null
@@ -1,189 +0,0 @@
-/* eslint-env detox/detox */
-
-import {describe, beforeAll, it} from '@jest/globals'
-import {expect} from 'detox'
-import {openApp, loginAsAlice, loginAsBob, createServer} from '../util'
-
-describe('Mod lists', () => {
-  beforeAll(async () => {
-    await createServer('?users&follows&labels')
-    await openApp({
-      permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
-    })
-  })
-
-  it('Login and view my modlists', async () => {
-    await loginAsAlice()
-    await element(by.id('e2eGotoModeration')).tap()
-    await element(by.id('moderationlistsBtn')).tap()
-    await expect(element(by.id('list-Muted Users'))).toBeVisible()
-    await element(by.id('list-Muted Users')).tap()
-    await expect(
-      element(by.id('user-muted-by-list-account.test')),
-    ).toBeVisible()
-  })
-
-  it('Toggle mute subscription', async () => {
-    await element(by.id('unmuteBtn')).tap()
-    await element(by.id('subscribeBtn')).tap()
-    await element(by.text('Mute accounts')).tap()
-    await element(by.id('confirmBtn')).tap()
-  })
-
-  it('Edit display name and description via the edit modlist modal', async () => {
-    await element(by.id('headerDropdownBtn')).tap()
-    await element(by.text('Edit list details')).tap()
-    await expect(element(by.id('createOrEditListModal'))).toBeVisible()
-    await element(by.id('editNameInput')).clearText()
-    await element(by.id('editNameInput')).typeText('Bad Ppl')
-    await element(by.id('editDescriptionInput')).clearText()
-    await element(by.id('editDescriptionInput')).typeText('They bad')
-    await element(by.id('saveBtn')).tap()
-    await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
-    await expect(element(by.id('headerTitle'))).toHaveText('Bad Ppl')
-    await expect(element(by.id('listDescription'))).toHaveText('They bad')
-    // have to wait for the toast to clear
-    await waitFor(element(by.id('headerDropdownBtn')))
-      .toBeVisible()
-      .withTimeout(5000)
-  })
-
-  it('Remove description via the edit modlist modal', async () => {
-    await element(by.id('headerDropdownBtn')).tap()
-    await element(by.text('Edit list details')).tap()
-    await expect(element(by.id('createOrEditListModal'))).toBeVisible()
-    await element(by.id('editDescriptionInput')).clearText()
-    await element(by.id('saveBtn')).tap()
-    await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
-    await expect(element(by.id('listDescription'))).not.toBeVisible()
-    // have to wait for the toast to clear
-    await waitFor(element(by.id('headerDropdownBtn')))
-      .toBeVisible()
-      .withTimeout(5000)
-  })
-
-  // DISABLED e2e environment is real finicky about avatar uploads -prf
-  // it('Set avi via the edit modlist modal', async () => {
-  //   await expect(element(by.id('userAvatarFallback'))).toExist()
-  //   await element(by.id('headerDropdownBtn')).tap()
-  //   await element(by.text('Edit list details')).tap()
-  //   await expect(element(by.id('createOrEditListModal'))).toBeVisible()
-  //   await element(by.id('changeAvatarBtn')).tap()
-  //   await element(by.text('Library')).tap()
-  //   await sleep(3e3)
-  //   await element(by.id('saveBtn')).tap()
-  //   await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
-  //   await expect(element(by.id('userAvatarImage'))).toExist()
-  //   // have to wait for the toast to clear
-  //   await waitFor(element(by.id('headerDropdownBtn')))
-  //     .toBeVisible()
-  //     .withTimeout(5000)
-  // })
-
-  // it('Remove avi via the edit modlist modal', async () => {
-  //   await expect(element(by.id('userAvatarImage'))).toExist()
-  //   await element(by.id('headerDropdownBtn')).tap()
-  //   await element(by.text('Edit list details')).tap()
-  //   await expect(element(by.id('createOrEditListModal'))).toBeVisible()
-  //   await element(by.id('changeAvatarBtn')).tap()
-  //   await element(by.text('Remove')).tap()
-  //   await element(by.id('saveBtn')).tap()
-  //   await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
-  //   await expect(element(by.id('userAvatarFallback'))).toExist()
-  //   // have to wait for the toast to clear
-  //   await waitFor(element(by.id('headerDropdownBtn')))
-  //     .toBeVisible()
-  //     .withTimeout(5000)
-  // })
-
-  it('Delete the modlist', async () => {
-    await element(by.id('headerDropdownBtn')).tap()
-    await element(by.text('Delete List')).tap()
-    await element(by.id('confirmBtn')).tap()
-    await expect(element(by.id('listsEmpty'))).toBeVisible()
-  })
-
-  it('Create a new modlist', async () => {
-    await element(by.id('newModListBtn')).tap()
-    await expect(element(by.id('createOrEditListModal'))).toBeVisible()
-    await element(by.id('editNameInput')).typeText('Bad Ppl')
-    await element(by.id('editDescriptionInput')).typeText('They bad')
-    await element(by.id('saveBtn')).tap()
-    await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
-    await expect(element(by.id('headerTitle'))).toHaveText('Bad Ppl')
-    await expect(element(by.id('listDescription'))).toHaveText('They bad')
-  })
-
-  it('Adds and removes users on modlists from the list', async () => {
-    await element(by.id('addUserBtn')).tap()
-    await expect(element(by.id('listAddUserModal'))).toBeVisible()
-    await waitFor(element(by.id('user-warn-posts.test-addBtn')))
-      .toBeVisible()
-      .withTimeout(5000)
-    await element(by.id('user-warn-posts.test-addBtn')).tap()
-    await element(by.id('doneBtn')).tap()
-    await expect(element(by.id('listAddUserModal'))).not.toBeVisible()
-    await element(by.id('listItems-flatlist')).swipe(
-      'down',
-      'slow',
-      1,
-      0.5,
-      0.5,
-    )
-    await expect(element(by.id('user-warn-posts.test'))).toBeVisible()
-    await element(by.id('user-warn-posts.test-editBtn')).tap()
-    await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
-    await element(by.id('user-warn-posts.test-addBtn')).tap()
-    await element(by.id('doneBtn')).tap()
-    await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
-  })
-
-  it('Shows the modlist on my profile', async () => {
-    await element(by.id('bottomBarProfileBtn')).tap()
-    await element(by.id('profilePager-selector')).swipe('left')
-    await element(by.id('profilePager-selector-5')).tap()
-    await element(by.id('list-Bad Ppl')).tap()
-  })
-
-  it('Adds and removes users on modlists from the profile', 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()
-
-    await element(by.id('profileHeaderDropdownBtn')).tap()
-    await element(by.text('Add to Lists')).tap()
-    await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
-    await element(by.id('user-bob.test-addBtn')).tap()
-    await element(by.id('doneBtn')).tap()
-    await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
-
-    await element(by.id('profileHeaderDropdownBtn')).tap()
-    await element(by.text('Add to Lists')).tap()
-    await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
-    await element(by.id('user-bob.test-addBtn')).tap()
-    await element(by.id('doneBtn')).tap()
-    await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
-  })
-
-  it('Can report a mute list', async () => {
-    await element(by.id('e2eGotoSettings')).tap()
-    await element(by.id('signOutBtn')).tap()
-    await loginAsBob()
-    await element(by.id('bottomBarSearchBtn')).tap()
-    await element(by.id('searchTextInput')).typeText('alice')
-    await element(by.id('searchAutoCompleteResult-alice.test')).tap()
-    await element(by.id('profilePager-selector')).swipe('left')
-    await element(by.id('profilePager-selector-3')).tap()
-    await element(by.id('list-Bad Ppl')).tap()
-    await element(by.id('headerDropdownBtn')).tap()
-    await element(by.text('Report List')).tap()
-    await expect(element(by.id('reportModal'))).toBeVisible()
-    await expect(element(by.text('Report List'))).toBeVisible()
-    await element(
-      by.id('reportReasonRadios-com.atproto.moderation.defs#reasonRude'),
-    ).tap()
-    await element(by.id('sendReportBtn')).tap()
-    await expect(element(by.id('reportModal'))).not.toBeVisible()
-  })
-})
diff --git a/__e2e__/tests/profile-screen.test.ts b/__e2e__/tests/profile-screen.test.ts
deleted file mode 100644
index 7c3207ec8..000000000
--- a/__e2e__/tests/profile-screen.test.ts
+++ /dev/null
@@ -1,196 +0,0 @@
-/* eslint-env detox/detox */
-
-import {beforeAll, describe, it} from '@jest/globals'
-import {expect} from 'detox'
-
-import {createServer, loginAsAlice, openApp, sleep} from '../util'
-
-describe('Profile screen', () => {
-  beforeAll(async () => {
-    await createServer('?users&posts&feeds')
-    await openApp({
-      permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
-    })
-  })
-
-  it('Login and navigate to my profile', async () => {
-    await loginAsAlice()
-    await element(by.id('bottomBarProfileBtn')).tap()
-  })
-
-  it('Can see feeds', async () => {
-    await element(by.id('profilePager-selector')).swipe('left')
-    await element(by.id('profilePager-selector-4')).tap()
-    await expect(element(by.id('feed-alice-favs'))).toBeVisible()
-    await element(by.id('profilePager-selector')).swipe('right')
-    await element(by.id('profilePager-selector-0')).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'))).not.toExist()
-  })
-
-  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.text('Upload from Library')).tap()
-    await sleep(3e3)
-    await element(by.id('changeAvatarBtn')).tap()
-    await element(by.text('Upload from Library')).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.text('Remove Banner')).tap()
-    await element(by.id('changeAvatarBtn')).tap()
-    await element(by.text('Remove Avatar')).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(5000)
-    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('profileHeaderAlert'))).not.toExist()
-    await element(by.id('profileHeaderDropdownBtn')).tap()
-    await element(by.text('Mute Account')).tap()
-    await expect(element(by.id('profileHeaderAlert'))).toBeVisible()
-    await element(by.id('profileHeaderDropdownBtn')).tap()
-    await element(by.text('Unmute Account')).tap()
-    await expect(element(by.id('profileHeaderAlert'))).not.toExist()
-  })
-
-  // TODO skipping because the test env PDS isnt setup correctly to handle the report -prf
-  // it('Can report another user', async () => {
-  //   await element(by.id('profileHeaderDropdownBtn')).tap()
-  //   await element(by.text('Report Account')).tap()
-  //   await expect(element(by.id('reportModal'))).toBeVisible()
-  //   await element(
-  //     by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
-  //   ).tap()
-  //   await element(by.id('sendReportBtn')).tap()
-  //   await expect(element(by.id('reportModal'))).not.toBeVisible()
-  // })
-
-  it('Can like posts', async () => {
-    await element(by.id('postsFeed-flatlist')).swipe(
-      'down',
-      'slow',
-      1,
-      0.5,
-      0.5,
-    )
-
-    const posts = by.id('feedItem-by-bob.test')
-    await expect(
-      element(by.id('likeCount').withAncestor(posts)).atIndex(0),
-    ).not.toExist()
-    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),
-    ).not.toExist()
-  })
-
-  it('Can repost posts', async () => {
-    const posts = by.id('feedItem-by-bob.test')
-    await expect(
-      element(by.id('repostCount').withAncestor(posts)).atIndex(0),
-    ).not.toExist()
-    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),
-    ).not.toExist()
-  })
-
-  // TODO skipping because the test env PDS isnt setup correctly to handle the report -prf
-  // 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.text('Report post')).tap()
-  //   await expect(element(by.id('reportModal'))).toBeVisible()
-  //   await element(
-  //     by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
-  //   ).tap()
-  //   await element(by.id('sendReportBtn')).tap()
-  //   await expect(element(by.id('reportModal'))).not.toBeVisible()
-  // })
-})
diff --git a/__e2e__/tests/search-screen.test.ts b/__e2e__/tests/search-screen.test.ts
deleted file mode 100644
index 1dbb3cbfa..000000000
--- a/__e2e__/tests/search-screen.test.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/* eslint-env detox/detox */
-
-import {describe, beforeAll, it} from '@jest/globals'
-import {expect} from 'detox'
-import {openApp, loginAsAlice, createServer} from '../util'
-
-describe('Search screen', () => {
-  beforeAll(async () => {
-    await createServer('?users')
-    await openApp({
-      permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
-    })
-  })
-
-  it('Login', async () => {
-    await loginAsAlice()
-  })
-
-  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/self-labeling.test.ts b/__e2e__/tests/self-labeling.test.ts
deleted file mode 100644
index bba8ed484..000000000
--- a/__e2e__/tests/self-labeling.test.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-/* eslint-env detox/detox */
-
-import {describe, beforeAll, it} from '@jest/globals'
-import {expect} from 'detox'
-import {openApp, loginAsAlice, createServer, sleep} from '../util'
-
-describe('Self-labeling', () => {
-  beforeAll(async () => {
-    await createServer('?users')
-    await openApp({
-      permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
-    })
-  })
-
-  it('Login', async () => {
-    await loginAsAlice()
-    await element(by.id('homeScreenFeedTabs-Following')).tap()
-  })
-
-  it('Post an image with the porn label', 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(3e3)
-    await element(by.id('labelsBtn')).tap()
-    await element(by.id('pornLabelBtn')).tap()
-    await element(by.id('confirmBtn')).tap()
-    await element(by.id('composerPublishBtn')).tap()
-    await expect(element(by.id('composeFAB'))).toBeVisible()
-    const posts = by.id('feedItem-by-alice.test')
-    await element(by.id('e2eRefreshHome')).tap()
-    await expect(
-      element(by.id('contentHider-embed').withAncestor(posts)).atIndex(0),
-    ).toExist()
-  })
-})
diff --git a/__e2e__/tests/shell.test.skip.ts b/__e2e__/tests/shell.test.skip.ts
deleted file mode 100644
index 69619dd81..000000000
--- a/__e2e__/tests/shell.test.skip.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-/* eslint-env detox/detox */
-
-import {openApp, loginAsAlice, createServer} from '../util'
-
-describe('Shell', () => {
-  beforeAll(async () => {
-    await createServer('?users')
-    await openApp({permissions: {notifications: 'YES'}})
-  })
-
-  it('Login', async () => {
-    await loginAsAlice()
-    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-muting.test.ts b/__e2e__/tests/thread-muting.test.ts
deleted file mode 100644
index ae62f93dc..000000000
--- a/__e2e__/tests/thread-muting.test.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-/* eslint-env detox/detox */
-
-import {describe, beforeAll, it} from '@jest/globals'
-import {expect} from 'detox'
-import {openApp, loginAsAlice, loginAsBob, createServer} from '../util'
-
-describe('Thread muting', () => {
-  beforeAll(async () => {
-    await createServer('?users&follows')
-    await openApp({permissions: {notifications: 'YES'}})
-  })
-
-  it('Login, create a thread, and log out', async () => {
-    await loginAsAlice()
-    await element(by.id('homeScreenFeedTabs-Following')).tap()
-    await element(by.id('composeFAB')).tap()
-    await element(by.id('composerTextInput')).typeText('Test thread')
-    await element(by.id('composerPublishBtn')).tap()
-    await expect(element(by.id('composeFAB'))).toBeVisible()
-  })
-
-  it('Login, reply to the thread, and log out', async () => {
-    await loginAsBob()
-    await element(by.id('homeScreenFeedTabs-Following')).tap()
-    const alicePosts = by.id('feedItem-by-alice.test')
-    await element(by.id('replyBtn').withAncestor(alicePosts)).atIndex(0).tap()
-    await element(by.id('composerTextInput')).typeText('Reply 1')
-    await element(by.id('composerPublishBtn')).tap()
-    await expect(element(by.id('composeFAB'))).toBeVisible()
-  })
-
-  it('Login, confirm notification exists, mute thread, and log out', async () => {
-    await loginAsAlice()
-    await element(by.id('bottomBarNotificationsBtn')).tap()
-    const bobNotifs = by.id('feedItem-by-bob.test')
-    await expect(
-      element(by.id('postText').withAncestor(bobNotifs)).atIndex(0),
-    ).toHaveText('Reply 1')
-    await element(by.id('postDropdownBtn').withAncestor(bobNotifs))
-      .atIndex(0)
-      .tap()
-    await element(by.text('Mute thread')).tap()
-    // have to wait for the toast to clear
-    await waitFor(element(by.id('viewHeaderDrawerBtn')))
-      .toBeVisible()
-      .withTimeout(5000)
-  })
-
-  it('Login, reply to the thread twice, and log out', async () => {
-    await loginAsBob()
-
-    await element(by.id('bottomBarProfileBtn')).tap()
-    await element(by.id('profilePager-selector-1')).tap()
-    const bobPosts = by.id('feedItem-by-bob.test')
-    await element(by.id('replyBtn').withAncestor(bobPosts)).atIndex(0).tap()
-    await element(by.id('composerTextInput')).typeText('Reply 2')
-    await element(by.id('composerPublishBtn')).tap()
-    await expect(element(by.id('composeFAB'))).toBeVisible()
-
-    const alicePosts = by.id('feedItem-by-alice.test')
-    await element(by.id('replyBtn').withAncestor(alicePosts)).atIndex(0).tap()
-    await element(by.id('composerTextInput')).typeText('Reply 3')
-    await element(by.id('composerPublishBtn')).tap()
-    await expect(element(by.id('composeFAB'))).toBeVisible()
-
-    await element(by.id('bottomBarHomeBtn')).tap()
-  })
-
-  it('Login, confirm notifications dont exist, unmute the thread, confirm notifications exist', async () => {
-    await loginAsAlice()
-
-    await element(by.id('bottomBarNotificationsBtn')).tap()
-    const bobNotifs = by.id('feedItem-by-bob.test')
-    await expect(
-      element(by.id('postText').withAncestor(bobNotifs)).atIndex(0),
-    ).not.toExist()
-
-    await element(by.id('bottomBarHomeBtn')).tap()
-    const alicePosts = by.id('feedItem-by-alice.test')
-    await element(by.id('postDropdownBtn').withAncestor(alicePosts))
-      .atIndex(0)
-      .tap()
-    await element(by.text('Unmute thread')).tap()
-
-    // TODO
-    // the swipe down to trigger PTR isnt working and I dont want to block on this
-    // -prf
-    // await element(by.id('bottomBarNotificationsBtn')).tap()
-    // await element(by.id('notifsFeed')).swipe('down', 'fast')
-    // await waitFor(element(by.id('postText').withAncestor(bobNotifs)))
-    //   .toBeVisible()
-    //   .withTimeout(5000)
-    // await expect(
-    //   element(by.id('postText').withAncestor(bobNotifs)).atIndex(0),
-    // ).toHaveText('Reply 2')
-    // await expect(
-    //   element(by.id('postText').withAncestor(bobNotifs)).atIndex(1),
-    // ).toHaveText('Reply 3')
-    // await expect(
-    //   element(by.id('postText').withAncestor(bobNotifs)).atIndex(2),
-    // ).toHaveText('Reply 1')
-  })
-})
diff --git a/__e2e__/tests/thread-screen.test.ts b/__e2e__/tests/thread-screen.test.ts
deleted file mode 100644
index b99da11a6..000000000
--- a/__e2e__/tests/thread-screen.test.ts
+++ /dev/null
@@ -1,131 +0,0 @@
-/* eslint-env detox/detox */
-
-import {beforeAll, describe, it} from '@jest/globals'
-import {expect} from 'detox'
-
-import {createServer, loginAsAlice, openApp} from '../util'
-
-describe('Thread screen', () => {
-  beforeAll(async () => {
-    await createServer('?users&follows&thread')
-    await openApp({permissions: {notifications: 'YES'}})
-  })
-
-  it('Login & navigate to thread', async () => {
-    await loginAsAlice()
-    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-expanded').withAncestor(post)).atIndex(0),
-    ).not.toExist()
-    await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap()
-    await expect(
-      element(by.id('likeCount-expanded').withAncestor(post)).atIndex(0),
-    ).toHaveText('1 like')
-    await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap()
-    await expect(
-      element(by.id('likeCount-expanded').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),
-    ).not.toExist()
-    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),
-    ).not.toExist()
-  })
-
-  it('Can repost the root post', async () => {
-    const post = by.id('postThreadItem-by-bob.test')
-    await expect(
-      element(by.id('repostCount-expanded').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-expanded').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-expanded').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),
-    ).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')
-    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()
-  })
-
-  // TODO skipping because the test env PDS isnt setup correctly to handle the report -prf
-  // 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.text('Report post')).tap()
-  //   await expect(element(by.id('reportModal'))).toBeVisible()
-  //   await element(
-  //     by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
-  //   ).tap()
-  //   await element(by.id('sendReportBtn')).tap()
-  //   await expect(element(by.id('reportModal'))).not.toBeVisible()
-  // })
-
-  // TODO skipping because the test env PDS isnt setup correctly to handle the report -prf
-  // 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.text('Report post')).tap()
-  //   await expect(element(by.id('reportModal'))).toBeVisible()
-  //   await element(
-  //     by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
-  //   ).tap()
-  //   await element(by.id('sendReportBtn')).tap()
-  //   await expect(element(by.id('reportModal'))).not.toBeVisible()
-  // })
-})
diff --git a/__e2e__/util.ts b/__e2e__/util.ts
deleted file mode 100644
index 70fbdb601..000000000
--- a/__e2e__/util.ts
+++ /dev/null
@@ -1,141 +0,0 @@
-import {execSync} from 'child_process'
-import {resolveConfig} from 'detox/internals'
-import http from 'http'
-
-const platform = device.getPlatform()
-
-export async function openApp(opts: any) {
-  opts = opts || {}
-  const config = await resolveConfig()
-
-  if (device.getPlatform() === 'ios') {
-    // disable password autofill
-    execSync(
-      `plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles/Library/ConfigurationProfiles/UserSettings.plist`,
-    )
-    execSync(
-      `plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Library/UserConfigurationProfiles/EffectiveUserSettings.plist`,
-    )
-    execSync(
-      `plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Library/UserConfigurationProfiles/PublicInfo/PublicEffectiveUserSettings.plist`,
-    )
-  }
-  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('selectServiceButton')).tap()
-  if (takeScreenshots) {
-    await device.takeScreenshot('2- opened service selector')
-  }
-  await element(by.id('customSelectBtn')).tap()
-  await element(by.id('customServerTextInput')).typeText(service)
-  await element(by.id('customServerTextInput')).tapReturnKey()
-  await element(by.id('doneBtn')).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()
-}
-
-export async function loginAsAlice() {
-  await element(by.id('e2eSignInAlice')).tap()
-}
-
-export async function loginAsBob() {
-  await element(by.id('e2eSignInBob')).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 = ''): Promise<string> {
-  return new Promise(function (resolve, reject) {
-    var req = http.request(
-      {
-        method: 'POST',
-        host: 'localhost',
-        port: 1986,
-        path: `/${path}`,
-      },
-      function (res) {
-        const body: Buffer[] = []
-        res.on('data', chunk => body.push(chunk))
-        res.on('end', function () {
-          try {
-            resolve(Buffer.concat(body).toString())
-          } catch (e) {
-            reject(e)
-          }
-        })
-      },
-    )
-    req.on('error', reject)
-    req.end()
-  })
-}
-
-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/docs/build.md b/docs/build.md
index deab91a5b..88733d3b0 100644
--- a/docs/build.md
+++ b/docs/build.md
@@ -16,10 +16,6 @@
     - Add `eval "$(rbenv init - zsh)"` to your `~/.zshrc`
   - From inside the project directory:
     - `bundler install` (this will install Cocoapods)
-- Setup your environment [for e2e testing using detox](https://wix.github.io/Detox/docs/introduction/getting-started):
-  - `yarn global add detox-cli`
-  - `brew tap wix/brew`
-  - `brew install applesimutils`
 - After initial setup:
   - Copy `google-services.json.example` to `google-services.json` or provide your own `google-services.json`. (A real firebase project is NOT required)
   - `npx expo prebuild` -> you will also need to run this anytime `app.json` or native `package.json` deps change
@@ -120,10 +116,7 @@ To open the [Developer Menu](https://docs.expo.dev/debugging/tools/#developer-me
 
 ### Running E2E Tests
 
-- Make sure you've set your environment following the above
-- Make sure Metro and the dev server are running
-- Run `yarn e2e`
-- Find the artifacts in the `artifact` folder
+See [testing.md](./testing.md).
 
 ### Polyfills
 
diff --git a/docs/testing.md b/docs/testing.md
index e9b9445e0..ae0a424fb 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -3,13 +3,19 @@
 Make sure you've copied `.env.example` to `.env.test` and provided any required
 values.
 
-### Using Maestro E2E tests
+## Using Maestro
+
 1. Install Maestro by following [these instructions](https://maestro.mobile.dev/getting-started/installing-maestro). This will help us run the E2E tests.
-2. You can write Maestro tests in `__e2e__/maestro` directory by creating a new `.yaml` file or by modifying an existing one.
-3. You can also use [Maestro Studio](https://maestro.mobile.dev/getting-started/maestro-studio) which automatically generates commands by recording your actions on the app. Therefore, you can create realistic tests without having to manually write any code. Use  the `maestro studio` command to start recording your actions.
+2. You can write Maestro tests in `/.maestro/flows/` directory by creating a new `.yml` file or by modifying an existing one.
+3. You can also use [Maestro Studio](https://maestro.mobile.dev/getting-started/maestro-studio) which automatically generates commands by recording your actions on the app. Therefore, you can create realistic tests without having to manually write any code. Use the `maestro studio` command to start recording your actions.
+
+### Running Maestro tests
 
+- In one tab, run `yarn e2e:mock-server`
+- In a second tab, run `yarn e2e:metro`
+- In a third tab, run `yarn e2e:run`
 
-### Using Flashlight for Performance Testing
+## Using Flashlight for Performance Testing
 1. Make sure Maestro is installed (optional: only for automated testing) by following the instructions above
 2. Install Flashlight by following [these instructions](https://docs.flashlight.dev/)
 3. The simplest way to get started is by running `yarn perf:measure` which will run a live preview of the performance test results. You can [see a demo here](https://github.com/bamlab/flashlight/assets/4534323/4038a342-f145-4c3b-8cde-17949bf52612)
diff --git a/jest/test-pds.ts b/jest/test-pds.ts
index 1c52d944c..2fe623ca9 100644
--- a/jest/test-pds.ts
+++ b/jest/test-pds.ts
@@ -114,8 +114,7 @@ export async function createServer(
     pdsUrl,
     mocker: new Mocker(testNet, pdsUrl, pic),
     async close() {
-      await testNet.pds.server.destroy()
-      await testNet.plc.server.destroy()
+      await testNet.close()
     },
   }
 }
diff --git a/package.json b/package.json
index 4ed2b933f..13ffc35c8 100644
--- a/package.json
+++ b/package.json
@@ -31,11 +31,10 @@
     "lint": "eslint --cache --ext .js,.jsx,.ts,.tsx src",
     "typecheck": "tsc --project ./tsconfig.check.json",
     "e2e:mock-server": "./jest/dev-infra/with-test-redis-and-db.sh ts-node --project tsconfig.e2e.json __e2e__/mock-server.ts",
-    "e2e:metro": "NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
-    "e2e:build": "NODE_ENV=test detox build -c ios.sim.debug",
-    "e2e:run": "NODE_ENV=test detox test --configuration ios.sim.debug --take-screenshots all",
+    "e2e:metro": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
+    "e2e:run": "maestro test __e2e__",
     "perf:test": "NODE_ENV=test maestro test",
-    "perf:test:run": "NODE_ENV=test maestro test __e2e__/maestro/scroll.yaml",
+    "perf:test:run": "NODE_ENV=test maestro test __e2e__/perf-test.yml",
     "perf:test:measure": "NODE_ENV=test flashlight test --bundleId xyz.blueskyweb.app --testCommand \"yarn perf:test\" --duration 150000 --resultsFilePath .perf/results.json",
     "perf:test:results": "NODE_ENV=test flashlight report .perf/results.json",
     "perf:measure": "NODE_ENV=test flashlight measure",
@@ -239,10 +238,8 @@
     "babel-plugin-module-resolver": "^5.0.0",
     "babel-plugin-react-native-web": "^0.18.12",
     "babel-preset-expo": "^10.0.0",
-    "detox": "^20.14.8",
     "eslint": "^8.19.0",
     "eslint-plugin-bsky-internal": "link:./eslint",
-    "eslint-plugin-detox": "^1.0.0",
     "eslint-plugin-ft-flow": "^2.0.3",
     "eslint-plugin-lingui": "^0.2.0",
     "eslint-plugin-react": "^7.33.2",
diff --git a/src/view/com/testing/TestCtrls.e2e.tsx b/src/view/com/testing/TestCtrls.e2e.tsx
index 1eb99c4f5..1c82a712e 100644
--- a/src/view/com/testing/TestCtrls.e2e.tsx
+++ b/src/view/com/testing/TestCtrls.e2e.tsx
@@ -1,11 +1,14 @@
 import React from 'react'
-import {Pressable, View} from 'react-native'
-import {navigate} from '../../../Navigation'
-import {useModalControls} from '#/state/modals'
+import {LogBox, Pressable, View} from 'react-native'
 import {useQueryClient} from '@tanstack/react-query'
-import {useSessionApi} from '#/state/session'
+
+import {useModalControls} from '#/state/modals'
 import {useSetFeedViewPreferencesMutation} from '#/state/queries/preferences'
+import {useSessionApi} from '#/state/session'
 import {useLoggedOutViewControls} from '#/state/shell/logged-out'
+import {navigate} from '../../../Navigation'
+
+LogBox.ignoreAllLogs()
 
 /**
  * This utility component is only included in the test simulator
diff --git a/src/view/com/util/Toast.e2e.tsx b/src/view/com/util/Toast.e2e.tsx
new file mode 100644
index 000000000..c5582ff0a
--- /dev/null
+++ b/src/view/com/util/Toast.e2e.tsx
@@ -0,0 +1 @@
+export function show() {}
diff --git a/yarn.lock b/yarn.lock
index 1e53b3062..f29994bd2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10719,47 +10719,6 @@ detect-port-alt@^1.1.6:
     address "^1.0.1"
     debug "^2.6.0"
 
-detox@^20.14.8:
-  version "20.14.8"
-  resolved "https://registry.yarnpkg.com/detox/-/detox-20.14.8.tgz#0a550cf677fc98a68d56d162e1c5caad317de9ca"
-  integrity sha512-3E/0/7Cb7x+wcBsZpCxD8FykZUsFnfVT00d6PWH940boc0Mo1Kzabh+I151X/On4qZMqVdUzgwmap/z8g/kmaw==
-  dependencies:
-    ajv "^8.6.3"
-    bunyan "^1.8.12"
-    bunyan-debug-stream "^3.1.0"
-    caf "^15.0.1"
-    chalk "^4.0.0"
-    child-process-promise "^2.2.0"
-    execa "^5.1.1"
-    find-up "^5.0.0"
-    fs-extra "^11.0.0"
-    funpermaproxy "^1.1.0"
-    glob "^8.0.3"
-    ini "^1.3.4"
-    jest-environment-emit "^1.0.5"
-    json-cycle "^1.3.0"
-    lodash "^4.17.11"
-    multi-sort-stream "^1.0.3"
-    multipipe "^4.0.0"
-    node-ipc "9.2.1"
-    proper-lockfile "^3.0.2"
-    resolve-from "^5.0.0"
-    sanitize-filename "^1.6.1"
-    semver "^7.0.0"
-    serialize-error "^8.0.1"
-    shell-quote "^1.7.2"
-    signal-exit "^3.0.3"
-    stream-json "^1.7.4"
-    strip-ansi "^6.0.1"
-    telnet-client "1.2.8"
-    tempfile "^2.0.0"
-    trace-event-lib "^1.3.1"
-    which "^1.3.1"
-    ws "^7.0.0"
-    yargs "^17.0.0"
-    yargs-parser "^21.0.0"
-    yargs-unparser "^2.0.0"
-
 didyoumean@^1.2.2:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
@@ -11393,13 +11352,6 @@ eslint-module-utils@^2.8.0:
   version "0.0.0"
   uid ""
 
-eslint-plugin-detox@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-detox/-/eslint-plugin-detox-1.0.0.tgz#2d9c0130e8ebc4ced56efb6eeaf0d0f5c163398d"
-  integrity sha512-Dd+Cwyap5IO9DBKXOKrQTE1RQk9hvSSi+qsS1cMVPZY37mojz2PvriEOfGhKj5XN1G14lJ8TArf+6Y+Np2ZsoQ==
-  dependencies:
-    requireindex "~1.1.0"
-
 eslint-plugin-eslint-comments@^3.2.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz#9e1cd7b4413526abb313933071d7aba05ca12ffa"