about summary refs log tree commit diff
path: root/src/view/com/modals
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/modals')
-rw-r--r--src/view/com/modals/AddAppPasswords.tsx72
-rw-r--r--src/view/com/modals/AltImage.tsx23
-rw-r--r--src/view/com/modals/BirthDateSettings.tsx72
-rw-r--r--src/view/com/modals/ChangeEmail.tsx83
-rw-r--r--src/view/com/modals/ChangeHandle.tsx210
-rw-r--r--src/view/com/modals/Confirm.tsx15
-rw-r--r--src/view/com/modals/ContentFilteringSettings.tsx322
-rw-r--r--src/view/com/modals/CreateOrEditList.tsx72
-rw-r--r--src/view/com/modals/DeleteAccount.tsx65
-rw-r--r--src/view/com/modals/EditImage.tsx27
-rw-r--r--src/view/com/modals/EditProfile.tsx115
-rw-r--r--src/view/com/modals/InviteCodes.tsx183
-rw-r--r--src/view/com/modals/LinkWarning.tsx36
-rw-r--r--src/view/com/modals/ListAddRemoveUsers.tsx (renamed from src/view/com/modals/ListAddUser.tsx)149
-rw-r--r--src/view/com/modals/Modal.tsx47
-rw-r--r--src/view/com/modals/Modal.web.tsx34
-rw-r--r--src/view/com/modals/ModerationDetails.tsx8
-rw-r--r--src/view/com/modals/ProfilePreview.tsx79
-rw-r--r--src/view/com/modals/Repost.tsx21
-rw-r--r--src/view/com/modals/SelfLabel.tsx42
-rw-r--r--src/view/com/modals/ServerInput.tsx33
-rw-r--r--src/view/com/modals/SwitchAccount.tsx147
-rw-r--r--src/view/com/modals/UserAddRemoveLists.tsx324
-rw-r--r--src/view/com/modals/VerifyEmail.tsx68
-rw-r--r--src/view/com/modals/Waitlist.tsx31
-rw-r--r--src/view/com/modals/crop-image/CropImage.web.tsx23
-rw-r--r--src/view/com/modals/lang-settings/ConfirmLanguagesButton.tsx9
-rw-r--r--src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx35
-rw-r--r--src/view/com/modals/lang-settings/LanguageToggle.tsx21
-rw-r--r--src/view/com/modals/lang-settings/PostLanguagesSettings.tsx44
-rw-r--r--src/view/com/modals/report/InputIssueDetails.tsx11
-rw-r--r--src/view/com/modals/report/Modal.tsx26
-rw-r--r--src/view/com/modals/report/SendReportButton.tsx9
33 files changed, 1405 insertions, 1051 deletions
diff --git a/src/view/com/modals/AddAppPasswords.tsx b/src/view/com/modals/AddAppPasswords.tsx
index 29763620f..812a36f45 100644
--- a/src/view/com/modals/AddAppPasswords.tsx
+++ b/src/view/com/modals/AddAppPasswords.tsx
@@ -3,7 +3,6 @@ import {StyleSheet, TextInput, View, TouchableOpacity} from 'react-native'
 import {Text} from '../util/text/Text'
 import {Button} from '../util/forms/Button'
 import {s} from 'lib/styles'
-import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
 import {isNative} from 'platform/detection'
 import {
@@ -13,6 +12,13 @@ import {
 import Clipboard from '@react-native-clipboard/clipboard'
 import * as Toast from '../util/Toast'
 import {logger} from '#/logger'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
+import {
+  useAppPasswordsQuery,
+  useAppPasswordCreateMutation,
+} from '#/state/queries/app-passwords'
 
 export const snapPoints = ['70%']
 
@@ -53,7 +59,10 @@ const shadesOfBlue: string[] = [
 
 export function Component({}: {}) {
   const pal = usePalette('default')
-  const store = useStores()
+  const {_} = useLingui()
+  const {closeModal} = useModalControls()
+  const {data: passwords} = useAppPasswordsQuery()
+  const createMutation = useAppPasswordCreateMutation()
   const [name, setName] = useState(
     shadesOfBlue[Math.floor(Math.random() * shadesOfBlue.length)],
   )
@@ -69,33 +78,42 @@ export function Component({}: {}) {
   }, [appPassword])
 
   const onDone = React.useCallback(() => {
-    store.shell.closeModal()
-  }, [store])
+    closeModal()
+  }, [closeModal])
 
   const createAppPassword = async () => {
     // if name is all whitespace, we don't allow it
     if (!name || !name.trim()) {
       Toast.show(
         'Please enter a name for your app password. All spaces is not allowed.',
+        'times',
       )
       return
     }
     // if name is too short (under 4 chars), we don't allow it
     if (name.length < 4) {
-      Toast.show('App Password names must be at least 4 characters long.')
+      Toast.show(
+        'App Password names must be at least 4 characters long.',
+        'times',
+      )
+      return
+    }
+
+    if (passwords?.find(p => p.name === name)) {
+      Toast.show('This name is already in use', 'times')
       return
     }
 
     try {
-      const newPassword = await store.me.createAppPassword(name)
+      const newPassword = await createMutation.mutateAsync({name})
       if (newPassword) {
         setAppPassword(newPassword.password)
       } else {
-        Toast.show('Failed to create app password.')
+        Toast.show('Failed to create app password.', 'times')
         // TODO: better error handling (?)
       }
     } catch (e) {
-      Toast.show('Failed to create app password.')
+      Toast.show('Failed to create app password.', 'times')
       logger.error('Failed to create app password', {error: e})
     }
   }
@@ -119,15 +137,19 @@ export function Component({}: {}) {
       <View>
         {!appPassword ? (
           <Text type="lg" style={[pal.text]}>
-            Please enter a unique name for this App Password or use our randomly
-            generated one.
+            <Trans>
+              Please enter a unique name for this App Password or use our
+              randomly generated one.
+            </Trans>
           </Text>
         ) : (
           <Text type="lg" style={[pal.text]}>
-            <Text type="lg-bold" style={[pal.text]}>
-              Here is your app password.
-            </Text>{' '}
-            Use this to sign into the other app along with your handle.
+            <Text type="lg-bold" style={[pal.text, s.mr5]}>
+              <Trans>Here is your app password.</Trans>
+            </Text>
+            <Trans>
+              Use this to sign into the other app along with your handle.
+            </Trans>
           </Text>
         )}
         {!appPassword ? (
@@ -152,7 +174,7 @@ export function Component({}: {}) {
               returnKeyType="done"
               onEndEditing={createAppPassword}
               accessible={true}
-              accessibilityLabel="Name"
+              accessibilityLabel={_(msg`Name`)}
               accessibilityHint="Input name for app password"
             />
           </View>
@@ -161,13 +183,15 @@ export function Component({}: {}) {
             style={[pal.border, styles.passwordContainer, pal.btn]}
             onPress={onCopy}
             accessibilityRole="button"
-            accessibilityLabel="Copy"
+            accessibilityLabel={_(msg`Copy`)}
             accessibilityHint="Copies app password">
             <Text type="2xl-bold" style={[pal.text]}>
               {appPassword}
             </Text>
             {wasCopied ? (
-              <Text style={[pal.textLight]}>Copied</Text>
+              <Text style={[pal.textLight]}>
+                <Trans>Copied</Trans>
+              </Text>
             ) : (
               <FontAwesomeIcon
                 icon={['far', 'clone']}
@@ -180,14 +204,18 @@ export function Component({}: {}) {
       </View>
       {appPassword ? (
         <Text type="lg" style={[pal.textLight, s.mb10]}>
-          For security reasons, you won't be able to view this again. If you
-          lose this password, you'll need to generate a new one.
+          <Trans>
+            For security reasons, you won't be able to view this again. If you
+            lose this password, you'll need to generate a new one.
+          </Trans>
         </Text>
       ) : (
         <Text type="xs" style={[pal.textLight, s.mb10, s.mt2]}>
-          Can only contain letters, numbers, spaces, dashes, and underscores.
-          Must be at least 4 characters long, but no more than 32 characters
-          long.
+          <Trans>
+            Can only contain letters, numbers, spaces, dashes, and underscores.
+            Must be at least 4 characters long, but no more than 32 characters
+            long.
+          </Trans>
         </Text>
       )}
       <View style={styles.btnContainer}>
diff --git a/src/view/com/modals/AltImage.tsx b/src/view/com/modals/AltImage.tsx
index c084e84a3..80130f43a 100644
--- a/src/view/com/modals/AltImage.tsx
+++ b/src/view/com/modals/AltImage.tsx
@@ -17,9 +17,11 @@ import {MAX_ALT_TEXT} from 'lib/constants'
 import {useTheme} from 'lib/ThemeContext'
 import {Text} from '../util/text/Text'
 import LinearGradient from 'react-native-linear-gradient'
-import {useStores} from 'state/index'
 import {isAndroid, isWeb} from 'platform/detection'
 import {ImageModel} from 'state/models/media/image'
+import {useLingui} from '@lingui/react'
+import {Trans, msg} from '@lingui/macro'
+import {useModalControls} from '#/state/modals'
 
 export const snapPoints = ['fullscreen']
 
@@ -29,10 +31,11 @@ interface Props {
 
 export function Component({image}: Props) {
   const pal = usePalette('default')
-  const store = useStores()
   const theme = useTheme()
+  const {_} = useLingui()
   const [altText, setAltText] = useState(image.altText)
   const windim = useWindowDimensions()
+  const {closeModal} = useModalControls()
 
   const imageStyles = useMemo<ImageStyle>(() => {
     const maxWidth = isWeb ? 450 : windim.width
@@ -53,11 +56,11 @@ export function Component({image}: Props) {
 
   const onPressSave = useCallback(() => {
     image.setAltText(altText)
-    store.shell.closeModal()
-  }, [store, image, altText])
+    closeModal()
+  }, [closeModal, image, altText])
 
   const onPressCancel = () => {
-    store.shell.closeModal()
+    closeModal()
   }
 
   return (
@@ -90,7 +93,7 @@ export function Component({image}: Props) {
             placeholderTextColor={pal.colors.textLight}
             value={altText}
             onChangeText={text => setAltText(enforceLen(text, MAX_ALT_TEXT))}
-            accessibilityLabel="Image alt text"
+            accessibilityLabel={_(msg`Image alt text`)}
             accessibilityHint=""
             accessibilityLabelledBy="imageAltText"
             autoFocus
@@ -99,7 +102,7 @@ export function Component({image}: Props) {
             <TouchableOpacity
               testID="altTextImageSaveBtn"
               onPress={onPressSave}
-              accessibilityLabel="Save alt text"
+              accessibilityLabel={_(msg`Save alt text`)}
               accessibilityHint={`Saves alt text, which reads: ${altText}`}
               accessibilityRole="button">
               <LinearGradient
@@ -108,7 +111,7 @@ export function Component({image}: Props) {
                 end={{x: 1, y: 1}}
                 style={[styles.button]}>
                 <Text type="button-lg" style={[s.white, s.bold]}>
-                  Save
+                  <Trans>Save</Trans>
                 </Text>
               </LinearGradient>
             </TouchableOpacity>
@@ -116,12 +119,12 @@ export function Component({image}: Props) {
               testID="altTextImageCancelBtn"
               onPress={onPressCancel}
               accessibilityRole="button"
-              accessibilityLabel="Cancel add image alt text"
+              accessibilityLabel={_(msg`Cancel add image alt text`)}
               accessibilityHint=""
               onAccessibilityEscape={onPressCancel}>
               <View style={[styles.button]}>
                 <Text type="button-lg" style={[pal.textLight]}>
-                  Cancel
+                  <Trans>Cancel</Trans>
                 </Text>
               </View>
             </TouchableOpacity>
diff --git a/src/view/com/modals/BirthDateSettings.tsx b/src/view/com/modals/BirthDateSettings.tsx
index 6927ba8d2..c78f06ed4 100644
--- a/src/view/com/modals/BirthDateSettings.tsx
+++ b/src/view/com/modals/BirthDateSettings.tsx
@@ -5,41 +5,47 @@ import {
   TouchableOpacity,
   View,
 } from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {Text} from '../util/text/Text'
 import {DateInput} from '../util/forms/DateInput'
 import {ErrorMessage} from '../util/error/ErrorMessage'
-import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb} from 'platform/detection'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {cleanError} from 'lib/strings/errors'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
+import {
+  usePreferencesQuery,
+  usePreferencesSetBirthDateMutation,
+  UsePreferencesQueryResponse,
+} from '#/state/queries/preferences'
+import {logger} from '#/logger'
 
 export const snapPoints = ['50%']
 
-export const Component = observer(function Component({}: {}) {
+function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) {
   const pal = usePalette('default')
-  const store = useStores()
-  const [date, setDate] = useState<Date>(
-    store.preferences.birthDate || new Date(),
-  )
-  const [isProcessing, setIsProcessing] = useState<boolean>(false)
-  const [error, setError] = useState<string>('')
   const {isMobile} = useWebMediaQueries()
+  const {_} = useLingui()
+  const {
+    isPending,
+    isError,
+    error,
+    mutateAsync: setBirthDate,
+  } = usePreferencesSetBirthDateMutation()
+  const [date, setDate] = useState(preferences.birthDate || new Date())
+  const {closeModal} = useModalControls()
 
-  const onSave = async () => {
-    setError('')
-    setIsProcessing(true)
+  const onSave = React.useCallback(async () => {
     try {
-      await store.preferences.setBirthDate(date)
-      store.shell.closeModal()
+      await setBirthDate({birthDate: date})
+      closeModal()
     } catch (e) {
-      setError(cleanError(String(e)))
-    } finally {
-      setIsProcessing(false)
+      logger.error(`setBirthDate failed`, {error: e})
     }
-  }
+  }, [date, setBirthDate, closeModal])
 
   return (
     <View
@@ -47,12 +53,12 @@ export const Component = observer(function Component({}: {}) {
       style={[pal.view, styles.container, isMobile && {paddingHorizontal: 18}]}>
       <View style={styles.titleSection}>
         <Text type="title-lg" style={[pal.text, styles.title]}>
-          My Birthday
+          <Trans>My Birthday</Trans>
         </Text>
       </View>
 
       <Text type="lg" style={[pal.textLight, {marginBottom: 10}]}>
-        This information is not shared with other users.
+        <Trans>This information is not shared with other users.</Trans>
       </Text>
 
       <View>
@@ -63,18 +69,18 @@ export const Component = observer(function Component({}: {}) {
           buttonType="default-light"
           buttonStyle={[pal.border, styles.dateInputButton]}
           buttonLabelType="lg"
-          accessibilityLabel="Birthday"
+          accessibilityLabel={_(msg`Birthday`)}
           accessibilityHint="Enter your birth date"
           accessibilityLabelledBy="birthDate"
         />
       </View>
 
-      {error ? (
-        <ErrorMessage message={error} style={styles.error} />
+      {isError ? (
+        <ErrorMessage message={cleanError(error)} style={styles.error} />
       ) : undefined}
 
       <View style={[styles.btnContainer, pal.borderDark]}>
-        {isProcessing ? (
+        {isPending ? (
           <View style={styles.btn}>
             <ActivityIndicator color="#fff" />
           </View>
@@ -84,15 +90,27 @@ export const Component = observer(function Component({}: {}) {
             onPress={onSave}
             style={styles.btn}
             accessibilityRole="button"
-            accessibilityLabel="Save"
+            accessibilityLabel={_(msg`Save`)}
             accessibilityHint="">
-            <Text style={[s.white, s.bold, s.f18]}>Save</Text>
+            <Text style={[s.white, s.bold, s.f18]}>
+              <Trans>Save</Trans>
+            </Text>
           </TouchableOpacity>
         )}
       </View>
     </View>
   )
-})
+}
+
+export function Component({}: {}) {
+  const {data: preferences} = usePreferencesQuery()
+
+  return !preferences ? (
+    <ActivityIndicator />
+  ) : (
+    <Inner preferences={preferences} />
+  )
+}
 
 const styles = StyleSheet.create({
   container: {
diff --git a/src/view/com/modals/ChangeEmail.tsx b/src/view/com/modals/ChangeEmail.tsx
index 012570556..73ab33dd4 100644
--- a/src/view/com/modals/ChangeEmail.tsx
+++ b/src/view/com/modals/ChangeEmail.tsx
@@ -1,17 +1,19 @@
 import React, {useState} from 'react'
 import {ActivityIndicator, SafeAreaView, StyleSheet, View} from 'react-native'
 import {ScrollView, TextInput} from './util'
-import {observer} from 'mobx-react-lite'
 import {Text} from '../util/text/Text'
 import {Button} from '../util/forms/Button'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import * as Toast from '../util/Toast'
-import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb} from 'platform/detection'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {cleanError} from 'lib/strings/errors'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
+import {useSession, useSessionApi, getAgent} from '#/state/session'
 
 enum Stages {
   InputEmail,
@@ -21,32 +23,33 @@ enum Stages {
 
 export const snapPoints = ['90%']
 
-export const Component = observer(function Component({}: {}) {
+export function Component() {
   const pal = usePalette('default')
-  const store = useStores()
+  const {currentAccount} = useSession()
+  const {updateCurrentAccount} = useSessionApi()
+  const {_} = useLingui()
   const [stage, setStage] = useState<Stages>(Stages.InputEmail)
-  const [email, setEmail] = useState<string>(
-    store.session.currentSession?.email || '',
-  )
+  const [email, setEmail] = useState<string>(currentAccount?.email || '')
   const [confirmationCode, setConfirmationCode] = useState<string>('')
   const [isProcessing, setIsProcessing] = useState<boolean>(false)
   const [error, setError] = useState<string>('')
   const {isMobile} = useWebMediaQueries()
+  const {openModal, closeModal} = useModalControls()
 
   const onRequestChange = async () => {
-    if (email === store.session.currentSession?.email) {
+    if (email === currentAccount?.email) {
       setError('Enter your new email above')
       return
     }
     setError('')
     setIsProcessing(true)
     try {
-      const res = await store.agent.com.atproto.server.requestEmailUpdate()
+      const res = await getAgent().com.atproto.server.requestEmailUpdate()
       if (res.data.tokenRequired) {
         setStage(Stages.ConfirmCode)
       } else {
-        await store.agent.com.atproto.server.updateEmail({email: email.trim()})
-        store.session.updateLocalAccountData({
+        await getAgent().com.atproto.server.updateEmail({email: email.trim()})
+        updateCurrentAccount({
           email: email.trim(),
           emailConfirmed: false,
         })
@@ -60,7 +63,9 @@ export const Component = observer(function Component({}: {}) {
       // you can remove this any time after Oct2023
       // -prf
       if (err === 'email must be confirmed (temporary)') {
-        err = `Please confirm your email before changing it. This is a temporary requirement while email-updating tools are added, and it will soon be removed.`
+        err = _(
+          msg`Please confirm your email before changing it. This is a temporary requirement while email-updating tools are added, and it will soon be removed.`,
+        )
       }
       setError(err)
     } finally {
@@ -72,11 +77,11 @@ export const Component = observer(function Component({}: {}) {
     setError('')
     setIsProcessing(true)
     try {
-      await store.agent.com.atproto.server.updateEmail({
+      await getAgent().com.atproto.server.updateEmail({
         email: email.trim(),
         token: confirmationCode.trim(),
       })
-      store.session.updateLocalAccountData({
+      updateCurrentAccount({
         email: email.trim(),
         emailConfirmed: false,
       })
@@ -90,8 +95,8 @@ export const Component = observer(function Component({}: {}) {
   }
 
   const onVerify = async () => {
-    store.shell.closeModal()
-    store.shell.openModal({name: 'verify-email'})
+    closeModal()
+    openModal({name: 'verify-email'})
   }
 
   return (
@@ -101,26 +106,26 @@ export const Component = observer(function Component({}: {}) {
         style={[s.flex1, isMobile && {paddingHorizontal: 18}]}>
         <View style={styles.titleSection}>
           <Text type="title-lg" style={[pal.text, styles.title]}>
-            {stage === Stages.InputEmail ? 'Change Your Email' : ''}
-            {stage === Stages.ConfirmCode ? 'Security Step Required' : ''}
-            {stage === Stages.Done ? 'Email Updated' : ''}
+            {stage === Stages.InputEmail ? _(msg`Change Your Email`) : ''}
+            {stage === Stages.ConfirmCode ? _(msg`Security Step Required`) : ''}
+            {stage === Stages.Done ? _(msg`Email Updated`) : ''}
           </Text>
         </View>
 
         <Text type="lg" style={[pal.textLight, {marginBottom: 10}]}>
           {stage === Stages.InputEmail ? (
-            <>Enter your new email address below.</>
+            <Trans>Enter your new email address below.</Trans>
           ) : stage === Stages.ConfirmCode ? (
-            <>
+            <Trans>
               An email has been sent to your previous address,{' '}
-              {store.session.currentSession?.email || ''}. It includes a
-              confirmation code which you can enter below.
-            </>
+              {currentAccount?.email || ''}. It includes a confirmation code
+              which you can enter below.
+            </Trans>
           ) : (
-            <>
+            <Trans>
               Your email has been updated but not verified. As a next step,
               please verify your new email.
-            </>
+            </Trans>
           )}
         </Text>
 
@@ -133,7 +138,7 @@ export const Component = observer(function Component({}: {}) {
             value={email}
             onChangeText={setEmail}
             accessible={true}
-            accessibilityLabel="Email"
+            accessibilityLabel={_(msg`Email`)}
             accessibilityHint=""
             autoCapitalize="none"
             autoComplete="email"
@@ -149,7 +154,7 @@ export const Component = observer(function Component({}: {}) {
             value={confirmationCode}
             onChangeText={setConfirmationCode}
             accessible={true}
-            accessibilityLabel="Confirmation code"
+            accessibilityLabel={_(msg`Confirmation code`)}
             accessibilityHint=""
             autoCapitalize="none"
             autoComplete="off"
@@ -173,9 +178,9 @@ export const Component = observer(function Component({}: {}) {
                   testID="requestChangeBtn"
                   type="primary"
                   onPress={onRequestChange}
-                  accessibilityLabel="Request Change"
+                  accessibilityLabel={_(msg`Request Change`)}
                   accessibilityHint=""
-                  label="Request Change"
+                  label={_(msg`Request Change`)}
                   labelContainerStyle={{justifyContent: 'center', padding: 4}}
                   labelStyle={[s.f18]}
                 />
@@ -185,9 +190,9 @@ export const Component = observer(function Component({}: {}) {
                   testID="confirmBtn"
                   type="primary"
                   onPress={onConfirm}
-                  accessibilityLabel="Confirm Change"
+                  accessibilityLabel={_(msg`Confirm Change`)}
                   accessibilityHint=""
-                  label="Confirm Change"
+                  label={_(msg`Confirm Change`)}
                   labelContainerStyle={{justifyContent: 'center', padding: 4}}
                   labelStyle={[s.f18]}
                 />
@@ -197,9 +202,9 @@ export const Component = observer(function Component({}: {}) {
                   testID="verifyBtn"
                   type="primary"
                   onPress={onVerify}
-                  accessibilityLabel="Verify New Email"
+                  accessibilityLabel={_(msg`Verify New Email`)}
                   accessibilityHint=""
-                  label="Verify New Email"
+                  label={_(msg`Verify New Email`)}
                   labelContainerStyle={{justifyContent: 'center', padding: 4}}
                   labelStyle={[s.f18]}
                 />
@@ -207,10 +212,12 @@ export const Component = observer(function Component({}: {}) {
               <Button
                 testID="cancelBtn"
                 type="default"
-                onPress={() => store.shell.closeModal()}
-                accessibilityLabel="Cancel"
+                onPress={() => {
+                  closeModal()
+                }}
+                accessibilityLabel={_(msg`Cancel`)}
                 accessibilityHint=""
-                label="Cancel"
+                label={_(msg`Cancel`)}
                 labelContainerStyle={{justifyContent: 'center', padding: 4}}
                 labelStyle={[s.f18]}
               />
@@ -220,7 +227,7 @@ export const Component = observer(function Component({}: {}) {
       </ScrollView>
     </SafeAreaView>
   )
-})
+}
 
 const styles = StyleSheet.create({
   titleSection: {
diff --git a/src/view/com/modals/ChangeHandle.tsx b/src/view/com/modals/ChangeHandle.tsx
index c54c1c043..03516d35a 100644
--- a/src/view/com/modals/ChangeHandle.tsx
+++ b/src/view/com/modals/ChangeHandle.tsx
@@ -1,5 +1,6 @@
 import React, {useState} from 'react'
 import Clipboard from '@react-native-clipboard/clipboard'
+import {ComAtprotoServerDescribeServer} from '@atproto/api'
 import * as Toast from '../util/Toast'
 import {
   ActivityIndicator,
@@ -13,8 +14,6 @@ import {Text} from '../util/text/Text'
 import {Button} from '../util/forms/Button'
 import {SelectableBtn} from '../util/forms/SelectableBtn'
 import {ErrorMessage} from '../util/error/ErrorMessage'
-import {useStores} from 'state/index'
-import {ServiceDescription} from 'state/models/session'
 import {s} from 'lib/styles'
 import {createFullHandle, makeValidHandle} from 'lib/strings/handles'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -22,75 +21,74 @@ import {useTheme} from 'lib/ThemeContext'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {cleanError} from 'lib/strings/errors'
 import {logger} from '#/logger'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
+import {useServiceQuery} from '#/state/queries/service'
+import {useUpdateHandleMutation, useFetchDid} from '#/state/queries/handle'
+import {
+  useSession,
+  useSessionApi,
+  SessionAccount,
+  getAgent,
+} from '#/state/session'
 
 export const snapPoints = ['100%']
 
-export function Component({onChanged}: {onChanged: () => void}) {
-  const store = useStores()
-  const [error, setError] = useState<string>('')
+export type Props = {onChanged: () => void}
+
+export function Component(props: Props) {
+  const {currentAccount} = useSession()
+  const {
+    isLoading,
+    data: serviceInfo,
+    error: serviceInfoError,
+  } = useServiceQuery(getAgent().service.toString())
+
+  return isLoading || !currentAccount ? (
+    <View style={{padding: 18}}>
+      <ActivityIndicator />
+    </View>
+  ) : serviceInfoError || !serviceInfo ? (
+    <ErrorMessage message={cleanError(serviceInfoError)} />
+  ) : (
+    <Inner
+      {...props}
+      currentAccount={currentAccount}
+      serviceInfo={serviceInfo}
+    />
+  )
+}
+
+export function Inner({
+  currentAccount,
+  serviceInfo,
+  onChanged,
+}: Props & {
+  currentAccount: SessionAccount
+  serviceInfo: ComAtprotoServerDescribeServer.OutputSchema
+}) {
+  const {_} = useLingui()
   const pal = usePalette('default')
   const {track} = useAnalytics()
+  const {updateCurrentAccount} = useSessionApi()
+  const {closeModal} = useModalControls()
+  const {mutateAsync: updateHandle, isPending: isUpdateHandlePending} =
+    useUpdateHandleMutation()
+
+  const [error, setError] = useState<string>('')
 
-  const [isProcessing, setProcessing] = useState<boolean>(false)
-  const [retryDescribeTrigger, setRetryDescribeTrigger] = React.useState<any>(
-    {},
-  )
-  const [serviceDescription, setServiceDescription] = React.useState<
-    ServiceDescription | undefined
-  >(undefined)
-  const [userDomain, setUserDomain] = React.useState<string>('')
   const [isCustom, setCustom] = React.useState<boolean>(false)
   const [handle, setHandle] = React.useState<string>('')
   const [canSave, setCanSave] = React.useState<boolean>(false)
 
-  // init
-  // =
-  React.useEffect(() => {
-    let aborted = false
-    setError('')
-    setServiceDescription(undefined)
-    setProcessing(true)
-
-    // load the service description so we can properly provision handles
-    store.session.describeService(String(store.agent.service)).then(
-      desc => {
-        if (aborted) {
-          return
-        }
-        setServiceDescription(desc)
-        setUserDomain(desc.availableUserDomains[0])
-        setProcessing(false)
-      },
-      err => {
-        if (aborted) {
-          return
-        }
-        setProcessing(false)
-        logger.warn(
-          `Failed to fetch service description for ${String(
-            store.agent.service,
-          )}`,
-          {error: err},
-        )
-        setError(
-          'Unable to contact your service. Please check your Internet connection.',
-        )
-      },
-    )
-    return () => {
-      aborted = true
-    }
-  }, [store.agent.service, store.session, retryDescribeTrigger])
+  const userDomain = serviceInfo.availableUserDomains?.[0]
 
   // events
   // =
   const onPressCancel = React.useCallback(() => {
-    store.shell.closeModal()
-  }, [store])
-  const onPressRetryConnect = React.useCallback(
-    () => setRetryDescribeTrigger({}),
-    [setRetryDescribeTrigger],
-  )
+    closeModal()
+  }, [closeModal])
   const onToggleCustom = React.useCallback(() => {
     // toggle between a provided domain vs a custom one
     setHandle('')
@@ -101,32 +99,42 @@ export function Component({onChanged}: {onChanged: () => void}) {
     )
   }, [setCustom, isCustom, track])
   const onPressSave = React.useCallback(async () => {
-    setError('')
-    setProcessing(true)
+    if (!userDomain) {
+      logger.error(`ChangeHandle: userDomain is undefined`, {
+        service: serviceInfo,
+      })
+      setError(`The service you've selected has no domains configured.`)
+      return
+    }
+
     try {
       track('EditHandle:SetNewHandle')
       const newHandle = isCustom ? handle : createFullHandle(handle, userDomain)
       logger.debug(`Updating handle to ${newHandle}`)
-      await store.agent.updateHandle({
+      await updateHandle({
+        handle: newHandle,
+      })
+      updateCurrentAccount({
         handle: newHandle,
       })
-      store.shell.closeModal()
+      closeModal()
       onChanged()
     } catch (err: any) {
       setError(cleanError(err))
       logger.error('Failed to update handle', {handle, error: err})
     } finally {
-      setProcessing(false)
     }
   }, [
     setError,
-    setProcessing,
     handle,
     userDomain,
-    store,
     isCustom,
     onChanged,
     track,
+    closeModal,
+    updateCurrentAccount,
+    updateHandle,
+    serviceInfo,
   ])
 
   // rendering
@@ -138,7 +146,7 @@ export function Component({onChanged}: {onChanged: () => void}) {
           <TouchableOpacity
             onPress={onPressCancel}
             accessibilityRole="button"
-            accessibilityLabel="Cancel change handle"
+            accessibilityLabel={_(msg`Cancel change handle`)}
             accessibilityHint="Exits handle change process"
             onAccessibilityEscape={onPressCancel}>
             <Text type="lg" style={pal.textLight}>
@@ -150,30 +158,19 @@ export function Component({onChanged}: {onChanged: () => void}) {
           type="2xl-bold"
           style={[styles.titleMiddle, pal.text]}
           numberOfLines={1}>
-          Change Handle
+          <Trans>Change Handle</Trans>
         </Text>
         <View style={styles.titleRight}>
-          {isProcessing ? (
+          {isUpdateHandlePending ? (
             <ActivityIndicator />
-          ) : error && !serviceDescription ? (
-            <TouchableOpacity
-              testID="retryConnectButton"
-              onPress={onPressRetryConnect}
-              accessibilityRole="button"
-              accessibilityLabel="Retry change handle"
-              accessibilityHint={`Retries handle change to ${handle}`}>
-              <Text type="xl-bold" style={[pal.link, s.pr5]}>
-                Retry
-              </Text>
-            </TouchableOpacity>
           ) : canSave ? (
             <TouchableOpacity
               onPress={onPressSave}
               accessibilityRole="button"
-              accessibilityLabel="Save handle change"
+              accessibilityLabel={_(msg`Save handle change`)}
               accessibilityHint={`Saves handle change to ${handle}`}>
               <Text type="2xl-medium" style={pal.link}>
-                Save
+                <Trans>Save</Trans>
               </Text>
             </TouchableOpacity>
           ) : undefined}
@@ -188,8 +185,9 @@ export function Component({onChanged}: {onChanged: () => void}) {
 
         {isCustom ? (
           <CustomHandleForm
+            currentAccount={currentAccount}
             handle={handle}
-            isProcessing={isProcessing}
+            isProcessing={isUpdateHandlePending}
             canSave={canSave}
             onToggleCustom={onToggleCustom}
             setHandle={setHandle}
@@ -200,7 +198,7 @@ export function Component({onChanged}: {onChanged: () => void}) {
           <ProvidedHandleForm
             handle={handle}
             userDomain={userDomain}
-            isProcessing={isProcessing}
+            isProcessing={isUpdateHandlePending}
             onToggleCustom={onToggleCustom}
             setHandle={setHandle}
             setCanSave={setCanSave}
@@ -231,6 +229,7 @@ function ProvidedHandleForm({
 }) {
   const pal = usePalette('default')
   const theme = useTheme()
+  const {_} = useLingui()
 
   // events
   // =
@@ -263,12 +262,12 @@ function ProvidedHandleForm({
           onChangeText={onChangeHandle}
           editable={!isProcessing}
           accessible={true}
-          accessibilityLabel="Handle"
+          accessibilityLabel={_(msg`Handle`)}
           accessibilityHint="Sets Bluesky username"
         />
       </View>
       <Text type="md" style={[pal.textLight, s.pl10, s.pt10]}>
-        Your full handle will be{' '}
+        <Trans>Your full handle will be </Trans>
         <Text type="md-bold" style={pal.textLight}>
           @{createFullHandle(handle, userDomain)}
         </Text>
@@ -277,9 +276,9 @@ function ProvidedHandleForm({
         onPress={onToggleCustom}
         accessibilityRole="button"
         accessibilityHint="Hosting provider"
-        accessibilityLabel="Opens modal for using custom domain">
+        accessibilityLabel={_(msg`Opens modal for using custom domain`)}>
         <Text type="md-medium" style={[pal.link, s.pl10, s.pt5]}>
-          I have my own domain
+          <Trans>I have my own domain</Trans>
         </Text>
       </TouchableOpacity>
     </>
@@ -290,6 +289,7 @@ function ProvidedHandleForm({
  * The form for using a custom domain
  */
 function CustomHandleForm({
+  currentAccount,
   handle,
   canSave,
   isProcessing,
@@ -298,6 +298,7 @@ function CustomHandleForm({
   onPressSave,
   setCanSave,
 }: {
+  currentAccount: SessionAccount
   handle: string
   canSave: boolean
   isProcessing: boolean
@@ -306,20 +307,23 @@ function CustomHandleForm({
   onPressSave: () => void
   setCanSave: (v: boolean) => void
 }) {
-  const store = useStores()
   const pal = usePalette('default')
   const palSecondary = usePalette('secondary')
   const palError = usePalette('error')
   const theme = useTheme()
+  const {_} = useLingui()
   const [isVerifying, setIsVerifying] = React.useState(false)
   const [error, setError] = React.useState<string>('')
   const [isDNSForm, setDNSForm] = React.useState<boolean>(true)
+  const fetchDid = useFetchDid()
   // events
   // =
   const onPressCopy = React.useCallback(() => {
-    Clipboard.setString(isDNSForm ? `did=${store.me.did}` : store.me.did)
+    Clipboard.setString(
+      isDNSForm ? `did=${currentAccount.did}` : currentAccount.did,
+    )
     Toast.show('Copied to clipboard')
-  }, [store.me.did, isDNSForm])
+  }, [currentAccount, isDNSForm])
   const onChangeHandle = React.useCallback(
     (v: string) => {
       setHandle(v)
@@ -334,13 +338,11 @@ function CustomHandleForm({
     try {
       setIsVerifying(true)
       setError('')
-      const res = await store.agent.com.atproto.identity.resolveHandle({
-        handle,
-      })
-      if (res.data.did === store.me.did) {
+      const did = await fetchDid(handle)
+      if (did === currentAccount.did) {
         setCanSave(true)
       } else {
-        setError(`Incorrect DID returned (got ${res.data.did})`)
+        setError(`Incorrect DID returned (got ${did})`)
       }
     } catch (err: any) {
       setError(cleanError(err))
@@ -350,13 +352,13 @@ function CustomHandleForm({
     }
   }, [
     handle,
-    store.me.did,
+    currentAccount,
     setIsVerifying,
     setCanSave,
     setError,
     canSave,
     onPressSave,
-    store.agent,
+    fetchDid,
   ])
 
   // rendering
@@ -364,7 +366,7 @@ function CustomHandleForm({
   return (
     <>
       <Text type="md" style={[pal.text, s.pb5, s.pl5]} nativeID="customDomain">
-        Enter the domain you want to use
+        <Trans>Enter the domain you want to use</Trans>
       </Text>
       <View style={[pal.btn, styles.textInputWrapper]}>
         <FontAwesomeIcon
@@ -382,7 +384,7 @@ function CustomHandleForm({
           onChangeText={onChangeHandle}
           editable={!isProcessing}
           accessibilityLabelledBy="customDomain"
-          accessibilityLabel="Custom domain"
+          accessibilityLabel={_(msg`Custom domain`)}
           accessibilityHint="Input your preferred hosting provider"
         />
       </View>
@@ -410,7 +412,7 @@ function CustomHandleForm({
       {isDNSForm ? (
         <>
           <Text type="md" style={[pal.text, s.pb5, s.pl5]}>
-            Add the following DNS record to your domain:
+            <Trans>Add the following DNS record to your domain:</Trans>
           </Text>
           <View style={[styles.dnsTable, pal.btn]}>
             <Text type="md-medium" style={[styles.dnsLabel, pal.text]}>
@@ -434,7 +436,7 @@ function CustomHandleForm({
             </Text>
             <View style={[styles.dnsValue]}>
               <Text type="mono" style={[styles.monoText, pal.text]}>
-                did={store.me.did}
+                did={currentAccount.did}
               </Text>
             </View>
           </View>
@@ -448,7 +450,7 @@ function CustomHandleForm({
       ) : (
         <>
           <Text type="md" style={[pal.text, s.pb5, s.pl5]}>
-            Upload a text file to:
+            <Trans>Upload a text file to:</Trans>
           </Text>
           <View style={[styles.valueContainer, pal.btn]}>
             <View style={[styles.dnsValue]}>
@@ -464,7 +466,7 @@ function CustomHandleForm({
           <View style={[styles.valueContainer, pal.btn]}>
             <View style={[styles.dnsValue]}>
               <Text type="mono" style={[styles.monoText, pal.text]}>
-                {store.me.did}
+                {currentAccount.did}
               </Text>
             </View>
           </View>
@@ -480,7 +482,7 @@ function CustomHandleForm({
       {canSave === true && (
         <View style={[styles.message, palSecondary.view]}>
           <Text type="md-medium" style={palSecondary.text}>
-            Domain verified!
+            <Trans>Domain verified!</Trans>
           </Text>
         </View>
       )}
@@ -508,7 +510,7 @@ function CustomHandleForm({
       <View style={styles.spacer} />
       <TouchableOpacity
         onPress={onToggleCustom}
-        accessibilityLabel="Use default provider"
+        accessibilityLabel={_(msg`Use default provider`)}
         accessibilityHint="Use bsky.social as hosting provider">
         <Text type="md-medium" style={[pal.link, s.pl10, s.pt5]}>
           Nevermind, create a handle for me
diff --git a/src/view/com/modals/Confirm.tsx b/src/view/com/modals/Confirm.tsx
index c1324b1cb..5e869f396 100644
--- a/src/view/com/modals/Confirm.tsx
+++ b/src/view/com/modals/Confirm.tsx
@@ -6,13 +6,15 @@ import {
   View,
 } from 'react-native'
 import {Text} from '../util/text/Text'
-import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import {cleanError} from 'lib/strings/errors'
 import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb} from 'platform/detection'
-import type {ConfirmModal} from 'state/models/ui/shell'
+import {useLingui} from '@lingui/react'
+import {msg} from '@lingui/macro'
+import type {ConfirmModal} from '#/state/modals'
+import {useModalControls} from '#/state/modals'
 
 export const snapPoints = ['50%']
 
@@ -26,7 +28,8 @@ export function Component({
   cancelBtnText,
 }: ConfirmModal) {
   const pal = usePalette('default')
-  const store = useStores()
+  const {_} = useLingui()
+  const {closeModal} = useModalControls()
   const [isProcessing, setIsProcessing] = useState<boolean>(false)
   const [error, setError] = useState<string>('')
   const onPress = async () => {
@@ -34,7 +37,7 @@ export function Component({
     setIsProcessing(true)
     try {
       await onPressConfirm()
-      store.shell.closeModal()
+      closeModal()
       return
     } catch (e: any) {
       setError(cleanError(e))
@@ -69,7 +72,7 @@ export function Component({
           onPress={onPress}
           style={[styles.btn, confirmBtnStyle]}
           accessibilityRole="button"
-          accessibilityLabel="Confirm"
+          accessibilityLabel={_(msg`Confirm`)}
           accessibilityHint="">
           <Text style={[s.white, s.bold, s.f18]}>
             {confirmBtnText ?? 'Confirm'}
@@ -82,7 +85,7 @@ export function Component({
           onPress={onPressCancel}
           style={[styles.btnCancel, s.mt10]}
           accessibilityRole="button"
-          accessibilityLabel="Cancel"
+          accessibilityLabel={_(msg`Cancel`)}
           accessibilityHint="">
           <Text type="button-lg" style={pal.textLight}>
             {cancelBtnText ?? 'Cancel'}
diff --git a/src/view/com/modals/ContentFilteringSettings.tsx b/src/view/com/modals/ContentFilteringSettings.tsx
index 9075d0272..8b42e1b1d 100644
--- a/src/view/com/modals/ContentFilteringSettings.tsx
+++ b/src/view/com/modals/ContentFilteringSettings.tsx
@@ -1,214 +1,228 @@
 import React from 'react'
+import {LabelPreference} from '@atproto/api'
 import {StyleSheet, Pressable, View} from 'react-native'
 import LinearGradient from 'react-native-linear-gradient'
-import {observer} from 'mobx-react-lite'
 import {ScrollView} from './util'
-import {useStores} from 'state/index'
-import {LabelPreference} from 'state/models/ui/preferences'
 import {s, colors, gradients} from 'lib/styles'
 import {Text} from '../util/text/Text'
 import {TextLink} from '../util/Link'
 import {ToggleButton} from '../util/forms/ToggleButton'
 import {Button} from '../util/forms/Button'
 import {usePalette} from 'lib/hooks/usePalette'
-import {CONFIGURABLE_LABEL_GROUPS} from 'lib/labeling/const'
 import {isIOS} from 'platform/detection'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import * as Toast from '../util/Toast'
 import {logger} from '#/logger'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
+import {
+  usePreferencesQuery,
+  usePreferencesSetContentLabelMutation,
+  usePreferencesSetAdultContentMutation,
+  ConfigurableLabelGroup,
+  CONFIGURABLE_LABEL_GROUPS,
+  UsePreferencesQueryResponse,
+} from '#/state/queries/preferences'
 
 export const snapPoints = ['90%']
 
-export const Component = observer(
-  function ContentFilteringSettingsImpl({}: {}) {
-    const store = useStores()
-    const {isMobile} = useWebMediaQueries()
-    const pal = usePalette('default')
+export function Component({}: {}) {
+  const {isMobile} = useWebMediaQueries()
+  const pal = usePalette('default')
+  const {_} = useLingui()
+  const {closeModal} = useModalControls()
+  const {data: preferences} = usePreferencesQuery()
 
-    React.useEffect(() => {
-      store.preferences.sync()
-    }, [store])
+  const onPressDone = React.useCallback(() => {
+    closeModal()
+  }, [closeModal])
 
-    const onPressDone = React.useCallback(() => {
-      store.shell.closeModal()
-    }, [store])
+  return (
+    <View testID="contentFilteringModal" style={[pal.view, styles.container]}>
+      <Text style={[pal.text, styles.title]}>
+        <Trans>Content Filtering</Trans>
+      </Text>
 
-    return (
-      <View testID="contentFilteringModal" style={[pal.view, styles.container]}>
-        <Text style={[pal.text, styles.title]}>Content Filtering</Text>
-        <ScrollView style={styles.scrollContainer}>
-          <AdultContentEnabledPref />
-          <ContentLabelPref
-            group="nsfw"
-            disabled={!store.preferences.adultContentEnabled}
-          />
-          <ContentLabelPref
-            group="nudity"
-            disabled={!store.preferences.adultContentEnabled}
-          />
-          <ContentLabelPref
-            group="suggestive"
-            disabled={!store.preferences.adultContentEnabled}
-          />
-          <ContentLabelPref
-            group="gore"
-            disabled={!store.preferences.adultContentEnabled}
-          />
-          <ContentLabelPref group="hate" />
-          <ContentLabelPref group="spam" />
-          <ContentLabelPref group="impersonation" />
-          <View style={{height: isMobile ? 60 : 0}} />
-        </ScrollView>
-        <View
-          style={[
-            styles.btnContainer,
-            isMobile && styles.btnContainerMobile,
-            pal.borderDark,
-          ]}>
-          <Pressable
-            testID="sendReportBtn"
-            onPress={onPressDone}
-            accessibilityRole="button"
-            accessibilityLabel="Done"
-            accessibilityHint="">
-            <LinearGradient
-              colors={[gradients.blueLight.start, gradients.blueLight.end]}
-              start={{x: 0, y: 0}}
-              end={{x: 1, y: 1}}
-              style={[styles.btn]}>
-              <Text style={[s.white, s.bold, s.f18]}>Done</Text>
-            </LinearGradient>
-          </Pressable>
-        </View>
+      <ScrollView style={styles.scrollContainer}>
+        <AdultContentEnabledPref />
+        <ContentLabelPref
+          preferences={preferences}
+          labelGroup="nsfw"
+          disabled={!preferences?.adultContentEnabled}
+        />
+        <ContentLabelPref
+          preferences={preferences}
+          labelGroup="nudity"
+          disabled={!preferences?.adultContentEnabled}
+        />
+        <ContentLabelPref
+          preferences={preferences}
+          labelGroup="suggestive"
+          disabled={!preferences?.adultContentEnabled}
+        />
+        <ContentLabelPref
+          preferences={preferences}
+          labelGroup="gore"
+          disabled={!preferences?.adultContentEnabled}
+        />
+        <ContentLabelPref preferences={preferences} labelGroup="hate" />
+        <ContentLabelPref preferences={preferences} labelGroup="spam" />
+        <ContentLabelPref
+          preferences={preferences}
+          labelGroup="impersonation"
+        />
+        <View style={{height: isMobile ? 60 : 0}} />
+      </ScrollView>
+
+      <View
+        style={[
+          styles.btnContainer,
+          isMobile && styles.btnContainerMobile,
+          pal.borderDark,
+        ]}>
+        <Pressable
+          testID="sendReportBtn"
+          onPress={onPressDone}
+          accessibilityRole="button"
+          accessibilityLabel={_(msg`Done`)}
+          accessibilityHint="">
+          <LinearGradient
+            colors={[gradients.blueLight.start, gradients.blueLight.end]}
+            start={{x: 0, y: 0}}
+            end={{x: 1, y: 1}}
+            style={[styles.btn]}>
+            <Text style={[s.white, s.bold, s.f18]}>
+              <Trans>Done</Trans>
+            </Text>
+          </LinearGradient>
+        </Pressable>
       </View>
-    )
-  },
-)
+    </View>
+  )
+}
 
-const AdultContentEnabledPref = observer(
-  function AdultContentEnabledPrefImpl() {
-    const store = useStores()
-    const pal = usePalette('default')
+function AdultContentEnabledPref() {
+  const pal = usePalette('default')
+  const {data: preferences} = usePreferencesQuery()
+  const {mutate, variables} = usePreferencesSetAdultContentMutation()
+  const {openModal} = useModalControls()
 
-    const onSetAge = () => store.shell.openModal({name: 'birth-date-settings'})
+  const onSetAge = React.useCallback(
+    () => openModal({name: 'birth-date-settings'}),
+    [openModal],
+  )
 
-    const onToggleAdultContent = async () => {
-      if (isIOS) {
-        return
-      }
-      try {
-        await store.preferences.setAdultContentEnabled(
-          !store.preferences.adultContentEnabled,
-        )
-      } catch (e) {
-        Toast.show(
-          'There was an issue syncing your preferences with the server',
-        )
-        logger.error('Failed to update preferences with server', {error: e})
-      }
+  const onToggleAdultContent = React.useCallback(async () => {
+    if (isIOS) return
+
+    try {
+      mutate({
+        enabled: !(variables?.enabled ?? preferences?.adultContentEnabled),
+      })
+    } catch (e) {
+      Toast.show('There was an issue syncing your preferences with the server')
+      logger.error('Failed to update preferences with server', {error: e})
     }
+  }, [variables, preferences, mutate])
 
-    return (
-      <View style={s.mb10}>
-        {isIOS ? (
-          store.preferences.adultContentEnabled ? null : (
-            <Text type="md" style={pal.textLight}>
-              Adult content can only be enabled via the Web at{' '}
-              <TextLink
-                style={pal.link}
-                href="https://bsky.app"
-                text="bsky.app"
-              />
-              .
-            </Text>
-          )
-        ) : typeof store.preferences.birthDate === 'undefined' ? (
-          <View style={[pal.viewLight, styles.agePrompt]}>
-            <Text type="md" style={[pal.text, {flex: 1}]}>
-              Confirm your age to enable adult content.
-            </Text>
-            <Button type="primary" label="Set Age" onPress={onSetAge} />
-          </View>
-        ) : (store.preferences.userAge || 0) >= 18 ? (
-          <ToggleButton
-            type="default-light"
-            label="Enable Adult Content"
-            isSelected={store.preferences.adultContentEnabled}
-            onPress={onToggleAdultContent}
-            style={styles.toggleBtn}
-          />
-        ) : (
-          <View style={[pal.viewLight, styles.agePrompt]}>
-            <Text type="md" style={[pal.text, {flex: 1}]}>
-              You must be 18 or older to enable adult content.
-            </Text>
-            <Button type="primary" label="Set Age" onPress={onSetAge} />
-          </View>
-        )}
-      </View>
-    )
-  },
-)
+  return (
+    <View style={s.mb10}>
+      {isIOS ? (
+        preferences?.adultContentEnabled ? null : (
+          <Text type="md" style={pal.textLight}>
+            Adult content can only be enabled via the Web at{' '}
+            <TextLink
+              style={pal.link}
+              href="https://bsky.app"
+              text="bsky.app"
+            />
+            .
+          </Text>
+        )
+      ) : typeof preferences?.birthDate === 'undefined' ? (
+        <View style={[pal.viewLight, styles.agePrompt]}>
+          <Text type="md" style={[pal.text, {flex: 1}]}>
+            Confirm your age to enable adult content.
+          </Text>
+          <Button type="primary" label="Set Age" onPress={onSetAge} />
+        </View>
+      ) : (preferences.userAge || 0) >= 18 ? (
+        <ToggleButton
+          type="default-light"
+          label="Enable Adult Content"
+          isSelected={variables?.enabled ?? preferences?.adultContentEnabled}
+          onPress={onToggleAdultContent}
+          style={styles.toggleBtn}
+        />
+      ) : (
+        <View style={[pal.viewLight, styles.agePrompt]}>
+          <Text type="md" style={[pal.text, {flex: 1}]}>
+            You must be 18 or older to enable adult content.
+          </Text>
+          <Button type="primary" label="Set Age" onPress={onSetAge} />
+        </View>
+      )}
+    </View>
+  )
+}
 
 // TODO: Refactor this component to pass labels down to each tab
-const ContentLabelPref = observer(function ContentLabelPrefImpl({
-  group,
+function ContentLabelPref({
+  preferences,
+  labelGroup,
   disabled,
 }: {
-  group: keyof typeof CONFIGURABLE_LABEL_GROUPS
+  preferences?: UsePreferencesQueryResponse
+  labelGroup: ConfigurableLabelGroup
   disabled?: boolean
 }) {
-  const store = useStores()
   const pal = usePalette('default')
+  const visibility = preferences?.contentLabels?.[labelGroup]
+  const {mutate, variables} = usePreferencesSetContentLabelMutation()
 
   const onChange = React.useCallback(
-    async (v: LabelPreference) => {
-      try {
-        await store.preferences.setContentLabelPref(group, v)
-      } catch (e) {
-        Toast.show(
-          'There was an issue syncing your preferences with the server',
-        )
-        logger.error('Failed to update preferences with server', {error: e})
-      }
+    (vis: LabelPreference) => {
+      mutate({labelGroup, visibility: vis})
     },
-    [store, group],
+    [mutate, labelGroup],
   )
 
   return (
     <View style={[styles.contentLabelPref, pal.border]}>
       <View style={s.flex1}>
         <Text type="md-medium" style={[pal.text]}>
-          {CONFIGURABLE_LABEL_GROUPS[group].title}
+          {CONFIGURABLE_LABEL_GROUPS[labelGroup].title}
         </Text>
-        {typeof CONFIGURABLE_LABEL_GROUPS[group].subtitle === 'string' && (
+        {typeof CONFIGURABLE_LABEL_GROUPS[labelGroup].subtitle === 'string' && (
           <Text type="sm" style={[pal.textLight]}>
-            {CONFIGURABLE_LABEL_GROUPS[group].subtitle}
+            {CONFIGURABLE_LABEL_GROUPS[labelGroup].subtitle}
           </Text>
         )}
       </View>
-      {disabled ? (
+
+      {disabled || !visibility ? (
         <Text type="sm-bold" style={pal.textLight}>
           Hide
         </Text>
       ) : (
         <SelectGroup
-          current={store.preferences.contentLabels[group]}
+          current={variables?.visibility || visibility}
           onChange={onChange}
-          group={group}
+          labelGroup={labelGroup}
         />
       )}
     </View>
   )
-})
+}
 
 interface SelectGroupProps {
   current: LabelPreference
   onChange: (v: LabelPreference) => void
-  group: keyof typeof CONFIGURABLE_LABEL_GROUPS
+  labelGroup: ConfigurableLabelGroup
 }
 
-function SelectGroup({current, onChange, group}: SelectGroupProps) {
+function SelectGroup({current, onChange, labelGroup}: SelectGroupProps) {
   return (
     <View style={styles.selectableBtns}>
       <SelectableBtn
@@ -217,14 +231,14 @@ function SelectGroup({current, onChange, group}: SelectGroupProps) {
         label="Hide"
         left
         onChange={onChange}
-        group={group}
+        labelGroup={labelGroup}
       />
       <SelectableBtn
         current={current}
         value="warn"
         label="Warn"
         onChange={onChange}
-        group={group}
+        labelGroup={labelGroup}
       />
       <SelectableBtn
         current={current}
@@ -232,7 +246,7 @@ function SelectGroup({current, onChange, group}: SelectGroupProps) {
         label="Show"
         right
         onChange={onChange}
-        group={group}
+        labelGroup={labelGroup}
       />
     </View>
   )
@@ -245,7 +259,7 @@ interface SelectableBtnProps {
   left?: boolean
   right?: boolean
   onChange: (v: LabelPreference) => void
-  group: keyof typeof CONFIGURABLE_LABEL_GROUPS
+  labelGroup: ConfigurableLabelGroup
 }
 
 function SelectableBtn({
@@ -255,7 +269,7 @@ function SelectableBtn({
   left,
   right,
   onChange,
-  group,
+  labelGroup,
 }: SelectableBtnProps) {
   const pal = usePalette('default')
   const palPrimary = usePalette('inverted')
@@ -271,7 +285,7 @@ function SelectableBtn({
       onPress={() => onChange(value)}
       accessibilityRole="button"
       accessibilityLabel={value}
-      accessibilityHint={`Set ${value} for ${group} content moderation policy`}>
+      accessibilityHint={`Set ${value} for ${labelGroup} content moderation policy`}>
       <Text style={current === value ? palPrimary.text : pal.text}>
         {label}
       </Text>
diff --git a/src/view/com/modals/CreateOrEditList.tsx b/src/view/com/modals/CreateOrEditList.tsx
index 1ea12695f..8d13cdf2f 100644
--- a/src/view/com/modals/CreateOrEditList.tsx
+++ b/src/view/com/modals/CreateOrEditList.tsx
@@ -1,5 +1,4 @@
 import React, {useState, useCallback, useMemo} from 'react'
-import * as Toast from '../util/Toast'
 import {
   ActivityIndicator,
   KeyboardAvoidingView,
@@ -9,12 +8,12 @@ import {
   TouchableOpacity,
   View,
 } from 'react-native'
+import {AppBskyGraphDefs} from '@atproto/api'
 import LinearGradient from 'react-native-linear-gradient'
 import {Image as RNImage} from 'react-native-image-crop-picker'
 import {Text} from '../util/text/Text'
 import {ErrorMessage} from '../util/error/ErrorMessage'
-import {useStores} from 'state/index'
-import {ListModel} from 'state/models/content/list'
+import * as Toast from '../util/Toast'
 import {s, colors, gradients} from 'lib/styles'
 import {enforceLen} from 'lib/strings/helpers'
 import {compressIfNeeded} from 'lib/media/manip'
@@ -24,6 +23,13 @@ import {useTheme} from 'lib/ThemeContext'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {cleanError, isNetworkError} from 'lib/strings/errors'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
+import {
+  useListCreateMutation,
+  useListMetadataMutation,
+} from '#/state/queries/list'
 
 const MAX_NAME = 64 // todo
 const MAX_DESCRIPTION = 300 // todo
@@ -37,18 +43,21 @@ export function Component({
 }: {
   purpose?: string
   onSave?: (uri: string) => void
-  list?: ListModel
+  list?: AppBskyGraphDefs.ListView
 }) {
-  const store = useStores()
+  const {closeModal} = useModalControls()
   const {isMobile} = useWebMediaQueries()
   const [error, setError] = useState<string>('')
   const pal = usePalette('default')
   const theme = useTheme()
   const {track} = useAnalytics()
+  const {_} = useLingui()
+  const listCreateMutation = useListCreateMutation()
+  const listMetadataMutation = useListMetadataMutation()
 
   const activePurpose = useMemo(() => {
-    if (list?.data?.purpose) {
-      return list.data.purpose
+    if (list?.purpose) {
+      return list.purpose
     }
     if (purpose) {
       return purpose
@@ -59,16 +68,16 @@ export function Component({
   const purposeLabel = isCurateList ? 'User' : 'Moderation'
 
   const [isProcessing, setProcessing] = useState<boolean>(false)
-  const [name, setName] = useState<string>(list?.data?.name || '')
+  const [name, setName] = useState<string>(list?.name || '')
   const [description, setDescription] = useState<string>(
-    list?.data?.description || '',
+    list?.description || '',
   )
-  const [avatar, setAvatar] = useState<string | undefined>(list?.data?.avatar)
+  const [avatar, setAvatar] = useState<string | undefined>(list?.avatar)
   const [newAvatar, setNewAvatar] = useState<RNImage | undefined | null>()
 
   const onPressCancel = useCallback(() => {
-    store.shell.closeModal()
-  }, [store])
+    closeModal()
+  }, [closeModal])
 
   const onSelectNewAvatar = useCallback(
     async (img: RNImage | null) => {
@@ -106,7 +115,8 @@ export function Component({
     }
     try {
       if (list) {
-        await list.updateMetadata({
+        await listMetadataMutation.mutateAsync({
+          uri: list.uri,
           name: nameTrimmed,
           description: description.trim(),
           avatar: newAvatar,
@@ -114,7 +124,7 @@ export function Component({
         Toast.show(`${purposeLabel} list updated`)
         onSave?.(list.uri)
       } else {
-        const res = await ListModel.createList(store, {
+        const res = await listCreateMutation.mutateAsync({
           purpose: activePurpose,
           name,
           description,
@@ -123,7 +133,7 @@ export function Component({
         Toast.show(`${purposeLabel} list created`)
         onSave?.(res.uri)
       }
-      store.shell.closeModal()
+      closeModal()
     } catch (e: any) {
       if (isNetworkError(e)) {
         setError(
@@ -140,7 +150,7 @@ export function Component({
     setError,
     error,
     onSave,
-    store,
+    closeModal,
     activePurpose,
     isCurateList,
     purposeLabel,
@@ -148,6 +158,8 @@ export function Component({
     description,
     newAvatar,
     list,
+    listMetadataMutation,
+    listCreateMutation,
   ])
 
   return (
@@ -161,14 +173,18 @@ export function Component({
         ]}
         testID="createOrEditListModal">
         <Text style={[styles.title, pal.text]}>
-          {list ? 'Edit' : 'New'} {purposeLabel} List
+          <Trans>
+            {list ? 'Edit' : 'New'} {purposeLabel} List
+          </Trans>
         </Text>
         {error !== '' && (
           <View style={styles.errorContainer}>
             <ErrorMessage message={error} />
           </View>
         )}
-        <Text style={[styles.label, pal.text]}>List Avatar</Text>
+        <Text style={[styles.label, pal.text]}>
+          <Trans>List Avatar</Trans>
+        </Text>
         <View style={[styles.avi, {borderColor: pal.colors.background}]}>
           <EditableUserAvatar
             type="list"
@@ -180,7 +196,7 @@ export function Component({
         <View style={styles.form}>
           <View>
             <Text style={[styles.label, pal.text]} nativeID="list-name">
-              List Name
+              <Trans>List Name</Trans>
             </Text>
             <TextInput
               testID="editNameInput"
@@ -192,14 +208,14 @@ export function Component({
               value={name}
               onChangeText={v => setName(enforceLen(v, MAX_NAME))}
               accessible={true}
-              accessibilityLabel="Name"
+              accessibilityLabel={_(msg`Name`)}
               accessibilityHint=""
               accessibilityLabelledBy="list-name"
             />
           </View>
           <View style={s.pb10}>
             <Text style={[styles.label, pal.text]} nativeID="list-description">
-              Description
+              <Trans>Description</Trans>
             </Text>
             <TextInput
               testID="editDescriptionInput"
@@ -215,7 +231,7 @@ export function Component({
               value={description}
               onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))}
               accessible={true}
-              accessibilityLabel="Description"
+              accessibilityLabel={_(msg`Description`)}
               accessibilityHint=""
               accessibilityLabelledBy="list-description"
             />
@@ -230,14 +246,16 @@ export function Component({
               style={s.mt10}
               onPress={onPressSave}
               accessibilityRole="button"
-              accessibilityLabel="Save"
+              accessibilityLabel={_(msg`Save`)}
               accessibilityHint="">
               <LinearGradient
                 colors={[gradients.blueLight.start, gradients.blueLight.end]}
                 start={{x: 0, y: 0}}
                 end={{x: 1, y: 1}}
                 style={[styles.btn]}>
-                <Text style={[s.white, s.bold]}>Save</Text>
+                <Text style={[s.white, s.bold]}>
+                  <Trans>Save</Trans>
+                </Text>
               </LinearGradient>
             </TouchableOpacity>
           )}
@@ -246,11 +264,13 @@ export function Component({
             style={s.mt5}
             onPress={onPressCancel}
             accessibilityRole="button"
-            accessibilityLabel="Cancel"
+            accessibilityLabel={_(msg`Cancel`)}
             accessibilityHint=""
             onAccessibilityEscape={onPressCancel}>
             <View style={[styles.btn]}>
-              <Text style={[s.black, s.bold, pal.text]}>Cancel</Text>
+              <Text style={[s.black, s.bold, pal.text]}>
+                <Trans>Cancel</Trans>
+              </Text>
             </View>
           </TouchableOpacity>
         </View>
diff --git a/src/view/com/modals/DeleteAccount.tsx b/src/view/com/modals/DeleteAccount.tsx
index 50a4cd603..ee16d46b3 100644
--- a/src/view/com/modals/DeleteAccount.tsx
+++ b/src/view/com/modals/DeleteAccount.tsx
@@ -9,7 +9,6 @@ import {TextInput} from './util'
 import LinearGradient from 'react-native-linear-gradient'
 import * as Toast from '../util/Toast'
 import {Text} from '../util/text/Text'
-import {useStores} from 'state/index'
 import {s, colors, gradients} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useTheme} from 'lib/ThemeContext'
@@ -17,13 +16,20 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import {cleanError} from 'lib/strings/errors'
 import {resetToTab} from '../../../Navigation'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
+import {useSession, useSessionApi, getAgent} from '#/state/session'
 
 export const snapPoints = ['60%']
 
 export function Component({}: {}) {
   const pal = usePalette('default')
   const theme = useTheme()
-  const store = useStores()
+  const {currentAccount} = useSession()
+  const {clearCurrentAccount, removeAccount} = useSessionApi()
+  const {_} = useLingui()
+  const {closeModal} = useModalControls()
   const {isMobile} = useWebMediaQueries()
   const [isEmailSent, setIsEmailSent] = React.useState<boolean>(false)
   const [confirmCode, setConfirmCode] = React.useState<string>('')
@@ -34,7 +40,7 @@ export function Component({}: {}) {
     setError('')
     setIsProcessing(true)
     try {
-      await store.agent.com.atproto.server.requestAccountDelete()
+      await getAgent().com.atproto.server.requestAccountDelete()
       setIsEmailSent(true)
     } catch (e: any) {
       setError(cleanError(e))
@@ -42,34 +48,39 @@ export function Component({}: {}) {
     setIsProcessing(false)
   }
   const onPressConfirmDelete = async () => {
+    if (!currentAccount?.did) {
+      throw new Error(`DeleteAccount modal: currentAccount.did is undefined`)
+    }
+
     setError('')
     setIsProcessing(true)
     const token = confirmCode.replace(/\s/g, '')
 
     try {
-      await store.agent.com.atproto.server.deleteAccount({
-        did: store.me.did,
+      await getAgent().com.atproto.server.deleteAccount({
+        did: currentAccount.did,
         password,
         token,
       })
       Toast.show('Your account has been deleted')
       resetToTab('HomeTab')
-      store.session.clear()
-      store.shell.closeModal()
+      removeAccount(currentAccount)
+      clearCurrentAccount()
+      closeModal()
     } catch (e: any) {
       setError(cleanError(e))
     }
     setIsProcessing(false)
   }
   const onCancel = () => {
-    store.shell.closeModal()
+    closeModal()
   }
   return (
     <View style={[styles.container, pal.view]}>
       <View style={[styles.innerContainer, pal.view]}>
         <View style={[styles.titleContainer, pal.view]}>
           <Text type="title-xl" style={[s.textCenter, pal.text]}>
-            Delete Account
+            <Trans>Delete Account</Trans>
           </Text>
           <View style={[pal.view, s.flexRow]}>
             <Text type="title-xl" style={[pal.text, s.bold]}>
@@ -83,7 +94,7 @@ export function Component({}: {}) {
                 pal.text,
                 s.bold,
               ]}>
-              {store.me.handle}
+              {currentAccount?.handle}
             </Text>
             <Text type="title-xl" style={[pal.text, s.bold]}>
               {'"'}
@@ -93,8 +104,10 @@ export function Component({}: {}) {
         {!isEmailSent ? (
           <>
             <Text type="lg" style={[styles.description, pal.text]}>
-              For security reasons, we'll need to send a confirmation code to
-              your email address.
+              <Trans>
+                For security reasons, we'll need to send a confirmation code to
+                your email address.
+              </Trans>
             </Text>
             {error ? (
               <View style={s.mt10}>
@@ -111,7 +124,7 @@ export function Component({}: {}) {
                   style={styles.mt20}
                   onPress={onPressSendEmail}
                   accessibilityRole="button"
-                  accessibilityLabel="Send email"
+                  accessibilityLabel={_(msg`Send email`)}
                   accessibilityHint="Sends email with confirmation code for account deletion">
                   <LinearGradient
                     colors={[
@@ -122,7 +135,7 @@ export function Component({}: {}) {
                     end={{x: 1, y: 1}}
                     style={[styles.btn]}>
                     <Text type="button-lg" style={[s.white, s.bold]}>
-                      Send Email
+                      <Trans>Send Email</Trans>
                     </Text>
                   </LinearGradient>
                 </TouchableOpacity>
@@ -130,11 +143,11 @@ export function Component({}: {}) {
                   style={[styles.btn, s.mt10]}
                   onPress={onCancel}
                   accessibilityRole="button"
-                  accessibilityLabel="Cancel account deletion"
+                  accessibilityLabel={_(msg`Cancel account deletion`)}
                   accessibilityHint=""
                   onAccessibilityEscape={onCancel}>
                   <Text type="button-lg" style={pal.textLight}>
-                    Cancel
+                    <Trans>Cancel</Trans>
                   </Text>
                 </TouchableOpacity>
               </>
@@ -147,8 +160,10 @@ export function Component({}: {}) {
               type="lg"
               style={styles.description}
               nativeID="confirmationCode">
-              Check your inbox for an email with the confirmation code to enter
-              below:
+              <Trans>
+                Check your inbox for an email with the confirmation code to
+                enter below:
+              </Trans>
             </Text>
             <TextInput
               style={[styles.textInput, pal.borderDark, pal.text, styles.mb20]}
@@ -158,11 +173,11 @@ export function Component({}: {}) {
               value={confirmCode}
               onChangeText={setConfirmCode}
               accessibilityLabelledBy="confirmationCode"
-              accessibilityLabel="Confirmation code"
+              accessibilityLabel={_(msg`Confirmation code`)}
               accessibilityHint="Input confirmation code for account deletion"
             />
             <Text type="lg" style={styles.description} nativeID="password">
-              Please enter your password as well:
+              <Trans>Please enter your password as well:</Trans>
             </Text>
             <TextInput
               style={[styles.textInput, pal.borderDark, pal.text]}
@@ -173,7 +188,7 @@ export function Component({}: {}) {
               value={password}
               onChangeText={setPassword}
               accessibilityLabelledBy="password"
-              accessibilityLabel="Password"
+              accessibilityLabel={_(msg`Password`)}
               accessibilityHint="Input password for account deletion"
             />
             {error ? (
@@ -191,21 +206,21 @@ export function Component({}: {}) {
                   style={[styles.btn, styles.evilBtn, styles.mt20]}
                   onPress={onPressConfirmDelete}
                   accessibilityRole="button"
-                  accessibilityLabel="Confirm delete account"
+                  accessibilityLabel={_(msg`Confirm delete account`)}
                   accessibilityHint="">
                   <Text type="button-lg" style={[s.white, s.bold]}>
-                    Delete my account
+                    <Trans>Delete my account</Trans>
                   </Text>
                 </TouchableOpacity>
                 <TouchableOpacity
                   style={[styles.btn, s.mt10]}
                   onPress={onCancel}
                   accessibilityRole="button"
-                  accessibilityLabel="Cancel account deletion"
+                  accessibilityLabel={_(msg`Cancel account deletion`)}
                   accessibilityHint="Exits account deletion process"
                   onAccessibilityEscape={onCancel}>
                   <Text type="button-lg" style={pal.textLight}>
-                    Cancel
+                    <Trans>Cancel</Trans>
                   </Text>
                 </TouchableOpacity>
               </>
diff --git a/src/view/com/modals/EditImage.tsx b/src/view/com/modals/EditImage.tsx
index dcb6668c7..753907472 100644
--- a/src/view/com/modals/EditImage.tsx
+++ b/src/view/com/modals/EditImage.tsx
@@ -6,7 +6,6 @@ import {gradients, s} from 'lib/styles'
 import {useTheme} from 'lib/ThemeContext'
 import {Text} from '../util/text/Text'
 import LinearGradient from 'react-native-linear-gradient'
-import {useStores} from 'state/index'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import ImageEditor, {Position} from 'react-avatar-editor'
 import {TextInput} from './util'
@@ -19,6 +18,9 @@ import {Slider} from '@miblanchard/react-native-slider'
 import {MaterialIcons} from '@expo/vector-icons'
 import {observer} from 'mobx-react-lite'
 import {getKeys} from 'lib/type-assertions'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
 
 export const snapPoints = ['80%']
 
@@ -52,9 +54,10 @@ export const Component = observer(function EditImageImpl({
 }: Props) {
   const pal = usePalette('default')
   const theme = useTheme()
-  const store = useStores()
+  const {_} = useLingui()
   const windowDimensions = useWindowDimensions()
   const {isMobile} = useWebMediaQueries()
+  const {closeModal} = useModalControls()
 
   const {
     aspectRatio,
@@ -128,8 +131,8 @@ export const Component = observer(function EditImageImpl({
   }, [image])
 
   const onCloseModal = useCallback(() => {
-    store.shell.closeModal()
-  }, [store.shell])
+    closeModal()
+  }, [closeModal])
 
   const onPressCancel = useCallback(async () => {
     await gallery.previous(image)
@@ -200,7 +203,9 @@ export const Component = observer(function EditImageImpl({
           paddingHorizontal: isMobile ? 16 : undefined,
         },
       ]}>
-      <Text style={[styles.title, pal.text]}>Edit image</Text>
+      <Text style={[styles.title, pal.text]}>
+        <Trans>Edit image</Trans>
+      </Text>
       <View style={[styles.gap18, s.flexRow]}>
         <View>
           <View
@@ -228,7 +233,7 @@ export const Component = observer(function EditImageImpl({
         <View>
           {!isMobile ? (
             <Text type="sm-bold" style={pal.text}>
-              Ratios
+              <Trans>Ratios</Trans>
             </Text>
           ) : null}
           <View style={imgControlStyles}>
@@ -263,7 +268,7 @@ export const Component = observer(function EditImageImpl({
           </View>
           {!isMobile ? (
             <Text type="sm-bold" style={[pal.text, styles.subsection]}>
-              Transformations
+              <Trans>Transformations</Trans>
             </Text>
           ) : null}
           <View style={imgControlStyles}>
@@ -291,7 +296,7 @@ export const Component = observer(function EditImageImpl({
       </View>
       <View style={[styles.gap18, styles.bottomSection, pal.border]}>
         <Text type="sm-bold" style={pal.text} nativeID="alt-text">
-          Accessibility
+          <Trans>Accessibility</Trans>
         </Text>
         <TextInput
           testID="altTextImageInput"
@@ -307,7 +312,7 @@ export const Component = observer(function EditImageImpl({
           multiline
           value={altText}
           onChangeText={text => setAltText(enforceLen(text, MAX_ALT_TEXT))}
-          accessibilityLabel="Alt text"
+          accessibilityLabel={_(msg`Alt text`)}
           accessibilityHint=""
           accessibilityLabelledBy="alt-text"
         />
@@ -315,7 +320,7 @@ export const Component = observer(function EditImageImpl({
       <View style={styles.btns}>
         <Pressable onPress={onPressCancel} accessibilityRole="button">
           <Text type="xl" style={pal.link}>
-            Cancel
+            <Trans>Cancel</Trans>
           </Text>
         </Pressable>
         <Pressable onPress={onPressSave} accessibilityRole="button">
@@ -325,7 +330,7 @@ export const Component = observer(function EditImageImpl({
             end={{x: 1, y: 1}}
             style={[styles.btn]}>
             <Text type="xl-medium" style={s.white}>
-              Done
+              <Trans>Done</Trans>
             </Text>
           </LinearGradient>
         </Pressable>
diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx
index dfd5305f5..e044f8c0e 100644
--- a/src/view/com/modals/EditProfile.tsx
+++ b/src/view/com/modals/EditProfile.tsx
@@ -11,10 +11,9 @@ import {
 } from 'react-native'
 import LinearGradient from 'react-native-linear-gradient'
 import {Image as RNImage} from 'react-native-image-crop-picker'
+import {AppBskyActorDefs} from '@atproto/api'
 import {Text} from '../util/text/Text'
 import {ErrorMessage} from '../util/error/ErrorMessage'
-import {useStores} from 'state/index'
-import {ProfileModel} from 'state/models/content/profile'
 import {s, colors, gradients} from 'lib/styles'
 import {enforceLen} from 'lib/strings/helpers'
 import {MAX_DISPLAY_NAME, MAX_DESCRIPTION} from 'lib/constants'
@@ -24,9 +23,14 @@ import {EditableUserAvatar} from '../util/UserAvatar'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useTheme} from 'lib/ThemeContext'
 import {useAnalytics} from 'lib/analytics/analytics'
-import {cleanError, isNetworkError} from 'lib/strings/errors'
+import {cleanError} from 'lib/strings/errors'
 import Animated, {FadeOut} from 'react-native-reanimated'
 import {isWeb} from 'platform/detection'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
+import {useProfileUpdateMutation} from '#/state/queries/profile'
+import {logger} from '#/logger'
 
 const AnimatedTouchableOpacity =
   Animated.createAnimatedComponent(TouchableOpacity)
@@ -34,30 +38,30 @@ const AnimatedTouchableOpacity =
 export const snapPoints = ['fullscreen']
 
 export function Component({
-  profileView,
+  profile,
   onUpdate,
 }: {
-  profileView: ProfileModel
+  profile: AppBskyActorDefs.ProfileViewDetailed
   onUpdate?: () => void
 }) {
-  const store = useStores()
-  const [error, setError] = useState<string>('')
   const pal = usePalette('default')
   const theme = useTheme()
   const {track} = useAnalytics()
-
-  const [isProcessing, setProcessing] = useState<boolean>(false)
+  const {_} = useLingui()
+  const {closeModal} = useModalControls()
+  const updateMutation = useProfileUpdateMutation()
+  const [imageError, setImageError] = useState<string>('')
   const [displayName, setDisplayName] = useState<string>(
-    profileView.displayName || '',
+    profile.displayName || '',
   )
   const [description, setDescription] = useState<string>(
-    profileView.description || '',
+    profile.description || '',
   )
   const [userBanner, setUserBanner] = useState<string | undefined | null>(
-    profileView.banner,
+    profile.banner,
   )
   const [userAvatar, setUserAvatar] = useState<string | undefined | null>(
-    profileView.avatar,
+    profile.avatar,
   )
   const [newUserBanner, setNewUserBanner] = useState<
     RNImage | undefined | null
@@ -66,10 +70,11 @@ export function Component({
     RNImage | undefined | null
   >()
   const onPressCancel = () => {
-    store.shell.closeModal()
+    closeModal()
   }
   const onSelectNewAvatar = useCallback(
     async (img: RNImage | null) => {
+      setImageError('')
       if (img === null) {
         setNewUserAvatar(null)
         setUserAvatar(null)
@@ -81,14 +86,15 @@ export function Component({
         setNewUserAvatar(finalImg)
         setUserAvatar(finalImg.path)
       } catch (e: any) {
-        setError(cleanError(e))
+        setImageError(cleanError(e))
       }
     },
-    [track, setNewUserAvatar, setUserAvatar, setError],
+    [track, setNewUserAvatar, setUserAvatar, setImageError],
   )
 
   const onSelectNewBanner = useCallback(
     async (img: RNImage | null) => {
+      setImageError('')
       if (!img) {
         setNewUserBanner(null)
         setUserBanner(null)
@@ -100,58 +106,50 @@ export function Component({
         setNewUserBanner(finalImg)
         setUserBanner(finalImg.path)
       } catch (e: any) {
-        setError(cleanError(e))
+        setImageError(cleanError(e))
       }
     },
-    [track, setNewUserBanner, setUserBanner, setError],
+    [track, setNewUserBanner, setUserBanner, setImageError],
   )
 
   const onPressSave = useCallback(async () => {
     track('EditProfile:Save')
-    setProcessing(true)
-    if (error) {
-      setError('')
-    }
+    setImageError('')
     try {
-      await profileView.updateProfile(
-        {
+      await updateMutation.mutateAsync({
+        profile,
+        updates: {
           displayName,
           description,
         },
         newUserAvatar,
         newUserBanner,
-      )
+      })
       Toast.show('Profile updated')
       onUpdate?.()
-      store.shell.closeModal()
+      closeModal()
     } catch (e: any) {
-      if (isNetworkError(e)) {
-        setError(
-          'Failed to save your profile. Check your internet connection and try again.',
-        )
-      } else {
-        setError(cleanError(e))
-      }
+      logger.error('Failed to update user profile', {error: String(e)})
     }
-    setProcessing(false)
   }, [
     track,
-    setProcessing,
-    setError,
-    error,
-    profileView,
+    updateMutation,
+    profile,
     onUpdate,
-    store,
+    closeModal,
     displayName,
     description,
     newUserAvatar,
     newUserBanner,
+    setImageError,
   ])
 
   return (
     <KeyboardAvoidingView style={s.flex1} behavior="height">
       <ScrollView style={[pal.view]} testID="editProfileModal">
-        <Text style={[styles.title, pal.text]}>Edit my profile</Text>
+        <Text style={[styles.title, pal.text]}>
+          <Trans>Edit my profile</Trans>
+        </Text>
         <View style={styles.photos}>
           <UserBanner
             banner={userBanner}
@@ -165,14 +163,21 @@ export function Component({
             />
           </View>
         </View>
-        {error !== '' && (
+        {updateMutation.isError && (
+          <View style={styles.errorContainer}>
+            <ErrorMessage message={cleanError(updateMutation.error)} />
+          </View>
+        )}
+        {imageError !== '' && (
           <View style={styles.errorContainer}>
-            <ErrorMessage message={error} />
+            <ErrorMessage message={imageError} />
           </View>
         )}
         <View style={styles.form}>
           <View>
-            <Text style={[styles.label, pal.text]}>Display Name</Text>
+            <Text style={[styles.label, pal.text]}>
+              <Trans>Display Name</Trans>
+            </Text>
             <TextInput
               testID="editProfileDisplayNameInput"
               style={[styles.textInput, pal.border, pal.text]}
@@ -183,12 +188,14 @@ export function Component({
                 setDisplayName(enforceLen(v, MAX_DISPLAY_NAME))
               }
               accessible={true}
-              accessibilityLabel="Display name"
+              accessibilityLabel={_(msg`Display name`)}
               accessibilityHint="Edit your display name"
             />
           </View>
           <View style={s.pb10}>
-            <Text style={[styles.label, pal.text]}>Description</Text>
+            <Text style={[styles.label, pal.text]}>
+              <Trans>Description</Trans>
+            </Text>
             <TextInput
               testID="editProfileDescriptionInput"
               style={[styles.textArea, pal.border, pal.text]}
@@ -199,11 +206,11 @@ export function Component({
               value={description}
               onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))}
               accessible={true}
-              accessibilityLabel="Description"
+              accessibilityLabel={_(msg`Description`)}
               accessibilityHint="Edit your profile description"
             />
           </View>
-          {isProcessing ? (
+          {updateMutation.isPending ? (
             <View style={[styles.btn, s.mt10, {backgroundColor: colors.gray2}]}>
               <ActivityIndicator />
             </View>
@@ -213,29 +220,33 @@ export function Component({
               style={s.mt10}
               onPress={onPressSave}
               accessibilityRole="button"
-              accessibilityLabel="Save"
+              accessibilityLabel={_(msg`Save`)}
               accessibilityHint="Saves any changes to your profile">
               <LinearGradient
                 colors={[gradients.blueLight.start, gradients.blueLight.end]}
                 start={{x: 0, y: 0}}
                 end={{x: 1, y: 1}}
                 style={[styles.btn]}>
-                <Text style={[s.white, s.bold]}>Save Changes</Text>
+                <Text style={[s.white, s.bold]}>
+                  <Trans>Save Changes</Trans>
+                </Text>
               </LinearGradient>
             </TouchableOpacity>
           )}
-          {!isProcessing && (
+          {!updateMutation.isPending && (
             <AnimatedTouchableOpacity
               exiting={!isWeb ? FadeOut : undefined}
               testID="editProfileCancelBtn"
               style={s.mt5}
               onPress={onPressCancel}
               accessibilityRole="button"
-              accessibilityLabel="Cancel profile editing"
+              accessibilityLabel={_(msg`Cancel profile editing`)}
               accessibilityHint=""
               onAccessibilityEscape={onPressCancel}>
               <View style={[styles.btn]}>
-                <Text style={[s.black, s.bold, pal.text]}>Cancel</Text>
+                <Text style={[s.black, s.bold, pal.text]}>
+                  <Trans>Cancel</Trans>
+                </Text>
               </View>
             </AnimatedTouchableOpacity>
           )}
diff --git a/src/view/com/modals/InviteCodes.tsx b/src/view/com/modals/InviteCodes.tsx
index 09cfd4de7..82a826aca 100644
--- a/src/view/com/modals/InviteCodes.tsx
+++ b/src/view/com/modals/InviteCodes.tsx
@@ -1,6 +1,11 @@
 import React from 'react'
-import {StyleSheet, TouchableOpacity, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
+import {
+  StyleSheet,
+  TouchableOpacity,
+  View,
+  ActivityIndicator,
+} from 'react-native'
+import {ComAtprotoServerDefs} from '@atproto/api'
 import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
@@ -9,30 +14,57 @@ import Clipboard from '@react-native-clipboard/clipboard'
 import {Text} from '../util/text/Text'
 import {Button} from '../util/forms/Button'
 import * as Toast from '../util/Toast'
-import {useStores} from 'state/index'
 import {ScrollView} from './util'
 import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb} from 'platform/detection'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {Trans} from '@lingui/macro'
+import {cleanError} from 'lib/strings/errors'
+import {useModalControls} from '#/state/modals'
+import {useInvitesState, useInvitesAPI} from '#/state/invites'
+import {UserInfoText} from '../util/UserInfoText'
+import {makeProfileLink} from '#/lib/routes/links'
+import {Link} from '../util/Link'
+import {ErrorMessage} from '../util/error/ErrorMessage'
+import {
+  useInviteCodesQuery,
+  InviteCodesQueryResponse,
+} from '#/state/queries/invites'
 
 export const snapPoints = ['70%']
 
-export function Component({}: {}) {
+export function Component() {
+  const {isLoading, data: invites, error} = useInviteCodesQuery()
+
+  return error ? (
+    <ErrorMessage message={cleanError(error)} />
+  ) : isLoading || !invites ? (
+    <View style={{padding: 18}}>
+      <ActivityIndicator />
+    </View>
+  ) : (
+    <Inner invites={invites} />
+  )
+}
+
+export function Inner({invites}: {invites: InviteCodesQueryResponse}) {
   const pal = usePalette('default')
-  const store = useStores()
+  const {closeModal} = useModalControls()
   const {isTabletOrDesktop} = useWebMediaQueries()
 
   const onClose = React.useCallback(() => {
-    store.shell.closeModal()
-  }, [store])
+    closeModal()
+  }, [closeModal])
 
-  if (store.me.invites.length === 0) {
+  if (invites.all.length === 0) {
     return (
       <View style={[styles.container, pal.view]} testID="inviteCodesModal">
         <View style={[styles.empty, pal.viewLight]}>
           <Text type="lg" style={[pal.text, styles.emptyText]}>
-            You don't have any invite codes yet! We'll send you some when you've
-            been on Bluesky for a little longer.
+            <Trans>
+              You don't have any invite codes yet! We'll send you some when
+              you've been on Bluesky for a little longer.
+            </Trans>
           </Text>
         </View>
         <View style={styles.flex1} />
@@ -56,18 +88,29 @@ export function Component({}: {}) {
   return (
     <View style={[styles.container, pal.view]} testID="inviteCodesModal">
       <Text type="title-xl" style={[styles.title, pal.text]}>
-        Invite a Friend
+        <Trans>Invite a Friend</Trans>
       </Text>
       <Text type="lg" style={[styles.description, pal.text]}>
-        Each code works once. You'll receive more invite codes periodically.
+        <Trans>
+          Each code works once. You'll receive more invite codes periodically.
+        </Trans>
       </Text>
       <ScrollView style={[styles.scrollContainer, pal.border]}>
-        {store.me.invites.map((invite, i) => (
+        {invites.available.map((invite, i) => (
           <InviteCode
             testID={`inviteCode-${i}`}
             key={invite.code}
-            code={invite.code}
-            used={invite.available - invite.uses.length <= 0 || invite.disabled}
+            invite={invite}
+            invites={invites}
+          />
+        ))}
+        {invites.used.map((invite, i) => (
+          <InviteCode
+            used
+            testID={`inviteCode-${i}`}
+            key={invite.code}
+            invite={invite}
+            invites={invites}
           />
         ))}
       </ScrollView>
@@ -85,56 +128,89 @@ export function Component({}: {}) {
   )
 }
 
-const InviteCode = observer(function InviteCodeImpl({
+function InviteCode({
   testID,
-  code,
+  invite,
   used,
+  invites,
 }: {
   testID: string
-  code: string
+  invite: ComAtprotoServerDefs.InviteCode
   used?: boolean
+  invites: InviteCodesQueryResponse
 }) {
   const pal = usePalette('default')
-  const store = useStores()
-  const {invitesAvailable} = store.me
+  const invitesState = useInvitesState()
+  const {setInviteCopied} = useInvitesAPI()
 
   const onPress = React.useCallback(() => {
-    Clipboard.setString(code)
+    Clipboard.setString(invite.code)
     Toast.show('Copied to clipboard')
-    store.invitedUsers.setInviteCopied(code)
-  }, [store, code])
+    setInviteCopied(invite.code)
+  }, [setInviteCopied, invite])
 
   return (
-    <TouchableOpacity
-      testID={testID}
-      style={[styles.inviteCode, pal.border]}
-      onPress={onPress}
-      accessibilityRole="button"
-      accessibilityLabel={
-        invitesAvailable === 1
-          ? 'Invite codes: 1 available'
-          : `Invite codes: ${invitesAvailable} available`
-      }
-      accessibilityHint="Opens list of invite codes">
-      <Text
-        testID={`${testID}-code`}
-        type={used ? 'md' : 'md-bold'}
-        style={used ? [pal.textLight, styles.strikeThrough] : pal.text}>
-        {code}
-      </Text>
-      <View style={styles.flex1} />
-      {!used && store.invitedUsers.isInviteCopied(code) && (
-        <Text style={[pal.textLight, styles.codeCopied]}>Copied</Text>
-      )}
-      {!used && (
-        <FontAwesomeIcon
-          icon={['far', 'clone']}
-          style={pal.text as FontAwesomeIconStyle}
-        />
-      )}
-    </TouchableOpacity>
+    <View
+      style={[
+        pal.border,
+        {borderBottomWidth: 1, paddingHorizontal: 20, paddingVertical: 14},
+      ]}>
+      <TouchableOpacity
+        testID={testID}
+        style={[styles.inviteCode]}
+        onPress={onPress}
+        accessibilityRole="button"
+        accessibilityLabel={
+          invites.available.length === 1
+            ? 'Invite codes: 1 available'
+            : `Invite codes: ${invites.available.length} available`
+        }
+        accessibilityHint="Opens list of invite codes">
+        <Text
+          testID={`${testID}-code`}
+          type={used ? 'md' : 'md-bold'}
+          style={used ? [pal.textLight, styles.strikeThrough] : pal.text}>
+          {invite.code}
+        </Text>
+        <View style={styles.flex1} />
+        {!used && invitesState.copiedInvites.includes(invite.code) && (
+          <Text style={[pal.textLight, styles.codeCopied]}>
+            <Trans>Copied</Trans>
+          </Text>
+        )}
+        {!used && (
+          <FontAwesomeIcon
+            icon={['far', 'clone']}
+            style={pal.text as FontAwesomeIconStyle}
+          />
+        )}
+      </TouchableOpacity>
+      {invite.uses.length > 0 ? (
+        <View
+          style={{
+            flexDirection: 'column',
+            gap: 8,
+            paddingTop: 6,
+          }}>
+          <Text style={pal.text}>
+            <Trans>Used by:</Trans>
+          </Text>
+          {invite.uses.map(use => (
+            <Link
+              key={use.usedBy}
+              href={makeProfileLink({handle: use.usedBy, did: ''})}
+              style={{
+                flexDirection: 'row',
+              }}>
+              <Text style={pal.text}>• </Text>
+              <UserInfoText did={use.usedBy} style={pal.link} />
+            </Link>
+          ))}
+        </View>
+      ) : null}
+    </View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   container: {
@@ -176,9 +252,6 @@ const styles = StyleSheet.create({
   inviteCode: {
     flexDirection: 'row',
     alignItems: 'center',
-    borderBottomWidth: 1,
-    paddingHorizontal: 20,
-    paddingVertical: 14,
   },
   codeCopied: {
     marginRight: 8,
diff --git a/src/view/com/modals/LinkWarning.tsx b/src/view/com/modals/LinkWarning.tsx
index 67a156af4..39e6cc3e6 100644
--- a/src/view/com/modals/LinkWarning.tsx
+++ b/src/view/com/modals/LinkWarning.tsx
@@ -1,33 +1,29 @@
 import React from 'react'
 import {Linking, SafeAreaView, StyleSheet, View} from 'react-native'
 import {ScrollView} from './util'
-import {observer} from 'mobx-react-lite'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {Text} from '../util/text/Text'
 import {Button} from '../util/forms/Button'
-import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb} from 'platform/detection'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {isPossiblyAUrl, splitApexDomain} from 'lib/strings/url-helpers'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
 
 export const snapPoints = ['50%']
 
-export const Component = observer(function Component({
-  text,
-  href,
-}: {
-  text: string
-  href: string
-}) {
+export function Component({text, href}: {text: string; href: string}) {
   const pal = usePalette('default')
-  const store = useStores()
+  const {closeModal} = useModalControls()
   const {isMobile} = useWebMediaQueries()
+  const {_} = useLingui()
   const potentiallyMisleading = isPossiblyAUrl(text)
 
   const onPressVisit = () => {
-    store.shell.closeModal()
+    closeModal()
     Linking.openURL(href)
   }
 
@@ -45,26 +41,26 @@ export const Component = observer(function Component({
                 size={18}
               />
               <Text type="title-lg" style={[pal.text, styles.title]}>
-                Potentially Misleading Link
+                <Trans>Potentially Misleading Link</Trans>
               </Text>
             </>
           ) : (
             <Text type="title-lg" style={[pal.text, styles.title]}>
-              Leaving Bluesky
+              <Trans>Leaving Bluesky</Trans>
             </Text>
           )}
         </View>
 
         <View style={{gap: 10}}>
           <Text type="lg" style={pal.text}>
-            This link is taking you to the following website:
+            <Trans>This link is taking you to the following website:</Trans>
           </Text>
 
           <LinkBox href={href} />
 
           {potentiallyMisleading && (
             <Text type="lg" style={pal.text}>
-              Make sure this is where you intend to go!
+              <Trans>Make sure this is where you intend to go!</Trans>
             </Text>
           )}
         </View>
@@ -74,7 +70,7 @@ export const Component = observer(function Component({
             testID="confirmBtn"
             type="primary"
             onPress={onPressVisit}
-            accessibilityLabel="Visit Site"
+            accessibilityLabel={_(msg`Visit Site`)}
             accessibilityHint=""
             label="Visit Site"
             labelContainerStyle={{justifyContent: 'center', padding: 4}}
@@ -83,8 +79,10 @@ export const Component = observer(function Component({
           <Button
             testID="cancelBtn"
             type="default"
-            onPress={() => store.shell.closeModal()}
-            accessibilityLabel="Cancel"
+            onPress={() => {
+              closeModal()
+            }}
+            accessibilityLabel={_(msg`Cancel`)}
             accessibilityHint=""
             label="Cancel"
             labelContainerStyle={{justifyContent: 'center', padding: 4}}
@@ -94,7 +92,7 @@ export const Component = observer(function Component({
       </ScrollView>
     </SafeAreaView>
   )
-})
+}
 
 function LinkBox({href}: {href: string}) {
   const pal = usePalette('default')
diff --git a/src/view/com/modals/ListAddUser.tsx b/src/view/com/modals/ListAddRemoveUsers.tsx
index a04e2d186..14e16d6bf 100644
--- a/src/view/com/modals/ListAddUser.tsx
+++ b/src/view/com/modals/ListAddRemoveUsers.tsx
@@ -1,4 +1,4 @@
-import React, {useEffect, useCallback, useState, useMemo} from 'react'
+import React, {useCallback, useState} from 'react'
 import {
   ActivityIndicator,
   Pressable,
@@ -6,17 +6,13 @@ import {
   StyleSheet,
   View,
 } from 'react-native'
-import {AppBskyActorDefs} from '@atproto/api'
+import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
 import {ScrollView, TextInput} from './util'
-import {observer} from 'mobx-react-lite'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {Text} from '../util/text/Text'
 import {Button} from '../util/forms/Button'
 import {UserAvatar} from '../util/UserAvatar'
 import * as Toast from '../util/Toast'
-import {useStores} from 'state/index'
-import {ListModel} from 'state/models/content/list'
-import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
 import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb} from 'platform/detection'
@@ -26,47 +22,40 @@ import {cleanError} from 'lib/strings/errors'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {sanitizeHandle} from 'lib/strings/handles'
 import {HITSLOP_20} from '#/lib/constants'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
+import {
+  useDangerousListMembershipsQuery,
+  getMembership,
+  ListMembersip,
+  useListMembershipAddMutation,
+  useListMembershipRemoveMutation,
+} from '#/state/queries/list-memberships'
+import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
 
 export const snapPoints = ['90%']
 
-export const Component = observer(function Component({
+export function Component({
   list,
-  onAdd,
+  onChange,
 }: {
-  list: ListModel
-  onAdd?: (profile: AppBskyActorDefs.ProfileViewBasic) => void
+  list: AppBskyGraphDefs.ListView
+  onChange?: (
+    type: 'add' | 'remove',
+    profile: AppBskyActorDefs.ProfileViewBasic,
+  ) => void
 }) {
   const pal = usePalette('default')
-  const store = useStores()
+  const {_} = useLingui()
+  const {closeModal} = useModalControls()
   const {isMobile} = useWebMediaQueries()
   const [query, setQuery] = useState('')
-  const autocompleteView = useMemo<UserAutocompleteModel>(
-    () => new UserAutocompleteModel(store),
-    [store],
-  )
+  const autocomplete = useActorAutocompleteQuery(query)
+  const {data: memberships} = useDangerousListMembershipsQuery()
   const [isKeyboardVisible] = useIsKeyboardVisible()
 
-  // initial setup
-  useEffect(() => {
-    autocompleteView.setup().then(() => {
-      autocompleteView.setPrefix('')
-    })
-    autocompleteView.setActive(true)
-    list.loadAll()
-  }, [autocompleteView, list])
-
-  const onChangeQuery = useCallback(
-    (text: string) => {
-      setQuery(text)
-      autocompleteView.setPrefix(text)
-    },
-    [setQuery, autocompleteView],
-  )
-
-  const onPressCancelSearch = useCallback(
-    () => onChangeQuery(''),
-    [onChangeQuery],
-  )
+  const onPressCancelSearch = useCallback(() => setQuery(''), [setQuery])
 
   return (
     <SafeAreaView
@@ -81,9 +70,9 @@ export const Component = observer(function Component({
             placeholder="Search for users"
             placeholderTextColor={pal.colors.textLight}
             value={query}
-            onChangeText={onChangeQuery}
+            onChangeText={setQuery}
             accessible={true}
-            accessibilityLabel="Search"
+            accessibilityLabel={_(msg`Search`)}
             accessibilityHint=""
             autoFocus
             autoCapitalize="none"
@@ -95,7 +84,7 @@ export const Component = observer(function Component({
             <Pressable
               onPress={onPressCancelSearch}
               accessibilityRole="button"
-              accessibilityLabel="Cancel search"
+              accessibilityLabel={_(msg`Cancel search`)}
               accessibilityHint="Exits inputting search query"
               onAccessibilityEscape={onPressCancelSearch}
               hitSlop={HITSLOP_20}>
@@ -111,19 +100,20 @@ export const Component = observer(function Component({
           style={[s.flex1]}
           keyboardDismissMode="none"
           keyboardShouldPersistTaps="always">
-          {autocompleteView.isLoading ? (
+          {autocomplete.isLoading ? (
             <View style={{marginVertical: 20}}>
               <ActivityIndicator />
             </View>
-          ) : autocompleteView.suggestions.length ? (
+          ) : autocomplete.data?.length ? (
             <>
-              {autocompleteView.suggestions.slice(0, 40).map((item, i) => (
+              {autocomplete.data.slice(0, 40).map((item, i) => (
                 <UserResult
                   key={item.did}
                   list={list}
                   profile={item}
+                  memberships={memberships}
                   noBorder={i === 0}
-                  onAdd={onAdd}
+                  onChange={onChange}
                 />
               ))}
             </>
@@ -134,7 +124,7 @@ export const Component = observer(function Component({
                 pal.textLight,
                 {paddingHorizontal: 12, paddingVertical: 16},
               ]}>
-              No results found for {autocompleteView.prefix}
+              <Trans>No results found for {query}</Trans>
             </Text>
           )}
         </ScrollView>
@@ -146,8 +136,10 @@ export const Component = observer(function Component({
           <Button
             testID="doneBtn"
             type="default"
-            onPress={() => store.shell.closeModal()}
-            accessibilityLabel="Done"
+            onPress={() => {
+              closeModal()
+            }}
+            accessibilityLabel={_(msg`Done`)}
             accessibilityHint=""
             label="Done"
             labelContainerStyle={{justifyContent: 'center', padding: 4}}
@@ -157,36 +149,71 @@ export const Component = observer(function Component({
       </View>
     </SafeAreaView>
   )
-})
+}
 
 function UserResult({
   profile,
   list,
+  memberships,
   noBorder,
-  onAdd,
+  onChange,
 }: {
   profile: AppBskyActorDefs.ProfileViewBasic
-  list: ListModel
+  list: AppBskyGraphDefs.ListView
+  memberships: ListMembersip[] | undefined
   noBorder: boolean
-  onAdd?: (profile: AppBskyActorDefs.ProfileViewBasic) => void | undefined
+  onChange?: (
+    type: 'add' | 'remove',
+    profile: AppBskyActorDefs.ProfileViewBasic,
+  ) => void | undefined
 }) {
   const pal = usePalette('default')
+  const {_} = useLingui()
   const [isProcessing, setIsProcessing] = useState(false)
-  const [isAdded, setIsAdded] = useState(list.isMember(profile.did))
+  const membership = React.useMemo(
+    () => getMembership(memberships, list.uri, profile.did),
+    [memberships, list.uri, profile.did],
+  )
+  const listMembershipAddMutation = useListMembershipAddMutation()
+  const listMembershipRemoveMutation = useListMembershipRemoveMutation()
 
-  const onPressAdd = useCallback(async () => {
+  const onToggleMembership = useCallback(async () => {
+    if (typeof membership === 'undefined') {
+      return
+    }
     setIsProcessing(true)
     try {
-      await list.addMember(profile)
-      Toast.show('Added to list')
-      setIsAdded(true)
-      onAdd?.(profile)
+      if (membership === false) {
+        await listMembershipAddMutation.mutateAsync({
+          listUri: list.uri,
+          actorDid: profile.did,
+        })
+        Toast.show(_(msg`Added to list`))
+        onChange?.('add', profile)
+      } else {
+        await listMembershipRemoveMutation.mutateAsync({
+          listUri: list.uri,
+          actorDid: profile.did,
+          membershipUri: membership,
+        })
+        Toast.show(_(msg`Removed from list`))
+        onChange?.('remove', profile)
+      }
     } catch (e) {
       Toast.show(cleanError(e))
     } finally {
       setIsProcessing(false)
     }
-  }, [list, profile, setIsProcessing, setIsAdded, onAdd])
+  }, [
+    _,
+    list,
+    profile,
+    membership,
+    setIsProcessing,
+    onChange,
+    listMembershipAddMutation,
+    listMembershipRemoveMutation,
+  ])
 
   return (
     <View
@@ -228,16 +255,14 @@ function UserResult({
         {!!profile.viewer?.followedBy && <View style={s.flexRow} />}
       </View>
       <View>
-        {isAdded ? (
-          <FontAwesomeIcon icon="check" />
-        ) : isProcessing ? (
+        {isProcessing || typeof membership === 'undefined' ? (
           <ActivityIndicator />
         ) : (
           <Button
             testID={`user-${profile.handle}-addBtn`}
             type="default"
-            label="Add"
-            onPress={onPressAdd}
+            label={membership === false ? _(msg`Add`) : _(msg`Remove`)}
+            onPress={onToggleMembership}
           />
         )}
       </View>
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index 5aaa09e87..a3e6fb9e5 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -1,15 +1,15 @@
 import React, {useRef, useEffect} from 'react'
 import {StyleSheet} from 'react-native'
 import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context'
-import {observer} from 'mobx-react-lite'
 import BottomSheet from '@gorhom/bottom-sheet'
-import {useStores} from 'state/index'
 import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
 import {usePalette} from 'lib/hooks/usePalette'
 import {timeout} from 'lib/async/timeout'
 import {navigate} from '../../../Navigation'
 import once from 'lodash.once'
 
+import {useModals, useModalControls} from '#/state/modals'
+import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
 import * as ConfirmModal from './Confirm'
 import * as EditProfileModal from './EditProfile'
 import * as ProfilePreviewModal from './ProfilePreview'
@@ -18,7 +18,7 @@ import * as RepostModal from './Repost'
 import * as SelfLabelModal from './SelfLabel'
 import * as CreateOrEditListModal from './CreateOrEditList'
 import * as UserAddRemoveListsModal from './UserAddRemoveLists'
-import * as ListAddUserModal from './ListAddUser'
+import * as ListAddUserModal from './ListAddRemoveUsers'
 import * as AltImageModal from './AltImage'
 import * as EditImageModal from './AltImage'
 import * as ReportModal from './report/Modal'
@@ -40,26 +40,29 @@ import * as LinkWarningModal from './LinkWarning'
 const DEFAULT_SNAPPOINTS = ['90%']
 const HANDLE_HEIGHT = 24
 
-export const ModalsContainer = observer(function ModalsContainer() {
-  const store = useStores()
+export function ModalsContainer() {
+  const {isModalActive, activeModals} = useModals()
+  const {closeModal} = useModalControls()
   const bottomSheetRef = useRef<BottomSheet>(null)
   const pal = usePalette('default')
   const safeAreaInsets = useSafeAreaInsets()
 
-  const activeModal =
-    store.shell.activeModals[store.shell.activeModals.length - 1]
+  const activeModal = activeModals[activeModals.length - 1]
 
   const navigateOnce = once(navigate)
 
-  const onBottomSheetAnimate = (fromIndex: number, toIndex: number) => {
-    if (activeModal?.name === 'profile-preview' && toIndex === 1) {
-      // begin loading the profile screen behind the scenes
-      navigateOnce('Profile', {name: activeModal.did})
-    }
-  }
+  // It seems like the bottom sheet bugs out when this callback changes.
+  const onBottomSheetAnimate = useNonReactiveCallback(
+    (_fromIndex: number, toIndex: number) => {
+      if (activeModal?.name === 'profile-preview' && toIndex === 1) {
+        // begin loading the profile screen behind the scenes
+        navigateOnce('Profile', {name: activeModal.did})
+      }
+    },
+  )
   const onBottomSheetChange = async (snapPoint: number) => {
     if (snapPoint === -1) {
-      store.shell.closeModal()
+      closeModal()
     } else if (activeModal?.name === 'profile-preview' && snapPoint === 1) {
       await navigateOnce('Profile', {name: activeModal.did})
       // There is no particular callback for when the view has actually been presented.
@@ -67,21 +70,21 @@ export const ModalsContainer = observer(function ModalsContainer() {
       // It's acceptable because the data is already being fetched + it usually takes longer anyway.
       // TODO: Figure out why avatar/cover don't always show instantly from cache.
       await timeout(200)
-      store.shell.closeModal()
+      closeModal()
     }
   }
   const onClose = () => {
     bottomSheetRef.current?.close()
-    store.shell.closeModal()
+    closeModal()
   }
 
   useEffect(() => {
-    if (store.shell.isModalActive) {
+    if (isModalActive) {
       bottomSheetRef.current?.expand()
     } else {
       bottomSheetRef.current?.close()
     }
-  }, [store.shell.isModalActive, bottomSheetRef, activeModal?.name])
+  }, [isModalActive, bottomSheetRef, activeModal?.name])
 
   let needsSafeTopInset = false
   let snapPoints: (string | number)[] = DEFAULT_SNAPPOINTS
@@ -108,7 +111,7 @@ export const ModalsContainer = observer(function ModalsContainer() {
   } else if (activeModal?.name === 'user-add-remove-lists') {
     snapPoints = UserAddRemoveListsModal.snapPoints
     element = <UserAddRemoveListsModal.Component {...activeModal} />
-  } else if (activeModal?.name === 'list-add-user') {
+  } else if (activeModal?.name === 'list-add-remove-users') {
     snapPoints = ListAddUserModal.snapPoints
     element = <ListAddUserModal.Component {...activeModal} />
   } else if (activeModal?.name === 'delete-account') {
@@ -184,12 +187,12 @@ export const ModalsContainer = observer(function ModalsContainer() {
       snapPoints={snapPoints}
       topInset={topInset}
       handleHeight={HANDLE_HEIGHT}
-      index={store.shell.isModalActive ? 0 : -1}
+      index={isModalActive ? 0 : -1}
       enablePanDownToClose
       android_keyboardInputMode="adjustResize"
       keyboardBlurBehavior="restore"
       backdropComponent={
-        store.shell.isModalActive ? createCustomBackdrop(onClose) : undefined
+        isModalActive ? createCustomBackdrop(onClose) : undefined
       }
       handleIndicatorStyle={{backgroundColor: pal.text.color}}
       handleStyle={[styles.handle, pal.view]}
@@ -198,7 +201,7 @@ export const ModalsContainer = observer(function ModalsContainer() {
       {element}
     </BottomSheet>
   )
-})
+}
 
 const styles = StyleSheet.create({
   handle: {
diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx
index ede845378..c39ba1f51 100644
--- a/src/view/com/modals/Modal.web.tsx
+++ b/src/view/com/modals/Modal.web.tsx
@@ -1,11 +1,11 @@
 import React from 'react'
 import {TouchableWithoutFeedback, StyleSheet, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
-import {useStores} from 'state/index'
+import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import type {Modal as ModalIface} from 'state/models/ui/shell'
 
+import {useModals, useModalControls} from '#/state/modals'
+import type {Modal as ModalIface} from '#/state/modals'
 import * as ConfirmModal from './Confirm'
 import * as EditProfileModal from './EditProfile'
 import * as ProfilePreviewModal from './ProfilePreview'
@@ -13,7 +13,7 @@ import * as ServerInputModal from './ServerInput'
 import * as ReportModal from './report/Modal'
 import * as CreateOrEditListModal from './CreateOrEditList'
 import * as UserAddRemoveLists from './UserAddRemoveLists'
-import * as ListAddUserModal from './ListAddUser'
+import * as ListAddUserModal from './ListAddRemoveUsers'
 import * as DeleteAccountModal from './DeleteAccount'
 import * as RepostModal from './Repost'
 import * as SelfLabelModal from './SelfLabel'
@@ -33,28 +33,29 @@ import * as VerifyEmailModal from './VerifyEmail'
 import * as ChangeEmailModal from './ChangeEmail'
 import * as LinkWarningModal from './LinkWarning'
 
-export const ModalsContainer = observer(function ModalsContainer() {
-  const store = useStores()
+export function ModalsContainer() {
+  const {isModalActive, activeModals} = useModals()
 
-  if (!store.shell.isModalActive) {
+  if (!isModalActive) {
     return null
   }
 
   return (
     <>
-      {store.shell.activeModals.map((modal, i) => (
+      {activeModals.map((modal, i) => (
         <Modal key={`modal-${i}`} modal={modal} />
       ))}
     </>
   )
-})
+}
 
 function Modal({modal}: {modal: ModalIface}) {
-  const store = useStores()
+  const {isModalActive} = useModals()
+  const {closeModal} = useModalControls()
   const pal = usePalette('default')
   const {isMobile} = useWebMediaQueries()
 
-  if (!store.shell.isModalActive) {
+  if (!isModalActive) {
     return null
   }
 
@@ -62,7 +63,7 @@ function Modal({modal}: {modal: ModalIface}) {
     if (modal.name === 'crop-image' || modal.name === 'edit-image') {
       return // dont close on mask presses during crop
     }
-    store.shell.closeModal()
+    closeModal()
   }
   const onInnerPress = () => {
     // TODO: can we use prevent default?
@@ -84,7 +85,7 @@ function Modal({modal}: {modal: ModalIface}) {
     element = <CreateOrEditListModal.Component {...modal} />
   } else if (modal.name === 'user-add-remove-lists') {
     element = <UserAddRemoveLists.Component {...modal} />
-  } else if (modal.name === 'list-add-user') {
+  } else if (modal.name === 'list-add-remove-users') {
     element = <ListAddUserModal.Component {...modal} />
   } else if (modal.name === 'crop-image') {
     element = <CropImageModal.Component {...modal} />
@@ -129,7 +130,10 @@ function Modal({modal}: {modal: ModalIface}) {
   return (
     // eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors
     <TouchableWithoutFeedback onPress={onPressMask}>
-      <View style={styles.mask}>
+      <Animated.View
+        style={styles.mask}
+        entering={FadeIn.duration(150)}
+        exiting={FadeOut}>
         {/* eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors */}
         <TouchableWithoutFeedback onPress={onInnerPress}>
           <View
@@ -142,7 +146,7 @@ function Modal({modal}: {modal: ModalIface}) {
             {element}
           </View>
         </TouchableWithoutFeedback>
-      </View>
+      </Animated.View>
     </TouchableWithoutFeedback>
   )
 }
diff --git a/src/view/com/modals/ModerationDetails.tsx b/src/view/com/modals/ModerationDetails.tsx
index c01312d69..c117023d4 100644
--- a/src/view/com/modals/ModerationDetails.tsx
+++ b/src/view/com/modals/ModerationDetails.tsx
@@ -1,7 +1,6 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
 import {ModerationUI} from '@atproto/api'
-import {useStores} from 'state/index'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {s} from 'lib/styles'
 import {Text} from '../util/text/Text'
@@ -10,6 +9,7 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb} from 'platform/detection'
 import {listUriToHref} from 'lib/strings/url-helpers'
 import {Button} from '../util/forms/Button'
+import {useModalControls} from '#/state/modals'
 
 export const snapPoints = [300]
 
@@ -20,7 +20,7 @@ export function Component({
   context: 'account' | 'content'
   moderation: ModerationUI
 }) {
-  const store = useStores()
+  const {closeModal} = useModalControls()
   const {isMobile} = useWebMediaQueries()
   const pal = usePalette('default')
 
@@ -102,7 +102,9 @@ export function Component({
       <Button
         type="primary"
         style={styles.btn}
-        onPress={() => store.shell.closeModal()}>
+        onPress={() => {
+          closeModal()
+        }}>
         <Text type="button-lg" style={[pal.textLight, s.textCenter, s.white]}>
           Okay
         </Text>
diff --git a/src/view/com/modals/ProfilePreview.tsx b/src/view/com/modals/ProfilePreview.tsx
index dad02aa5e..edfbf6a82 100644
--- a/src/view/com/modals/ProfilePreview.tsx
+++ b/src/view/com/modals/ProfilePreview.tsx
@@ -1,27 +1,81 @@
 import React, {useState, useEffect} from 'react'
 import {ActivityIndicator, StyleSheet, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
+import {AppBskyActorDefs, ModerationOpts, moderateProfile} from '@atproto/api'
 import {ThemedText} from '../util/text/ThemedText'
-import {useStores} from 'state/index'
-import {ProfileModel} from 'state/models/content/profile'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {ProfileHeader} from '../profile/ProfileHeader'
 import {InfoCircleIcon} from 'lib/icons'
 import {useNavigationState} from '@react-navigation/native'
 import {s} from 'lib/styles'
+import {useModerationOpts} from '#/state/queries/preferences'
+import {useProfileQuery} from '#/state/queries/profile'
+import {ErrorScreen} from '../util/error/ErrorScreen'
+import {CenteredView} from '../util/Views'
+import {cleanError} from '#/lib/strings/errors'
+import {useProfileShadow} from '#/state/cache/profile-shadow'
 
 export const snapPoints = [520, '100%']
 
-export const Component = observer(function ProfilePreviewImpl({
-  did,
+export function Component({did}: {did: string}) {
+  const pal = usePalette('default')
+  const moderationOpts = useModerationOpts()
+  const {
+    data: profile,
+    error: profileError,
+    refetch: refetchProfile,
+    isFetching: isFetchingProfile,
+  } = useProfileQuery({
+    did: did,
+  })
+
+  if (isFetchingProfile || !moderationOpts) {
+    return (
+      <CenteredView style={[pal.view, s.flex1]}>
+        <ProfileHeader
+          profile={null}
+          moderation={null}
+          isProfilePreview={true}
+        />
+      </CenteredView>
+    )
+  }
+  if (profileError) {
+    return (
+      <ErrorScreen
+        title="Oops!"
+        message={cleanError(profileError)}
+        onPressTryAgain={refetchProfile}
+      />
+    )
+  }
+  if (profile && moderationOpts) {
+    return <ComponentLoaded profile={profile} moderationOpts={moderationOpts} />
+  }
+  // should never happen
+  return (
+    <ErrorScreen
+      title="Oops!"
+      message="Something went wrong and we're not sure what."
+      onPressTryAgain={refetchProfile}
+    />
+  )
+}
+
+function ComponentLoaded({
+  profile: profileUnshadowed,
+  moderationOpts,
 }: {
-  did: string
+  profile: AppBskyActorDefs.ProfileViewDetailed
+  moderationOpts: ModerationOpts
 }) {
-  const store = useStores()
   const pal = usePalette('default')
-  const [model] = useState(new ProfileModel(store, {actor: did}))
+  const profile = useProfileShadow(profileUnshadowed)
   const {screen} = useAnalytics()
+  const moderation = React.useMemo(
+    () => moderateProfile(profile, moderationOpts),
+    [profile, moderationOpts],
+  )
 
   // track the navigator state to detect if a page-load occurred
   const navState = useNavigationState(state => state)
@@ -30,16 +84,15 @@ export const Component = observer(function ProfilePreviewImpl({
 
   useEffect(() => {
     screen('Profile:Preview')
-    model.setup()
-  }, [model, screen])
+  }, [screen])
 
   return (
     <View testID="profilePreview" style={[pal.view, s.flex1]}>
       <View style={[styles.headerWrapper]}>
         <ProfileHeader
-          view={model}
+          profile={profile}
+          moderation={moderation}
           hideBackButton
-          onRefreshAll={() => {}}
           isProfilePreview
         />
       </View>
@@ -59,7 +112,7 @@ export const Component = observer(function ProfilePreviewImpl({
       </View>
     </View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   headerWrapper: {
diff --git a/src/view/com/modals/Repost.tsx b/src/view/com/modals/Repost.tsx
index b1862ecbd..a72da29b4 100644
--- a/src/view/com/modals/Repost.tsx
+++ b/src/view/com/modals/Repost.tsx
@@ -1,12 +1,14 @@
 import React from 'react'
 import {StyleSheet, TouchableOpacity, View} from 'react-native'
 import LinearGradient from 'react-native-linear-gradient'
-import {useStores} from 'state/index'
 import {s, colors, gradients} from 'lib/styles'
 import {Text} from '../util/text/Text'
 import {usePalette} from 'lib/hooks/usePalette'
 import {RepostIcon} from 'lib/icons'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
 
 export const snapPoints = [250]
 
@@ -20,10 +22,11 @@ export function Component({
   isReposted: boolean
   // TODO: Add author into component
 }) {
-  const store = useStores()
   const pal = usePalette('default')
+  const {_} = useLingui()
+  const {closeModal} = useModalControls()
   const onPress = async () => {
-    store.shell.closeModal()
+    closeModal()
   }
 
   return (
@@ -38,7 +41,7 @@ export function Component({
           accessibilityHint={isReposted ? 'Remove repost' : 'Repost '}>
           <RepostIcon strokeWidth={2} size={24} style={s.blue3} />
           <Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
-            {!isReposted ? 'Repost' : 'Undo repost'}
+            <Trans>{!isReposted ? 'Repost' : 'Undo repost'}</Trans>
           </Text>
         </TouchableOpacity>
         <TouchableOpacity
@@ -46,11 +49,11 @@ export function Component({
           style={[styles.actionBtn]}
           onPress={onQuote}
           accessibilityRole="button"
-          accessibilityLabel="Quote post"
+          accessibilityLabel={_(msg`Quote post`)}
           accessibilityHint="">
           <FontAwesomeIcon icon="quote-left" size={24} style={s.blue3} />
           <Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
-            Quote Post
+            <Trans>Quote Post</Trans>
           </Text>
         </TouchableOpacity>
       </View>
@@ -58,7 +61,7 @@ export function Component({
         testID="cancelBtn"
         onPress={onPress}
         accessibilityRole="button"
-        accessibilityLabel="Cancel quote post"
+        accessibilityLabel={_(msg`Cancel quote post`)}
         accessibilityHint=""
         onAccessibilityEscape={onPress}>
         <LinearGradient
@@ -66,7 +69,9 @@ export function Component({
           start={{x: 0, y: 0}}
           end={{x: 1, y: 1}}
           style={[styles.btn]}>
-          <Text style={[s.white, s.bold, s.f18]}>Cancel</Text>
+          <Text style={[s.white, s.bold, s.f18]}>
+            <Trans>Cancel</Trans>
+          </Text>
         </LinearGradient>
       </TouchableOpacity>
     </View>
diff --git a/src/view/com/modals/SelfLabel.tsx b/src/view/com/modals/SelfLabel.tsx
index 820f2895b..092dd2d32 100644
--- a/src/view/com/modals/SelfLabel.tsx
+++ b/src/view/com/modals/SelfLabel.tsx
@@ -1,8 +1,6 @@
 import React, {useState} from 'react'
 import {StyleSheet, TouchableOpacity, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {Text} from '../util/text/Text'
-import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@@ -10,12 +8,15 @@ import {isWeb} from 'platform/detection'
 import {Button} from '../util/forms/Button'
 import {SelectableBtn} from '../util/forms/SelectableBtn'
 import {ScrollView} from 'view/com/modals/util'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
 
 const ADULT_CONTENT_LABELS = ['sexual', 'nudity', 'porn']
 
 export const snapPoints = ['50%']
 
-export const Component = observer(function Component({
+export function Component({
   labels,
   hasMedia,
   onChange,
@@ -25,9 +26,10 @@ export const Component = observer(function Component({
   onChange: (labels: string[]) => void
 }) {
   const pal = usePalette('default')
-  const store = useStores()
+  const {closeModal} = useModalControls()
   const {isMobile} = useWebMediaQueries()
   const [selected, setSelected] = useState(labels)
+  const {_} = useLingui()
 
   const toggleAdultLabel = (label: string) => {
     const hadLabel = selected.includes(label)
@@ -51,7 +53,7 @@ export const Component = observer(function Component({
     <View testID="selfLabelModal" style={[pal.view, styles.container]}>
       <View style={styles.titleSection}>
         <Text type="title-lg" style={[pal.text, styles.title]}>
-          Add a content warning
+          <Trans>Add a content warning</Trans>
         </Text>
       </View>
 
@@ -70,7 +72,7 @@ export const Component = observer(function Component({
               paddingBottom: 8,
             }}>
             <Text type="title" style={pal.text}>
-              Adult Content
+              <Trans>Adult Content</Trans>
             </Text>
             {hasAdultSelection ? (
               <Button
@@ -78,7 +80,7 @@ export const Component = observer(function Component({
                 onPress={removeAdultLabel}
                 style={{paddingTop: 0, paddingBottom: 0, paddingRight: 0}}>
                 <Text type="md" style={pal.link}>
-                  Remove
+                  <Trans>Remove</Trans>
                 </Text>
               </Button>
             ) : null}
@@ -116,23 +118,25 @@ export const Component = observer(function Component({
 
               <Text style={[pal.text, styles.adultExplainer]}>
                 {selected.includes('sexual') ? (
-                  <>Pictures meant for adults.</>
+                  <Trans>Pictures meant for adults.</Trans>
                 ) : selected.includes('nudity') ? (
-                  <>Artistic or non-erotic nudity.</>
+                  <Trans>Artistic or non-erotic nudity.</Trans>
                 ) : selected.includes('porn') ? (
-                  <>Sexual activity or erotic nudity.</>
+                  <Trans>Sexual activity or erotic nudity.</Trans>
                 ) : (
-                  <>If none are selected, suitable for all ages.</>
+                  <Trans>If none are selected, suitable for all ages.</Trans>
                 )}
               </Text>
             </>
           ) : (
             <View>
               <Text style={[pal.textLight]}>
-                <Text type="md-bold" style={[pal.textLight]}>
-                  Not Applicable
+                <Text type="md-bold" style={[pal.textLight, s.mr5]}>
+                  <Trans>Not Applicable.</Trans>
                 </Text>
-                . This warning is only available for posts with media attached.
+                <Trans>
+                  This warning is only available for posts with media attached.
+                </Trans>
               </Text>
             </View>
           )}
@@ -143,18 +147,20 @@ export const Component = observer(function Component({
         <TouchableOpacity
           testID="confirmBtn"
           onPress={() => {
-            store.shell.closeModal()
+            closeModal()
           }}
           style={styles.btn}
           accessibilityRole="button"
-          accessibilityLabel="Confirm"
+          accessibilityLabel={_(msg`Confirm`)}
           accessibilityHint="">
-          <Text style={[s.white, s.bold, s.f18]}>Done</Text>
+          <Text style={[s.white, s.bold, s.f18]}>
+            <Trans>Done</Trans>
+          </Text>
         </TouchableOpacity>
       </View>
     </View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   container: {
diff --git a/src/view/com/modals/ServerInput.tsx b/src/view/com/modals/ServerInput.tsx
index 13b21fe22..b30293859 100644
--- a/src/view/com/modals/ServerInput.tsx
+++ b/src/view/com/modals/ServerInput.tsx
@@ -6,33 +6,36 @@ import {
 } from '@fortawesome/react-native-fontawesome'
 import {ScrollView, TextInput} from './util'
 import {Text} from '../util/text/Text'
-import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useTheme} from 'lib/ThemeContext'
-import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'state/index'
+import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'lib/constants'
 import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
 
 export const snapPoints = ['80%']
 
 export function Component({onSelect}: {onSelect: (url: string) => void}) {
   const theme = useTheme()
   const pal = usePalette('default')
-  const store = useStores()
   const [customUrl, setCustomUrl] = useState<string>('')
+  const {_} = useLingui()
+  const {closeModal} = useModalControls()
 
   const doSelect = (url: string) => {
     if (!url.startsWith('http://') && !url.startsWith('https://')) {
       url = `https://${url}`
     }
-    store.shell.closeModal()
+    closeModal()
     onSelect(url)
   }
 
   return (
     <View style={[pal.view, s.flex1]} testID="serverInputModal">
       <Text type="2xl-bold" style={[pal.text, s.textCenter]}>
-        Choose Service
+        <Trans>Choose Service</Trans>
       </Text>
       <ScrollView style={styles.inner}>
         <View style={styles.group}>
@@ -43,7 +46,9 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
                 style={styles.btn}
                 onPress={() => doSelect(LOCAL_DEV_SERVICE)}
                 accessibilityRole="button">
-                <Text style={styles.btnText}>Local dev server</Text>
+                <Text style={styles.btnText}>
+                  <Trans>Local dev server</Trans>
+                </Text>
                 <FontAwesomeIcon
                   icon="arrow-right"
                   style={s.white as FontAwesomeIconStyle}
@@ -53,7 +58,9 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
                 style={styles.btn}
                 onPress={() => doSelect(STAGING_SERVICE)}
                 accessibilityRole="button">
-                <Text style={styles.btnText}>Staging</Text>
+                <Text style={styles.btnText}>
+                  <Trans>Staging</Trans>
+                </Text>
                 <FontAwesomeIcon
                   icon="arrow-right"
                   style={s.white as FontAwesomeIconStyle}
@@ -65,9 +72,11 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
             style={styles.btn}
             onPress={() => doSelect(PROD_SERVICE)}
             accessibilityRole="button"
-            accessibilityLabel="Select Bluesky Social"
+            accessibilityLabel={_(msg`Select Bluesky Social`)}
             accessibilityHint="Sets Bluesky Social as your service provider">
-            <Text style={styles.btnText}>Bluesky.Social</Text>
+            <Text style={styles.btnText}>
+              <Trans>Bluesky.Social</Trans>
+            </Text>
             <FontAwesomeIcon
               icon="arrow-right"
               style={s.white as FontAwesomeIconStyle}
@@ -75,7 +84,9 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
           </TouchableOpacity>
         </View>
         <View style={styles.group}>
-          <Text style={[pal.text, styles.label]}>Other service</Text>
+          <Text style={[pal.text, styles.label]}>
+            <Trans>Other service</Trans>
+          </Text>
           <View style={s.flexRow}>
             <TextInput
               testID="customServerTextInput"
@@ -88,7 +99,7 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
               keyboardAppearance={theme.colorScheme}
               value={customUrl}
               onChangeText={setCustomUrl}
-              accessibilityLabel="Custom domain"
+              accessibilityLabel={_(msg`Custom domain`)}
               // TODO: Simplify this wording further to be understandable by everyone
               accessibilityHint="Use your domain as your Bluesky client service provider"
             />
diff --git a/src/view/com/modals/SwitchAccount.tsx b/src/view/com/modals/SwitchAccount.tsx
index d5fa32692..38e1ce1e0 100644
--- a/src/view/com/modals/SwitchAccount.tsx
+++ b/src/view/com/modals/SwitchAccount.tsx
@@ -6,7 +6,6 @@ import {
   View,
 } from 'react-native'
 import {Text} from '../util/text/Text'
-import {useStores} from 'state/index'
 import {s} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useAnalytics} from 'lib/analytics/analytics'
@@ -17,88 +16,114 @@ import {Link} from '../util/Link'
 import {makeProfileLink} from 'lib/routes/links'
 import {BottomSheetScrollView} from '@gorhom/bottom-sheet'
 import {Haptics} from 'lib/haptics'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useSession, useSessionApi, SessionAccount} from '#/state/session'
+import {useProfileQuery} from '#/state/queries/profile'
 
 export const snapPoints = ['40%', '90%']
 
-export function Component({}: {}) {
+function SwitchAccountCard({account}: {account: SessionAccount}) {
   const pal = usePalette('default')
+  const {_} = useLingui()
   const {track} = useAnalytics()
+  const {isSwitchingAccounts, currentAccount} = useSession()
+  const {logout} = useSessionApi()
+  const {data: profile} = useProfileQuery({did: account.did})
+  const isCurrentAccount = account.did === currentAccount?.did
+  const {onPressSwitchAccount} = useAccountSwitcher()
+
+  const onPressSignout = React.useCallback(() => {
+    track('Settings:SignOutButtonClicked')
+    logout()
+  }, [track, logout])
 
-  const store = useStores()
-  const [isSwitching, _, onPressSwitchAccount] = useAccountSwitcher()
+  const contents = (
+    <View style={[pal.view, styles.linkCard]}>
+      <View style={styles.avi}>
+        <UserAvatar size={40} avatar={profile?.avatar} />
+      </View>
+      <View style={[s.flex1]}>
+        <Text type="md-bold" style={pal.text} numberOfLines={1}>
+          {profile?.displayName || account?.handle}
+        </Text>
+        <Text type="sm" style={pal.textLight} numberOfLines={1}>
+          {account?.handle}
+        </Text>
+      </View>
+
+      {isCurrentAccount ? (
+        <TouchableOpacity
+          testID="signOutBtn"
+          onPress={isSwitchingAccounts ? undefined : onPressSignout}
+          accessibilityRole="button"
+          accessibilityLabel={_(msg`Sign out`)}
+          accessibilityHint={`Signs ${profile?.displayName} out of Bluesky`}>
+          <Text type="lg" style={pal.link}>
+            <Trans>Sign out</Trans>
+          </Text>
+        </TouchableOpacity>
+      ) : (
+        <AccountDropdownBtn account={account} />
+      )}
+    </View>
+  )
+
+  return isCurrentAccount ? (
+    <Link
+      href={makeProfileLink({
+        did: currentAccount.did,
+        handle: currentAccount.handle,
+      })}
+      title={_(msg`Your profile`)}
+      noFeedback>
+      {contents}
+    </Link>
+  ) : (
+    <TouchableOpacity
+      testID={`switchToAccountBtn-${account.handle}`}
+      key={account.did}
+      style={[isSwitchingAccounts && styles.dimmed]}
+      onPress={
+        isSwitchingAccounts ? undefined : () => onPressSwitchAccount(account)
+      }
+      accessibilityRole="button"
+      accessibilityLabel={`Switch to ${account.handle}`}
+      accessibilityHint="Switches the account you are logged in to">
+      {contents}
+    </TouchableOpacity>
+  )
+}
+
+export function Component({}: {}) {
+  const pal = usePalette('default')
+  const {isSwitchingAccounts, currentAccount, accounts} = useSession()
 
   React.useEffect(() => {
     Haptics.default()
   })
 
-  const onPressSignout = React.useCallback(() => {
-    track('Settings:SignOutButtonClicked')
-    store.session.logout()
-  }, [track, store])
-
   return (
     <BottomSheetScrollView
       style={[styles.container, pal.view]}
       contentContainerStyle={[styles.innerContainer, pal.view]}>
       <Text type="title-xl" style={[styles.title, pal.text]}>
-        Switch Account
+        <Trans>Switch Account</Trans>
       </Text>
-      {isSwitching ? (
+
+      {isSwitchingAccounts || !currentAccount ? (
         <View style={[pal.view, styles.linkCard]}>
           <ActivityIndicator />
         </View>
       ) : (
-        <Link href={makeProfileLink(store.me)} title="Your profile" noFeedback>
-          <View style={[pal.view, styles.linkCard]}>
-            <View style={styles.avi}>
-              <UserAvatar size={40} avatar={store.me.avatar} />
-            </View>
-            <View style={[s.flex1]}>
-              <Text type="md-bold" style={pal.text} numberOfLines={1}>
-                {store.me.displayName || store.me.handle}
-              </Text>
-              <Text type="sm" style={pal.textLight} numberOfLines={1}>
-                {store.me.handle}
-              </Text>
-            </View>
-            <TouchableOpacity
-              testID="signOutBtn"
-              onPress={isSwitching ? undefined : onPressSignout}
-              accessibilityRole="button"
-              accessibilityLabel="Sign out"
-              accessibilityHint={`Signs ${store.me.displayName} out of Bluesky`}>
-              <Text type="lg" style={pal.link}>
-                Sign out
-              </Text>
-            </TouchableOpacity>
-          </View>
-        </Link>
+        <SwitchAccountCard account={currentAccount} />
       )}
-      {store.session.switchableAccounts.map(account => (
-        <TouchableOpacity
-          testID={`switchToAccountBtn-${account.handle}`}
-          key={account.did}
-          style={[pal.view, styles.linkCard, isSwitching && styles.dimmed]}
-          onPress={
-            isSwitching ? undefined : () => onPressSwitchAccount(account)
-          }
-          accessibilityRole="button"
-          accessibilityLabel={`Switch to ${account.handle}`}
-          accessibilityHint="Switches the account you are logged in to">
-          <View style={styles.avi}>
-            <UserAvatar size={40} avatar={account.aviUrl} />
-          </View>
-          <View style={[s.flex1]}>
-            <Text type="md-bold" style={pal.text}>
-              {account.displayName || account.handle}
-            </Text>
-            <Text type="sm" style={pal.textLight}>
-              {account.handle}
-            </Text>
-          </View>
-          <AccountDropdownBtn handle={account.handle} />
-        </TouchableOpacity>
-      ))}
+
+      {accounts
+        .filter(a => a.did !== currentAccount?.did)
+        .map(account => (
+          <SwitchAccountCard key={account.did} account={account} />
+        ))}
     </BottomSheetScrollView>
   )
 }
diff --git a/src/view/com/modals/UserAddRemoveLists.tsx b/src/view/com/modals/UserAddRemoveLists.tsx
index aeec2e87f..8c3dc8bb7 100644
--- a/src/view/com/modals/UserAddRemoveLists.tsx
+++ b/src/view/com/modals/UserAddRemoveLists.tsx
@@ -1,30 +1,32 @@
 import React, {useCallback} from 'react'
-import {observer} from 'mobx-react-lite'
-import {ActivityIndicator, Pressable, StyleSheet, View} from 'react-native'
+import {ActivityIndicator, StyleSheet, View} from 'react-native'
 import {AppBskyGraphDefs as GraphDefs} from '@atproto/api'
-import {
-  FontAwesomeIcon,
-  FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
 import {Text} from '../util/text/Text'
 import {UserAvatar} from '../util/UserAvatar'
-import {ListsList} from '../lists/ListsList'
-import {ListsListModel} from 'state/models/lists/lists-list'
-import {ListMembershipModel} from 'state/models/content/list-membership'
+import {MyLists} from '../lists/MyLists'
 import {Button} from '../util/forms/Button'
 import * as Toast from '../util/Toast'
-import {useStores} from 'state/index'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {sanitizeHandle} from 'lib/strings/handles'
 import {s} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb, isAndroid} from 'platform/detection'
-import isEqual from 'lodash.isequal'
-import {logger} from '#/logger'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
+import {
+  useDangerousListMembershipsQuery,
+  getMembership,
+  ListMembersip,
+  useListMembershipAddMutation,
+  useListMembershipRemoveMutation,
+} from '#/state/queries/list-memberships'
+import {cleanError} from '#/lib/strings/errors'
+import {useSession} from '#/state/session'
 
 export const snapPoints = ['fullscreen']
 
-export const Component = observer(function UserAddRemoveListsImpl({
+export function Component({
   subject,
   displayName,
   onAdd,
@@ -35,191 +37,161 @@ export const Component = observer(function UserAddRemoveListsImpl({
   onAdd?: (listUri: string) => void
   onRemove?: (listUri: string) => void
 }) {
-  const store = useStores()
+  const {closeModal} = useModalControls()
   const pal = usePalette('default')
-  const palPrimary = usePalette('primary')
-  const palInverted = usePalette('inverted')
-  const [originalSelections, setOriginalSelections] = React.useState<string[]>(
-    [],
-  )
-  const [selected, setSelected] = React.useState<string[]>([])
-  const [membershipsLoaded, setMembershipsLoaded] = React.useState(false)
+  const {_} = useLingui()
+  const {data: memberships} = useDangerousListMembershipsQuery()
 
-  const listsList: ListsListModel = React.useMemo(
-    () => new ListsListModel(store, store.me.did),
-    [store],
-  )
-  const memberships: ListMembershipModel = React.useMemo(
-    () => new ListMembershipModel(store, subject),
-    [store, subject],
-  )
-  React.useEffect(() => {
-    listsList.refresh()
-    memberships.fetch().then(
-      () => {
-        const ids = memberships.memberships.map(m => m.value.list)
-        setOriginalSelections(ids)
-        setSelected(ids)
-        setMembershipsLoaded(true)
-      },
-      err => {
-        logger.error('Failed to fetch memberships', {error: err})
-      },
-    )
-  }, [memberships, listsList, store, setSelected, setMembershipsLoaded])
-
-  const onPressCancel = useCallback(() => {
-    store.shell.closeModal()
-  }, [store])
-
-  const onPressSave = useCallback(async () => {
-    let changes
-    try {
-      changes = await memberships.updateTo(selected)
-    } catch (err) {
-      logger.error('Failed to update memberships', {error: err})
-      return
-    }
-    Toast.show('Lists updated')
-    for (const uri of changes.added) {
-      onAdd?.(uri)
-    }
-    for (const uri of changes.removed) {
-      onRemove?.(uri)
-    }
-    store.shell.closeModal()
-  }, [store, selected, memberships, onAdd, onRemove])
-
-  const onToggleSelected = useCallback(
-    (uri: string) => {
-      if (selected.includes(uri)) {
-        setSelected(selected.filter(uri2 => uri2 !== uri))
-      } else {
-        setSelected([...selected, uri])
-      }
-    },
-    [selected, setSelected],
-  )
-
-  const renderItem = useCallback(
-    (list: GraphDefs.ListView, index: number) => {
-      const isSelected = selected.includes(list.uri)
-      return (
-        <Pressable
-          testID={`toggleBtn-${list.name}`}
-          style={[
-            styles.listItem,
-            pal.border,
-            {
-              opacity: membershipsLoaded ? 1 : 0.5,
-              borderTopWidth: index === 0 ? 0 : 1,
-            },
-          ]}
-          accessibilityLabel={`${isSelected ? 'Remove from' : 'Add to'} ${
-            list.name
-          }`}
-          accessibilityHint=""
-          disabled={!membershipsLoaded}
-          onPress={() => onToggleSelected(list.uri)}>
-          <View style={styles.listItemAvi}>
-            <UserAvatar size={40} avatar={list.avatar} />
-          </View>
-          <View style={styles.listItemContent}>
-            <Text
-              type="lg"
-              style={[s.bold, pal.text]}
-              numberOfLines={1}
-              lineHeight={1.2}>
-              {sanitizeDisplayName(list.name)}
-            </Text>
-            <Text type="md" style={[pal.textLight]} numberOfLines={1}>
-              {list.purpose === 'app.bsky.graph.defs#curatelist' &&
-                'User list '}
-              {list.purpose === 'app.bsky.graph.defs#modlist' &&
-                'Moderation list '}
-              by{' '}
-              {list.creator.did === store.me.did
-                ? 'you'
-                : sanitizeHandle(list.creator.handle, '@')}
-            </Text>
-          </View>
-          {membershipsLoaded && (
-            <View
-              style={
-                isSelected
-                  ? [styles.checkbox, palPrimary.border, palPrimary.view]
-                  : [styles.checkbox, pal.borderDark]
-              }>
-              {isSelected && (
-                <FontAwesomeIcon
-                  icon="check"
-                  style={palInverted.text as FontAwesomeIconStyle}
-                />
-              )}
-            </View>
-          )}
-        </Pressable>
-      )
-    },
-    [
-      pal,
-      palPrimary,
-      palInverted,
-      onToggleSelected,
-      selected,
-      store.me.did,
-      membershipsLoaded,
-    ],
-  )
-
-  // Only show changes button if there are some items on the list to choose from AND user has made changes in selection
-  const canSaveChanges =
-    !listsList.isEmpty && !isEqual(selected, originalSelections)
+  const onPressDone = useCallback(() => {
+    closeModal()
+  }, [closeModal])
 
   return (
     <View testID="userAddRemoveListsModal" style={s.hContentRegion}>
       <Text style={[styles.title, pal.text]}>
-        Update {displayName} in Lists
+        <Trans>Update {displayName} in Lists</Trans>
       </Text>
-      <ListsList
-        listsList={listsList}
+      <MyLists
+        filter="all"
         inline
-        renderItem={renderItem}
+        renderItem={(list, index) => (
+          <ListItem
+            index={index}
+            list={list}
+            memberships={memberships}
+            subject={subject}
+            onAdd={onAdd}
+            onRemove={onRemove}
+          />
+        )}
         style={[styles.list, pal.border]}
       />
       <View style={[styles.btns, pal.border]}>
         <Button
-          testID="cancelBtn"
+          testID="doneBtn"
           type="default"
-          onPress={onPressCancel}
+          onPress={onPressDone}
           style={styles.footerBtn}
-          accessibilityLabel="Cancel"
+          accessibilityLabel={_(msg`Done`)}
           accessibilityHint=""
-          onAccessibilityEscape={onPressCancel}
-          label="Cancel"
+          onAccessibilityEscape={onPressDone}
+          label="Done"
         />
-        {canSaveChanges && (
+      </View>
+    </View>
+  )
+}
+
+function ListItem({
+  index,
+  list,
+  memberships,
+  subject,
+  onAdd,
+  onRemove,
+}: {
+  index: number
+  list: GraphDefs.ListView
+  memberships: ListMembersip[] | undefined
+  subject: string
+  onAdd?: (listUri: string) => void
+  onRemove?: (listUri: string) => void
+}) {
+  const pal = usePalette('default')
+  const {_} = useLingui()
+  const {currentAccount} = useSession()
+  const [isProcessing, setIsProcessing] = React.useState(false)
+  const membership = React.useMemo(
+    () => getMembership(memberships, list.uri, subject),
+    [memberships, list.uri, subject],
+  )
+  const listMembershipAddMutation = useListMembershipAddMutation()
+  const listMembershipRemoveMutation = useListMembershipRemoveMutation()
+
+  const onToggleMembership = useCallback(async () => {
+    if (typeof membership === 'undefined') {
+      return
+    }
+    setIsProcessing(true)
+    try {
+      if (membership === false) {
+        await listMembershipAddMutation.mutateAsync({
+          listUri: list.uri,
+          actorDid: subject,
+        })
+        Toast.show(_(msg`Added to list`))
+        onAdd?.(list.uri)
+      } else {
+        await listMembershipRemoveMutation.mutateAsync({
+          listUri: list.uri,
+          actorDid: subject,
+          membershipUri: membership,
+        })
+        Toast.show(_(msg`Removed from list`))
+        onRemove?.(list.uri)
+      }
+    } catch (e) {
+      Toast.show(cleanError(e))
+    } finally {
+      setIsProcessing(false)
+    }
+  }, [
+    _,
+    list,
+    subject,
+    membership,
+    setIsProcessing,
+    onAdd,
+    onRemove,
+    listMembershipAddMutation,
+    listMembershipRemoveMutation,
+  ])
+
+  return (
+    <View
+      testID={`toggleBtn-${list.name}`}
+      style={[
+        styles.listItem,
+        pal.border,
+        {
+          borderTopWidth: index === 0 ? 0 : 1,
+        },
+      ]}>
+      <View style={styles.listItemAvi}>
+        <UserAvatar size={40} avatar={list.avatar} />
+      </View>
+      <View style={styles.listItemContent}>
+        <Text
+          type="lg"
+          style={[s.bold, pal.text]}
+          numberOfLines={1}
+          lineHeight={1.2}>
+          {sanitizeDisplayName(list.name)}
+        </Text>
+        <Text type="md" style={[pal.textLight]} numberOfLines={1}>
+          {list.purpose === 'app.bsky.graph.defs#curatelist' && 'User list '}
+          {list.purpose === 'app.bsky.graph.defs#modlist' && 'Moderation list '}
+          by{' '}
+          {list.creator.did === currentAccount?.did
+            ? 'you'
+            : sanitizeHandle(list.creator.handle, '@')}
+        </Text>
+      </View>
+      <View>
+        {isProcessing || typeof membership === 'undefined' ? (
+          <ActivityIndicator />
+        ) : (
           <Button
-            testID="saveBtn"
-            type="primary"
-            onPress={onPressSave}
-            style={styles.footerBtn}
-            accessibilityLabel="Save changes"
-            accessibilityHint=""
-            onAccessibilityEscape={onPressSave}
-            label="Save Changes"
+            testID={`user-${subject}-addBtn`}
+            type="default"
+            label={membership === false ? _(msg`Add`) : _(msg`Remove`)}
+            onPress={onToggleMembership}
           />
         )}
-
-        {(listsList.isLoading || !membershipsLoaded) && (
-          <View style={styles.loadingContainer}>
-            <ActivityIndicator />
-          </View>
-        )}
       </View>
     </View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   container: {
diff --git a/src/view/com/modals/VerifyEmail.tsx b/src/view/com/modals/VerifyEmail.tsx
index 9fe8811b0..4376a3e45 100644
--- a/src/view/com/modals/VerifyEmail.tsx
+++ b/src/view/com/modals/VerifyEmail.tsx
@@ -8,18 +8,20 @@ import {
 } from 'react-native'
 import {Svg, Circle, Path} from 'react-native-svg'
 import {ScrollView, TextInput} from './util'
-import {observer} from 'mobx-react-lite'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {Text} from '../util/text/Text'
 import {Button} from '../util/forms/Button'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import * as Toast from '../util/Toast'
-import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb} from 'platform/detection'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {cleanError} from 'lib/strings/errors'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
+import {useSession, useSessionApi, getAgent} from '#/state/session'
 
 export const snapPoints = ['90%']
 
@@ -29,13 +31,11 @@ enum Stages {
   ConfirmCode,
 }
 
-export const Component = observer(function Component({
-  showReminder,
-}: {
-  showReminder?: boolean
-}) {
+export function Component({showReminder}: {showReminder?: boolean}) {
   const pal = usePalette('default')
-  const store = useStores()
+  const {currentAccount} = useSession()
+  const {updateCurrentAccount} = useSessionApi()
+  const {_} = useLingui()
   const [stage, setStage] = useState<Stages>(
     showReminder ? Stages.Reminder : Stages.Email,
   )
@@ -43,12 +43,13 @@ export const Component = observer(function Component({
   const [isProcessing, setIsProcessing] = useState<boolean>(false)
   const [error, setError] = useState<string>('')
   const {isMobile} = useWebMediaQueries()
+  const {openModal, closeModal} = useModalControls()
 
   const onSendEmail = async () => {
     setError('')
     setIsProcessing(true)
     try {
-      await store.agent.com.atproto.server.requestEmailConfirmation()
+      await getAgent().com.atproto.server.requestEmailConfirmation()
       setStage(Stages.ConfirmCode)
     } catch (e) {
       setError(cleanError(String(e)))
@@ -61,13 +62,13 @@ export const Component = observer(function Component({
     setError('')
     setIsProcessing(true)
     try {
-      await store.agent.com.atproto.server.confirmEmail({
-        email: (store.session.currentSession?.email || '').trim(),
+      await getAgent().com.atproto.server.confirmEmail({
+        email: (currentAccount?.email || '').trim(),
         token: confirmationCode.trim(),
       })
-      store.session.updateLocalAccountData({emailConfirmed: true})
+      updateCurrentAccount({emailConfirmed: true})
       Toast.show('Email verified')
-      store.shell.closeModal()
+      closeModal()
     } catch (e) {
       setError(cleanError(String(e)))
     } finally {
@@ -76,8 +77,8 @@ export const Component = observer(function Component({
   }
 
   const onEmailIncorrect = () => {
-    store.shell.closeModal()
-    store.shell.openModal({name: 'change-email'})
+    closeModal()
+    openModal({name: 'change-email'})
   }
 
   return (
@@ -96,21 +97,20 @@ export const Component = observer(function Component({
 
         <Text type="lg" style={[pal.textLight, {marginBottom: 10}]}>
           {stage === Stages.Reminder ? (
-            <>
+            <Trans>
               Your email has not yet been verified. This is an important
               security step which we recommend.
-            </>
+            </Trans>
           ) : stage === Stages.Email ? (
-            <>
+            <Trans>
               This is important in case you ever need to change your email or
               reset your password.
-            </>
+            </Trans>
           ) : stage === Stages.ConfirmCode ? (
-            <>
-              An email has been sent to{' '}
-              {store.session.currentSession?.email || ''}. It includes a
-              confirmation code which you can enter below.
-            </>
+            <Trans>
+              An email has been sent to {currentAccount?.email || ''}. It
+              includes a confirmation code which you can enter below.
+            </Trans>
           ) : (
             ''
           )}
@@ -125,12 +125,12 @@ export const Component = observer(function Component({
                 size={16}
               />
               <Text type="xl-medium" style={[pal.text, s.flex1, {minWidth: 0}]}>
-                {store.session.currentSession?.email || ''}
+                {currentAccount?.email || ''}
               </Text>
             </View>
             <Pressable
               accessibilityRole="link"
-              accessibilityLabel="Change my email"
+              accessibilityLabel={_(msg`Change my email`)}
               accessibilityHint=""
               onPress={onEmailIncorrect}
               style={styles.changeEmailLink}>
@@ -148,7 +148,7 @@ export const Component = observer(function Component({
             value={confirmationCode}
             onChangeText={setConfirmationCode}
             accessible={true}
-            accessibilityLabel="Confirmation code"
+            accessibilityLabel={_(msg`Confirmation code`)}
             accessibilityHint=""
             autoCapitalize="none"
             autoComplete="off"
@@ -172,7 +172,7 @@ export const Component = observer(function Component({
                   testID="getStartedBtn"
                   type="primary"
                   onPress={() => setStage(Stages.Email)}
-                  accessibilityLabel="Get Started"
+                  accessibilityLabel={_(msg`Get Started`)}
                   accessibilityHint=""
                   label="Get Started"
                   labelContainerStyle={{justifyContent: 'center', padding: 4}}
@@ -185,7 +185,7 @@ export const Component = observer(function Component({
                     testID="sendEmailBtn"
                     type="primary"
                     onPress={onSendEmail}
-                    accessibilityLabel="Send Confirmation Email"
+                    accessibilityLabel={_(msg`Send Confirmation Email`)}
                     accessibilityHint=""
                     label="Send Confirmation Email"
                     labelContainerStyle={{
@@ -197,7 +197,7 @@ export const Component = observer(function Component({
                   <Button
                     testID="haveCodeBtn"
                     type="default"
-                    accessibilityLabel="I have a code"
+                    accessibilityLabel={_(msg`I have a code`)}
                     accessibilityHint=""
                     label="I have a confirmation code"
                     labelContainerStyle={{
@@ -214,7 +214,7 @@ export const Component = observer(function Component({
                   testID="confirmBtn"
                   type="primary"
                   onPress={onConfirm}
-                  accessibilityLabel="Confirm"
+                  accessibilityLabel={_(msg`Confirm`)}
                   accessibilityHint=""
                   label="Confirm"
                   labelContainerStyle={{justifyContent: 'center', padding: 4}}
@@ -224,7 +224,9 @@ export const Component = observer(function Component({
               <Button
                 testID="cancelBtn"
                 type="default"
-                onPress={() => store.shell.closeModal()}
+                onPress={() => {
+                  closeModal()
+                }}
                 accessibilityLabel={
                   stage === Stages.Reminder ? 'Not right now' : 'Cancel'
                 }
@@ -239,7 +241,7 @@ export const Component = observer(function Component({
       </ScrollView>
     </SafeAreaView>
   )
-})
+}
 
 function ReminderIllustration() {
   const pal = usePalette('default')
diff --git a/src/view/com/modals/Waitlist.tsx b/src/view/com/modals/Waitlist.tsx
index 0fb371fe4..a31545c0a 100644
--- a/src/view/com/modals/Waitlist.tsx
+++ b/src/view/com/modals/Waitlist.tsx
@@ -12,19 +12,22 @@ import {
 } from '@fortawesome/react-native-fontawesome'
 import LinearGradient from 'react-native-linear-gradient'
 import {Text} from '../util/text/Text'
-import {useStores} from 'state/index'
 import {s, gradients} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useTheme} from 'lib/ThemeContext'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import {cleanError} from 'lib/strings/errors'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
 
 export const snapPoints = ['80%']
 
 export function Component({}: {}) {
   const pal = usePalette('default')
   const theme = useTheme()
-  const store = useStores()
+  const {_} = useLingui()
+  const {closeModal} = useModalControls()
   const [email, setEmail] = React.useState<string>('')
   const [isEmailSent, setIsEmailSent] = React.useState<boolean>(false)
   const [isProcessing, setIsProcessing] = React.useState<boolean>(false)
@@ -54,19 +57,21 @@ export function Component({}: {}) {
     setIsProcessing(false)
   }
   const onCancel = () => {
-    store.shell.closeModal()
+    closeModal()
   }
 
   return (
     <View style={[styles.container, pal.view]}>
       <View style={[styles.innerContainer, pal.view]}>
         <Text type="title-xl" style={[styles.title, pal.text]}>
-          Join the waitlist
+          <Trans>Join the waitlist</Trans>
         </Text>
         <Text type="lg" style={[styles.description, pal.text]}>
-          Bluesky uses invites to build a healthier community. If you don't know
-          anybody with an invite, you can sign up for the waitlist and we'll
-          send one soon.
+          <Trans>
+            Bluesky uses invites to build a healthier community. If you don't
+            know anybody with an invite, you can sign up for the waitlist and
+            we'll send one soon.
+          </Trans>
         </Text>
         <TextInput
           style={[styles.textInput, pal.borderDark, pal.text, s.mb10, s.mt10]}
@@ -80,7 +85,7 @@ export function Component({}: {}) {
           onSubmitEditing={onPressSignup}
           enterKeyHint="done"
           accessible={true}
-          accessibilityLabel="Email"
+          accessibilityLabel={_(msg`Email`)}
           accessibilityHint="Input your email to get on the Bluesky waitlist"
         />
         {error ? (
@@ -99,7 +104,9 @@ export function Component({}: {}) {
               style={pal.text as FontAwesomeIconStyle}
             />
             <Text style={[s.ml10, pal.text]}>
-              Your email has been saved! We&apos;ll be in touch soon.
+              <Trans>
+                Your email has been saved! We&apos;ll be in touch soon.
+              </Trans>
             </Text>
           </View>
         ) : (
@@ -114,7 +121,7 @@ export function Component({}: {}) {
                 end={{x: 1, y: 1}}
                 style={[styles.btn]}>
                 <Text type="button-lg" style={[s.white, s.bold]}>
-                  Join Waitlist
+                  <Trans>Join Waitlist</Trans>
                 </Text>
               </LinearGradient>
             </TouchableOpacity>
@@ -122,11 +129,11 @@ export function Component({}: {}) {
               style={[styles.btn, s.mt10]}
               onPress={onCancel}
               accessibilityRole="button"
-              accessibilityLabel="Cancel waitlist signup"
+              accessibilityLabel={_(msg`Cancel waitlist signup`)}
               accessibilityHint={`Exits signing up for waitlist with ${email}`}
               onAccessibilityEscape={onCancel}>
               <Text type="button-lg" style={pal.textLight}>
-                Cancel
+                <Trans>Cancel</Trans>
               </Text>
             </TouchableOpacity>
           </>
diff --git a/src/view/com/modals/crop-image/CropImage.web.tsx b/src/view/com/modals/crop-image/CropImage.web.tsx
index 8e35201d1..6f094a1fd 100644
--- a/src/view/com/modals/crop-image/CropImage.web.tsx
+++ b/src/view/com/modals/crop-image/CropImage.web.tsx
@@ -7,10 +7,12 @@ import {Text} from 'view/com/util/text/Text'
 import {Dimensions} from 'lib/media/types'
 import {getDataUriSize} from 'lib/media/util'
 import {s, gradients} from 'lib/styles'
-import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
 import {SquareIcon, RectWideIcon, RectTallIcon} from 'lib/icons'
 import {Image as RNImage} from 'react-native-image-crop-picker'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
 
 enum AspectRatio {
   Square = 'square',
@@ -33,8 +35,9 @@ export function Component({
   uri: string
   onSelect: (img?: RNImage) => void
 }) {
-  const store = useStores()
+  const {closeModal} = useModalControls()
   const pal = usePalette('default')
+  const {_} = useLingui()
   const [as, setAs] = React.useState<AspectRatio>(AspectRatio.Square)
   const [scale, setScale] = React.useState<number>(1)
   const editorRef = React.useRef<ImageEditor>(null)
@@ -43,7 +46,7 @@ export function Component({
 
   const onPressCancel = () => {
     onSelect(undefined)
-    store.shell.closeModal()
+    closeModal()
   }
   const onPressDone = () => {
     const canvas = editorRef.current?.getImageScaledToCanvas()
@@ -59,7 +62,7 @@ export function Component({
     } else {
       onSelect(undefined)
     }
-    store.shell.closeModal()
+    closeModal()
   }
 
   let cropperStyle
@@ -96,7 +99,7 @@ export function Component({
         <TouchableOpacity
           onPress={doSetAs(AspectRatio.Wide)}
           accessibilityRole="button"
-          accessibilityLabel="Wide"
+          accessibilityLabel={_(msg`Wide`)}
           accessibilityHint="Sets image aspect ratio to wide">
           <RectWideIcon
             size={24}
@@ -106,7 +109,7 @@ export function Component({
         <TouchableOpacity
           onPress={doSetAs(AspectRatio.Tall)}
           accessibilityRole="button"
-          accessibilityLabel="Tall"
+          accessibilityLabel={_(msg`Tall`)}
           accessibilityHint="Sets image aspect ratio to tall">
           <RectTallIcon
             size={24}
@@ -116,7 +119,7 @@ export function Component({
         <TouchableOpacity
           onPress={doSetAs(AspectRatio.Square)}
           accessibilityRole="button"
-          accessibilityLabel="Square"
+          accessibilityLabel={_(msg`Square`)}
           accessibilityHint="Sets image aspect ratio to square">
           <SquareIcon
             size={24}
@@ -128,7 +131,7 @@ export function Component({
         <TouchableOpacity
           onPress={onPressCancel}
           accessibilityRole="button"
-          accessibilityLabel="Cancel image crop"
+          accessibilityLabel={_(msg`Cancel image crop`)}
           accessibilityHint="Exits image cropping process">
           <Text type="xl" style={pal.link}>
             Cancel
@@ -138,7 +141,7 @@ export function Component({
         <TouchableOpacity
           onPress={onPressDone}
           accessibilityRole="button"
-          accessibilityLabel="Save image crop"
+          accessibilityLabel={_(msg`Save image crop`)}
           accessibilityHint="Saves image crop settings">
           <LinearGradient
             colors={[gradients.blueLight.start, gradients.blueLight.end]}
@@ -146,7 +149,7 @@ export function Component({
             end={{x: 1, y: 1}}
             style={[styles.btn]}>
             <Text type="xl-medium" style={s.white}>
-              Done
+              <Trans>Done</Trans>
             </Text>
           </LinearGradient>
         </TouchableOpacity>
diff --git a/src/view/com/modals/lang-settings/ConfirmLanguagesButton.tsx b/src/view/com/modals/lang-settings/ConfirmLanguagesButton.tsx
index c2d0c222a..91e11a19c 100644
--- a/src/view/com/modals/lang-settings/ConfirmLanguagesButton.tsx
+++ b/src/view/com/modals/lang-settings/ConfirmLanguagesButton.tsx
@@ -4,6 +4,8 @@ import LinearGradient from 'react-native-linear-gradient'
 import {s, colors, gradients} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 
 export const ConfirmLanguagesButton = ({
   onPress,
@@ -13,6 +15,7 @@ export const ConfirmLanguagesButton = ({
   extraText?: string
 }) => {
   const pal = usePalette('default')
+  const {_} = useLingui()
   const {isMobile} = useWebMediaQueries()
   return (
     <View
@@ -28,14 +31,16 @@ export const ConfirmLanguagesButton = ({
         testID="confirmContentLanguagesBtn"
         onPress={onPress}
         accessibilityRole="button"
-        accessibilityLabel="Confirm content language settings"
+        accessibilityLabel={_(msg`Confirm content language settings`)}
         accessibilityHint="">
         <LinearGradient
           colors={[gradients.blueLight.start, gradients.blueLight.end]}
           start={{x: 0, y: 0}}
           end={{x: 1, y: 1}}
           style={[styles.btn]}>
-          <Text style={[s.white, s.bold, s.f18]}>Done{extraText}</Text>
+          <Text style={[s.white, s.bold, s.f18]}>
+            <Trans>Done{extraText}</Trans>
+          </Text>
         </LinearGradient>
       </Pressable>
     </View>
diff --git a/src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx b/src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx
index 910522f90..b8c125b65 100644
--- a/src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx
+++ b/src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx
@@ -1,7 +1,6 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
 import {ScrollView} from '../util'
-import {useStores} from 'state/index'
 import {Text} from '../../util/text/Text'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@@ -9,16 +8,24 @@ import {deviceLocales} from 'platform/detection'
 import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
 import {LanguageToggle} from './LanguageToggle'
 import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
+import {Trans} from '@lingui/macro'
+import {useModalControls} from '#/state/modals'
+import {
+  useLanguagePrefs,
+  useLanguagePrefsApi,
+} from '#/state/preferences/languages'
 
 export const snapPoints = ['100%']
 
 export function Component({}: {}) {
-  const store = useStores()
+  const {closeModal} = useModalControls()
+  const langPrefs = useLanguagePrefs()
+  const setLangPrefs = useLanguagePrefsApi()
   const pal = usePalette('default')
   const {isMobile} = useWebMediaQueries()
   const onPressDone = React.useCallback(() => {
-    store.shell.closeModal()
-  }, [store])
+    closeModal()
+  }, [closeModal])
 
   const languages = React.useMemo(() => {
     const langs = LANGUAGES.filter(
@@ -29,23 +36,23 @@ export function Component({}: {}) {
     // sort so that device & selected languages are on top, then alphabetically
     langs.sort((a, b) => {
       const hasA =
-        store.preferences.hasContentLanguage(a.code2) ||
+        langPrefs.contentLanguages.includes(a.code2) ||
         deviceLocales.includes(a.code2)
       const hasB =
-        store.preferences.hasContentLanguage(b.code2) ||
+        langPrefs.contentLanguages.includes(b.code2) ||
         deviceLocales.includes(b.code2)
       if (hasA === hasB) return a.name.localeCompare(b.name)
       if (hasA) return -1
       return 1
     })
     return langs
-  }, [store])
+  }, [langPrefs])
 
   const onPress = React.useCallback(
     (code2: string) => {
-      store.preferences.toggleContentLanguage(code2)
+      setLangPrefs.toggleContentLanguage(code2)
     },
-    [store],
+    [setLangPrefs],
   )
 
   return (
@@ -63,12 +70,16 @@ export function Component({}: {}) {
               maxHeight: '90vh',
             },
       ]}>
-      <Text style={[pal.text, styles.title]}>Content Languages</Text>
+      <Text style={[pal.text, styles.title]}>
+        <Trans>Content Languages</Trans>
+      </Text>
       <Text style={[pal.text, styles.description]}>
-        Which languages would you like to see in your algorithmic feeds?
+        <Trans>
+          Which languages would you like to see in your algorithmic feeds?
+        </Trans>
       </Text>
       <Text style={[pal.textLight, styles.description]}>
-        Leave them all unchecked to see any language.
+        <Trans>Leave them all unchecked to see any language.</Trans>
       </Text>
       <ScrollView style={styles.scrollContainer}>
         {languages.map(lang => (
diff --git a/src/view/com/modals/lang-settings/LanguageToggle.tsx b/src/view/com/modals/lang-settings/LanguageToggle.tsx
index 187b46e8c..45b100f20 100644
--- a/src/view/com/modals/lang-settings/LanguageToggle.tsx
+++ b/src/view/com/modals/lang-settings/LanguageToggle.tsx
@@ -1,11 +1,10 @@
 import React from 'react'
 import {StyleSheet} from 'react-native'
 import {usePalette} from 'lib/hooks/usePalette'
-import {observer} from 'mobx-react-lite'
 import {ToggleButton} from 'view/com/util/forms/ToggleButton'
-import {useStores} from 'state/index'
+import {useLanguagePrefs, toPostLanguages} from '#/state/preferences/languages'
 
-export const LanguageToggle = observer(function LanguageToggleImpl({
+export function LanguageToggle({
   code2,
   name,
   onPress,
@@ -17,17 +16,17 @@ export const LanguageToggle = observer(function LanguageToggleImpl({
   langType: 'contentLanguages' | 'postLanguages'
 }) {
   const pal = usePalette('default')
-  const store = useStores()
+  const langPrefs = useLanguagePrefs()
 
-  const isSelected = store.preferences[langType].includes(code2)
+  const values =
+    langType === 'contentLanguages'
+      ? langPrefs.contentLanguages
+      : toPostLanguages(langPrefs.postLanguage)
+  const isSelected = values.includes(code2)
 
   // enforce a max of 3 selections for post languages
   let isDisabled = false
-  if (
-    langType === 'postLanguages' &&
-    store.preferences[langType].length >= 3 &&
-    !isSelected
-  ) {
+  if (langType === 'postLanguages' && values.length >= 3 && !isSelected) {
     isDisabled = true
   }
 
@@ -39,7 +38,7 @@ export const LanguageToggle = observer(function LanguageToggleImpl({
       style={[pal.border, styles.languageToggle, isDisabled && styles.dimmed]}
     />
   )
-})
+}
 
 const styles = StyleSheet.create({
   languageToggle: {
diff --git a/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx b/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx
index d74d884cc..05cfb8115 100644
--- a/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx
+++ b/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx
@@ -1,8 +1,6 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {ScrollView} from '../util'
-import {useStores} from 'state/index'
 import {Text} from '../../util/text/Text'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@@ -10,16 +8,25 @@ import {deviceLocales} from 'platform/detection'
 import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
 import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
 import {ToggleButton} from 'view/com/util/forms/ToggleButton'
+import {Trans} from '@lingui/macro'
+import {useModalControls} from '#/state/modals'
+import {
+  useLanguagePrefs,
+  useLanguagePrefsApi,
+  hasPostLanguage,
+} from '#/state/preferences/languages'
 
 export const snapPoints = ['100%']
 
-export const Component = observer(function PostLanguagesSettingsImpl() {
-  const store = useStores()
+export function Component() {
+  const {closeModal} = useModalControls()
+  const langPrefs = useLanguagePrefs()
+  const setLangPrefs = useLanguagePrefsApi()
   const pal = usePalette('default')
   const {isMobile} = useWebMediaQueries()
   const onPressDone = React.useCallback(() => {
-    store.shell.closeModal()
-  }, [store])
+    closeModal()
+  }, [closeModal])
 
   const languages = React.useMemo(() => {
     const langs = LANGUAGES.filter(
@@ -30,23 +37,23 @@ export const Component = observer(function PostLanguagesSettingsImpl() {
     // sort so that device & selected languages are on top, then alphabetically
     langs.sort((a, b) => {
       const hasA =
-        store.preferences.hasPostLanguage(a.code2) ||
+        hasPostLanguage(langPrefs.postLanguage, a.code2) ||
         deviceLocales.includes(a.code2)
       const hasB =
-        store.preferences.hasPostLanguage(b.code2) ||
+        hasPostLanguage(langPrefs.postLanguage, b.code2) ||
         deviceLocales.includes(b.code2)
       if (hasA === hasB) return a.name.localeCompare(b.name)
       if (hasA) return -1
       return 1
     })
     return langs
-  }, [store])
+  }, [langPrefs])
 
   const onPress = React.useCallback(
     (code2: string) => {
-      store.preferences.togglePostLanguage(code2)
+      setLangPrefs.togglePostLanguage(code2)
     },
-    [store],
+    [setLangPrefs],
   )
 
   return (
@@ -64,20 +71,19 @@ export const Component = observer(function PostLanguagesSettingsImpl() {
               maxHeight: '90vh',
             },
       ]}>
-      <Text style={[pal.text, styles.title]}>Post Languages</Text>
+      <Text style={[pal.text, styles.title]}>
+        <Trans>Post Languages</Trans>
+      </Text>
       <Text style={[pal.text, styles.description]}>
-        Which languages are used in this post?
+        <Trans>Which languages are used in this post?</Trans>
       </Text>
       <ScrollView style={styles.scrollContainer}>
         {languages.map(lang => {
-          const isSelected = store.preferences.hasPostLanguage(lang.code2)
+          const isSelected = hasPostLanguage(langPrefs.postLanguage, lang.code2)
 
           // enforce a max of 3 selections for post languages
           let isDisabled = false
-          if (
-            store.preferences.postLanguage.split(',').length >= 3 &&
-            !isSelected
-          ) {
+          if (langPrefs.postLanguage.split(',').length >= 3 && !isSelected) {
             isDisabled = true
           }
 
@@ -104,7 +110,7 @@ export const Component = observer(function PostLanguagesSettingsImpl() {
       <ConfirmLanguagesButton onPress={onPressDone} />
     </View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   container: {
diff --git a/src/view/com/modals/report/InputIssueDetails.tsx b/src/view/com/modals/report/InputIssueDetails.tsx
index 70a8f7b24..2f701b799 100644
--- a/src/view/com/modals/report/InputIssueDetails.tsx
+++ b/src/view/com/modals/report/InputIssueDetails.tsx
@@ -8,6 +8,8 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {s} from 'lib/styles'
 import {SendReportButton} from './SendReportButton'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 
 export function InputIssueDetails({
   details,
@@ -23,6 +25,7 @@ export function InputIssueDetails({
   isProcessing: boolean
 }) {
   const pal = usePalette('default')
+  const {_} = useLingui()
   const {isMobile} = useWebMediaQueries()
 
   return (
@@ -35,14 +38,16 @@ export function InputIssueDetails({
         style={[s.mb10, styles.backBtn]}
         onPress={goBack}
         accessibilityRole="button"
-        accessibilityLabel="Add details"
+        accessibilityLabel={_(msg`Add details`)}
         accessibilityHint="Add more details to your report">
         <FontAwesomeIcon size={18} icon="angle-left" style={[pal.link]} />
-        <Text style={[pal.text, s.f18, pal.link]}> Back</Text>
+        <Text style={[pal.text, s.f18, pal.link]}>
+          <Trans> Back</Trans>
+        </Text>
       </TouchableOpacity>
       <View style={[pal.btn, styles.detailsInputContainer]}>
         <TextInput
-          accessibilityLabel="Text input field"
+          accessibilityLabel={_(msg`Text input field`)}
           accessibilityHint="Enter a reason for reporting this post."
           placeholder="Enter a reason or any other details here."
           placeholderTextColor={pal.textLight.color}
diff --git a/src/view/com/modals/report/Modal.tsx b/src/view/com/modals/report/Modal.tsx
index 98aa2d471..60c3f06b7 100644
--- a/src/view/com/modals/report/Modal.tsx
+++ b/src/view/com/modals/report/Modal.tsx
@@ -2,7 +2,6 @@ import React, {useState, useMemo} from 'react'
 import {Linking, StyleSheet, TouchableOpacity, View} from 'react-native'
 import {ScrollView} from 'react-native-gesture-handler'
 import {AtUri} from '@atproto/api'
-import {useStores} from 'state/index'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {s} from 'lib/styles'
 import {Text} from '../../util/text/Text'
@@ -14,6 +13,10 @@ import {SendReportButton} from './SendReportButton'
 import {InputIssueDetails} from './InputIssueDetails'
 import {ReportReasonOptions} from './ReasonOptions'
 import {CollectionId} from './types'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
+import {getAgent} from '#/state/session'
 
 const DMCA_LINK = 'https://blueskyweb.xyz/support/copyright'
 
@@ -36,7 +39,7 @@ type ReportComponentProps =
     }
 
 export function Component(content: ReportComponentProps) {
-  const store = useStores()
+  const {closeModal} = useModalControls()
   const pal = usePalette('default')
   const {isMobile} = useWebMediaQueries()
   const [isProcessing, setIsProcessing] = useState(false)
@@ -60,13 +63,13 @@ export function Component(content: ReportComponentProps) {
     try {
       if (issue === '__copyright__') {
         Linking.openURL(DMCA_LINK)
-        store.shell.closeModal()
+        closeModal()
         return
       }
       const $type = !isAccountReport
         ? 'com.atproto.repo.strongRef'
         : 'com.atproto.admin.defs#repoRef'
-      await store.agent.createModerationReport({
+      await getAgent().createModerationReport({
         reasonType: issue,
         subject: {
           $type,
@@ -76,7 +79,7 @@ export function Component(content: ReportComponentProps) {
       })
       Toast.show("Thank you for your report! We'll look into it promptly.")
 
-      store.shell.closeModal()
+      closeModal()
       return
     } catch (e: any) {
       setError(cleanError(e))
@@ -146,6 +149,7 @@ const SelectIssue = ({
   atUri: AtUri | null
 }) => {
   const pal = usePalette('default')
+  const {_} = useLingui()
   const collectionName = getCollectionNameForReport(atUri)
   const onSelectIssue = (v: string) => setIssue(v)
   const goToDetails = () => {
@@ -158,9 +162,11 @@ const SelectIssue = ({
 
   return (
     <>
-      <Text style={[pal.text, styles.title]}>Report {collectionName}</Text>
+      <Text style={[pal.text, styles.title]}>
+        <Trans>Report {collectionName}</Trans>
+      </Text>
       <Text style={[pal.textLight, styles.description]}>
-        What is the issue with this {collectionName}?
+        <Trans>What is the issue with this {collectionName}?</Trans>
       </Text>
       <View style={{marginBottom: 10}}>
         <ReportReasonOptions
@@ -182,9 +188,11 @@ const SelectIssue = ({
             style={styles.addDetailsBtn}
             onPress={goToDetails}
             accessibilityRole="button"
-            accessibilityLabel="Add details"
+            accessibilityLabel={_(msg`Add details`)}
             accessibilityHint="Add more details to your report">
-            <Text style={[s.f18, pal.link]}>Add details to report</Text>
+            <Text style={[s.f18, pal.link]}>
+              <Trans>Add details to report</Trans>
+            </Text>
           </TouchableOpacity>
         </>
       ) : undefined}
diff --git a/src/view/com/modals/report/SendReportButton.tsx b/src/view/com/modals/report/SendReportButton.tsx
index 82fb65f20..40c239bff 100644
--- a/src/view/com/modals/report/SendReportButton.tsx
+++ b/src/view/com/modals/report/SendReportButton.tsx
@@ -8,6 +8,8 @@ import {
 } from 'react-native'
 import {Text} from '../../util/text/Text'
 import {s, gradients, colors} from 'lib/styles'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 
 export function SendReportButton({
   onPress,
@@ -16,6 +18,7 @@ export function SendReportButton({
   onPress: () => void
   isProcessing: boolean
 }) {
+  const {_} = useLingui()
   // loading state
   // =
   if (isProcessing) {
@@ -31,14 +34,16 @@ export function SendReportButton({
       style={s.mt10}
       onPress={onPress}
       accessibilityRole="button"
-      accessibilityLabel="Report post"
+      accessibilityLabel={_(msg`Report post`)}
       accessibilityHint={`Reports post with reason and details`}>
       <LinearGradient
         colors={[gradients.blueLight.start, gradients.blueLight.end]}
         start={{x: 0, y: 0}}
         end={{x: 1, y: 1}}
         style={[styles.btn]}>
-        <Text style={[s.white, s.bold, s.f18]}>Send Report</Text>
+        <Text style={[s.white, s.bold, s.f18]}>
+          <Trans>Send Report</Trans>
+        </Text>
       </LinearGradient>
     </TouchableOpacity>
   )