about summary refs log tree commit diff
path: root/src/view/com/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/util')
-rw-r--r--src/view/com/util/AccountDropdownBtn.tsx16
-rw-r--r--src/view/com/util/BottomSheetCustomBackdrop.tsx3
-rw-r--r--src/view/com/util/ErrorBoundary.tsx5
-rw-r--r--src/view/com/util/Link.tsx63
-rw-r--r--src/view/com/util/LoadingPlaceholder.tsx28
-rw-r--r--src/view/com/util/PostMeta.tsx5
-rw-r--r--src/view/com/util/PostSandboxWarning.tsx6
-rw-r--r--src/view/com/util/SimpleViewHeader.tsx5
-rw-r--r--src/view/com/util/TimeElapsed.tsx12
-rw-r--r--src/view/com/util/Toast.tsx6
-rw-r--r--src/view/com/util/Toast.web.tsx9
-rw-r--r--src/view/com/util/UserAvatar.tsx27
-rw-r--r--src/view/com/util/UserBanner.tsx19
-rw-r--r--src/view/com/util/UserInfoText.tsx33
-rw-r--r--src/view/com/util/UserPreviewLink.tsx6
-rw-r--r--src/view/com/util/ViewHeader.tsx9
-rw-r--r--src/view/com/util/Views.web.tsx4
-rw-r--r--src/view/com/util/error/ErrorMessage.tsx5
-rw-r--r--src/view/com/util/error/ErrorScreen.tsx8
-rw-r--r--src/view/com/util/fab/FABInner.tsx9
-rw-r--r--src/view/com/util/forms/Button.tsx4
-rw-r--r--src/view/com/util/forms/DropdownButton.tsx5
-rw-r--r--src/view/com/util/forms/PostDropdownBtn.tsx153
-rw-r--r--src/view/com/util/forms/SearchInput.tsx7
-rw-r--r--src/view/com/util/images/AutoSizedImage.tsx9
-rw-r--r--src/view/com/util/images/ImageLayoutGrid.tsx4
-rw-r--r--src/view/com/util/layouts/Breakpoints.web.tsx6
-rw-r--r--src/view/com/util/load-latest/LoadLatestBtn.tsx5
-rw-r--r--src/view/com/util/moderation/ContentHider.tsx13
-rw-r--r--src/view/com/util/moderation/PostAlerts.tsx13
-rw-r--r--src/view/com/util/moderation/PostHider.tsx11
-rw-r--r--src/view/com/util/moderation/ProfileHeaderAlerts.tsx13
-rw-r--r--src/view/com/util/moderation/ScreenHider.tsx27
-rw-r--r--src/view/com/util/post-ctrls/PostCtrls.tsx204
-rw-r--r--src/view/com/util/post-ctrls/RepostButton.tsx14
-rw-r--r--src/view/com/util/post-ctrls/RepostButton.web.tsx67
-rw-r--r--src/view/com/util/post-embeds/CustomFeedEmbed.tsx38
-rw-r--r--src/view/com/util/post-embeds/ListEmbed.tsx5
-rw-r--r--src/view/com/util/post-embeds/QuoteEmbed.tsx2
-rw-r--r--src/view/com/util/post-embeds/index.tsx30
40 files changed, 514 insertions, 394 deletions
diff --git a/src/view/com/util/AccountDropdownBtn.tsx b/src/view/com/util/AccountDropdownBtn.tsx
index 29571696b..76d493886 100644
--- a/src/view/com/util/AccountDropdownBtn.tsx
+++ b/src/view/com/util/AccountDropdownBtn.tsx
@@ -5,19 +5,23 @@ import {
   FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
 import {s} from 'lib/styles'
-import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
 import {DropdownItem, NativeDropdown} from './forms/NativeDropdown'
 import * as Toast from '../../com/util/Toast'
+import {useSessionApi, SessionAccount} from '#/state/session'
+import {useLingui} from '@lingui/react'
+import {msg} from '@lingui/macro'
 
-export function AccountDropdownBtn({handle}: {handle: string}) {
-  const store = useStores()
+export function AccountDropdownBtn({account}: {account: SessionAccount}) {
   const pal = usePalette('default')
+  const {removeAccount} = useSessionApi()
+  const {_} = useLingui()
+
   const items: DropdownItem[] = [
     {
-      label: 'Remove account',
+      label: _(msg`Remove account`),
       onPress: () => {
-        store.session.removeAccount(handle)
+        removeAccount(account)
         Toast.show('Account removed from quick access')
       },
       icon: {
@@ -34,7 +38,7 @@ export function AccountDropdownBtn({handle}: {handle: string}) {
       <NativeDropdown
         testID="accountSettingsDropdownBtn"
         items={items}
-        accessibilityLabel="Account options"
+        accessibilityLabel={_(msg`Account options`)}
         accessibilityHint="">
         <FontAwesomeIcon
           icon="ellipsis-h"
diff --git a/src/view/com/util/BottomSheetCustomBackdrop.tsx b/src/view/com/util/BottomSheetCustomBackdrop.tsx
index 91379f1c9..ed5a2f165 100644
--- a/src/view/com/util/BottomSheetCustomBackdrop.tsx
+++ b/src/view/com/util/BottomSheetCustomBackdrop.tsx
@@ -6,6 +6,7 @@ import Animated, {
   interpolate,
   useAnimatedStyle,
 } from 'react-native-reanimated'
+import {t} from '@lingui/macro'
 
 export function createCustomBackdrop(
   onClose?: (() => void) | undefined,
@@ -29,7 +30,7 @@ export function createCustomBackdrop(
     return (
       <TouchableWithoutFeedback
         onPress={onClose}
-        accessibilityLabel="Close bottom drawer"
+        accessibilityLabel={t`Close bottom drawer`}
         accessibilityHint=""
         onAccessibilityEscape={() => {
           if (onClose !== undefined) {
diff --git a/src/view/com/util/ErrorBoundary.tsx b/src/view/com/util/ErrorBoundary.tsx
index 529435cf1..397588cfb 100644
--- a/src/view/com/util/ErrorBoundary.tsx
+++ b/src/view/com/util/ErrorBoundary.tsx
@@ -1,6 +1,7 @@
 import React, {Component, ErrorInfo, ReactNode} from 'react'
 import {ErrorScreen} from './error/ErrorScreen'
 import {CenteredView} from './Views'
+import {t} from '@lingui/macro'
 
 interface Props {
   children?: ReactNode
@@ -30,8 +31,8 @@ export class ErrorBoundary extends Component<Props, State> {
       return (
         <CenteredView style={{height: '100%', flex: 1}}>
           <ErrorScreen
-            title="Oh no!"
-            message="There was an unexpected issue in the application. Please let us know if this happened to you!"
+            title={t`Oh no!`}
+            message={t`There was an unexpected issue in the application. Please let us know if this happened to you!`}
             details={this.state.error.toString()}
           />
         </CenteredView>
diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx
index 1777f6659..dcbec7cb4 100644
--- a/src/view/com/util/Link.tsx
+++ b/src/view/com/util/Link.tsx
@@ -21,7 +21,6 @@ import {Text} from './text/Text'
 import {TypographyVariant} from 'lib/ThemeContext'
 import {NavigationProp} from 'lib/routes/types'
 import {router} from '../../../routes'
-import {useStores, RootStoreModel} from 'state/index'
 import {
   convertBskyAppUrlIfNeeded,
   isExternalUrl,
@@ -31,6 +30,7 @@ import {isAndroid, isWeb} from 'platform/detection'
 import {sanitizeUrl} from '@braintree/sanitize-url'
 import {PressableWithHover} from './PressableWithHover'
 import FixedTouchableHighlight from '../pager/FixedTouchableHighlight'
+import {useModalControls} from '#/state/modals'
 
 type Event =
   | React.MouseEvent<HTMLAnchorElement, MouseEvent>
@@ -46,6 +46,7 @@ interface Props extends ComponentProps<typeof TouchableOpacity> {
   noFeedback?: boolean
   asAnchor?: boolean
   anchorNoUnderline?: boolean
+  navigationAction?: 'push' | 'replace' | 'navigate'
 }
 
 export const Link = memo(function Link({
@@ -58,19 +59,26 @@ export const Link = memo(function Link({
   asAnchor,
   accessible,
   anchorNoUnderline,
+  navigationAction,
   ...props
 }: Props) {
-  const store = useStores()
+  const {closeModal} = useModalControls()
   const navigation = useNavigation<NavigationProp>()
   const anchorHref = asAnchor ? sanitizeUrl(href) : undefined
 
   const onPress = React.useCallback(
     (e?: Event) => {
       if (typeof href === 'string') {
-        return onPressInner(store, navigation, sanitizeUrl(href), e)
+        return onPressInner(
+          closeModal,
+          navigation,
+          sanitizeUrl(href),
+          navigationAction,
+          e,
+        )
       }
     },
-    [store, navigation, href],
+    [closeModal, navigation, navigationAction, href],
   )
 
   if (noFeedback) {
@@ -146,6 +154,7 @@ export const TextLink = memo(function TextLink({
   title,
   onPress,
   warnOnMismatchingLabel,
+  navigationAction,
   ...orgProps
 }: {
   testID?: string
@@ -158,10 +167,11 @@ export const TextLink = memo(function TextLink({
   dataSet?: any
   title?: string
   warnOnMismatchingLabel?: boolean
+  navigationAction?: 'push' | 'replace' | 'navigate'
 } & TextProps) {
   const {...props} = useLinkProps({to: sanitizeUrl(href)})
-  const store = useStores()
   const navigation = useNavigation<NavigationProp>()
+  const {openModal, closeModal} = useModalControls()
 
   if (warnOnMismatchingLabel && typeof text !== 'string') {
     console.error('Unable to detect mismatching label')
@@ -174,7 +184,7 @@ export const TextLink = memo(function TextLink({
         linkRequiresWarning(href, typeof text === 'string' ? text : '')
       if (requiresWarning) {
         e?.preventDefault?.()
-        store.shell.openModal({
+        openModal({
           name: 'link-warning',
           text: typeof text === 'string' ? text : '',
           href,
@@ -185,9 +195,24 @@ export const TextLink = memo(function TextLink({
         // @ts-ignore function signature differs by platform -prf
         return onPress()
       }
-      return onPressInner(store, navigation, sanitizeUrl(href), e)
+      return onPressInner(
+        closeModal,
+        navigation,
+        sanitizeUrl(href),
+        navigationAction,
+        e,
+      )
     },
-    [onPress, store, navigation, href, text, warnOnMismatchingLabel],
+    [
+      onPress,
+      closeModal,
+      openModal,
+      navigation,
+      href,
+      text,
+      warnOnMismatchingLabel,
+      navigationAction,
+    ],
   )
   const hrefAttrs = useMemo(() => {
     const isExternal = isExternalUrl(href)
@@ -233,6 +258,7 @@ interface TextLinkOnWebOnlyProps extends TextProps {
   accessibilityLabel?: string
   accessibilityHint?: string
   title?: string
+  navigationAction?: 'push' | 'replace' | 'navigate'
 }
 export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
   testID,
@@ -242,6 +268,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
   text,
   numberOfLines,
   lineHeight,
+  navigationAction,
   ...props
 }: TextLinkOnWebOnlyProps) {
   if (isWeb) {
@@ -255,6 +282,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
         numberOfLines={numberOfLines}
         lineHeight={lineHeight}
         title={props.title}
+        navigationAction={navigationAction}
         {...props}
       />
     )
@@ -285,9 +313,10 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
 // needed customizations
 // -prf
 function onPressInner(
-  store: RootStoreModel,
+  closeModal = () => {},
   navigation: NavigationProp,
   href: string,
+  navigationAction: 'push' | 'replace' | 'navigate' = 'push',
   e?: Event,
 ) {
   let shouldHandle = false
@@ -318,10 +347,20 @@ function onPressInner(
     if (newTab || href.startsWith('http') || href.startsWith('mailto')) {
       Linking.openURL(href)
     } else {
-      store.shell.closeModal() // close any active modals
+      closeModal() // close any active modals
 
-      // @ts-ignore we're not able to type check on this one -prf
-      navigation.dispatch(StackActions.push(...router.matchPath(href)))
+      if (navigationAction === 'push') {
+        // @ts-ignore we're not able to type check on this one -prf
+        navigation.dispatch(StackActions.push(...router.matchPath(href)))
+      } else if (navigationAction === 'replace') {
+        // @ts-ignore we're not able to type check on this one -prf
+        navigation.dispatch(StackActions.replace(...router.matchPath(href)))
+      } else if (navigationAction === 'navigate') {
+        // @ts-ignore we're not able to type check on this one -prf
+        navigation.navigate(...router.matchPath(href))
+      } else {
+        throw Error('Unsupported navigator action.')
+      }
     }
   }
 }
diff --git a/src/view/com/util/LoadingPlaceholder.tsx b/src/view/com/util/LoadingPlaceholder.tsx
index 461cbcbe5..74e36ff7b 100644
--- a/src/view/com/util/LoadingPlaceholder.tsx
+++ b/src/view/com/util/LoadingPlaceholder.tsx
@@ -171,14 +171,22 @@ export function ProfileCardFeedLoadingPlaceholder() {
 
 export function FeedLoadingPlaceholder({
   style,
+  showLowerPlaceholder = true,
+  showTopBorder = true,
 }: {
   style?: StyleProp<ViewStyle>
+  showTopBorder?: boolean
+  showLowerPlaceholder?: boolean
 }) {
   const pal = usePalette('default')
   return (
     <View
       style={[
-        {paddingHorizontal: 12, paddingVertical: 18, borderTopWidth: 1},
+        {
+          paddingHorizontal: 12,
+          paddingVertical: 18,
+          borderTopWidth: showTopBorder ? 1 : 0,
+        },
         pal.border,
         style,
       ]}>
@@ -193,14 +201,16 @@ export function FeedLoadingPlaceholder({
           <LoadingPlaceholder width={120} height={8} />
         </View>
       </View>
-      <View style={{paddingHorizontal: 5}}>
-        <LoadingPlaceholder
-          width={260}
-          height={8}
-          style={{marginVertical: 12}}
-        />
-        <LoadingPlaceholder width={120} height={8} />
-      </View>
+      {showLowerPlaceholder && (
+        <View style={{paddingHorizontal: 5}}>
+          <LoadingPlaceholder
+            width={260}
+            height={8}
+            style={{marginVertical: 12}}
+          />
+          <LoadingPlaceholder width={120} height={8} />
+        </View>
+      )}
     </View>
   )
 }
diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx
index c5e438f8d..fa5f12f6b 100644
--- a/src/view/com/util/PostMeta.tsx
+++ b/src/view/com/util/PostMeta.tsx
@@ -6,7 +6,6 @@ import {niceDate} from 'lib/strings/time'
 import {usePalette} from 'lib/hooks/usePalette'
 import {TypographyVariant} from 'lib/ThemeContext'
 import {UserAvatar} from './UserAvatar'
-import {observer} from 'mobx-react-lite'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {sanitizeHandle} from 'lib/strings/handles'
 import {isAndroid} from 'platform/detection'
@@ -30,7 +29,7 @@ interface PostMetaOpts {
   style?: StyleProp<ViewStyle>
 }
 
-export const PostMeta = observer(function PostMetaImpl(opts: PostMetaOpts) {
+export function PostMeta(opts: PostMetaOpts) {
   const pal = usePalette('default')
   const displayName = opts.author.displayName || opts.author.handle
   const handle = opts.author.handle
@@ -92,7 +91,7 @@ export const PostMeta = observer(function PostMetaImpl(opts: PostMetaOpts) {
       </TimeElapsed>
     </View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   container: {
diff --git a/src/view/com/util/PostSandboxWarning.tsx b/src/view/com/util/PostSandboxWarning.tsx
index 21f5f7b90..b2375c703 100644
--- a/src/view/com/util/PostSandboxWarning.tsx
+++ b/src/view/com/util/PostSandboxWarning.tsx
@@ -1,13 +1,13 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
 import {Text} from './text/Text'
-import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
+import {useSession} from '#/state/session'
 
 export function PostSandboxWarning() {
-  const store = useStores()
+  const {isSandbox} = useSession()
   const pal = usePalette('default')
-  if (store.session.isSandbox) {
+  if (isSandbox) {
     return (
       <View style={styles.container}>
         <Text
diff --git a/src/view/com/util/SimpleViewHeader.tsx b/src/view/com/util/SimpleViewHeader.tsx
index c871d9404..e86e37565 100644
--- a/src/view/com/util/SimpleViewHeader.tsx
+++ b/src/view/com/util/SimpleViewHeader.tsx
@@ -1,5 +1,4 @@
 import React from 'react'
-import {observer} from 'mobx-react-lite'
 import {
   StyleProp,
   StyleSheet,
@@ -18,7 +17,7 @@ import {useSetDrawerOpen} from '#/state/shell'
 
 const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
 
-export const SimpleViewHeader = observer(function SimpleViewHeaderImpl({
+export function SimpleViewHeader({
   showBackButton = true,
   style,
   children,
@@ -76,7 +75,7 @@ export const SimpleViewHeader = observer(function SimpleViewHeaderImpl({
       {children}
     </Container>
   )
-})
+}
 
 const styles = StyleSheet.create({
   header: {
diff --git a/src/view/com/util/TimeElapsed.tsx b/src/view/com/util/TimeElapsed.tsx
index 0765f65b2..aa3a09223 100644
--- a/src/view/com/util/TimeElapsed.tsx
+++ b/src/view/com/util/TimeElapsed.tsx
@@ -1,24 +1,22 @@
 import React from 'react'
-import {observer} from 'mobx-react-lite'
 import {ago} from 'lib/strings/time'
-import {useStores} from 'state/index'
+import {useTickEveryMinute} from '#/state/shell'
 
 // FIXME(dan): Figure out why the false positives
-/* eslint-disable react/prop-types */
 
-export const TimeElapsed = observer(function TimeElapsed({
+export function TimeElapsed({
   timestamp,
   children,
 }: {
   timestamp: string
   children: ({timeElapsed}: {timeElapsed: string}) => JSX.Element
 }) {
-  const stores = useStores()
+  const tick = useTickEveryMinute()
   const [timeElapsed, setTimeAgo] = React.useState(ago(timestamp))
 
   React.useEffect(() => {
     setTimeAgo(ago(timestamp))
-  }, [timestamp, setTimeAgo, stores.shell.tickEveryMinute])
+  }, [timestamp, setTimeAgo, tick])
 
   return children({timeElapsed})
-})
+}
diff --git a/src/view/com/util/Toast.tsx b/src/view/com/util/Toast.tsx
index 4c9045d1e..c7134febe 100644
--- a/src/view/com/util/Toast.tsx
+++ b/src/view/com/util/Toast.tsx
@@ -1,6 +1,7 @@
 import RootSiblings from 'react-native-root-siblings'
 import React from 'react'
 import {Animated, StyleSheet, View} from 'react-native'
+import {Props as FontAwesomeProps} from '@fortawesome/react-native-fontawesome'
 import {Text} from './text/Text'
 import {colors} from 'lib/styles'
 import {useTheme} from 'lib/ThemeContext'
@@ -9,7 +10,10 @@ import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
 
 const TIMEOUT = 4e3
 
-export function show(message: string) {
+export function show(
+  message: string,
+  _icon: FontAwesomeProps['icon'] = 'check',
+) {
   const item = new RootSiblings(<Toast message={message} />)
   setTimeout(() => {
     item.destroy()
diff --git a/src/view/com/util/Toast.web.tsx b/src/view/com/util/Toast.web.tsx
index c295bad69..beb67c30c 100644
--- a/src/view/com/util/Toast.web.tsx
+++ b/src/view/com/util/Toast.web.tsx
@@ -7,12 +7,14 @@ import {StyleSheet, Text, View} from 'react-native'
 import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
+  Props as FontAwesomeProps,
 } from '@fortawesome/react-native-fontawesome'
 
 const DURATION = 3500
 
 interface ActiveToast {
   text: string
+  icon: FontAwesomeProps['icon']
 }
 type GlobalSetActiveToast = (_activeToast: ActiveToast | undefined) => void
 
@@ -36,7 +38,7 @@ export const ToastContainer: React.FC<ToastContainerProps> = ({}) => {
       {activeToast && (
         <View style={styles.container}>
           <FontAwesomeIcon
-            icon="check"
+            icon={activeToast.icon}
             size={24}
             style={styles.icon as FontAwesomeIconStyle}
           />
@@ -49,11 +51,12 @@ export const ToastContainer: React.FC<ToastContainerProps> = ({}) => {
 
 // methods
 // =
-export function show(text: string) {
+
+export function show(text: string, icon: FontAwesomeProps['icon'] = 'check') {
   if (toastTimeout) {
     clearTimeout(toastTimeout)
   }
-  globalSetActiveToast?.({text})
+  globalSetActiveToast?.({text, icon})
   toastTimeout = setTimeout(() => {
     globalSetActiveToast?.(undefined)
   }, DURATION)
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index 9db457325..395e9eb3a 100644
--- a/src/view/com/util/UserAvatar.tsx
+++ b/src/view/com/util/UserAvatar.tsx
@@ -9,13 +9,14 @@ import {
   usePhotoLibraryPermission,
   useCameraPermission,
 } from 'lib/hooks/usePermissions'
-import {useStores} from 'state/index'
 import {colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb, isAndroid} from 'platform/detection'
 import {Image as RNImage} from 'react-native-image-crop-picker'
 import {UserPreviewLink} from './UserPreviewLink'
 import {DropdownItem, NativeDropdown} from './forms/NativeDropdown'
+import {useLingui} from '@lingui/react'
+import {msg} from '@lingui/macro'
 
 export type UserAvatarType = 'user' | 'algo' | 'list'
 
@@ -42,7 +43,13 @@ interface PreviewableUserAvatarProps extends BaseUserAvatarProps {
 
 const BLUR_AMOUNT = isWeb ? 5 : 100
 
-function DefaultAvatar({type, size}: {type: UserAvatarType; size: number}) {
+export function DefaultAvatar({
+  type,
+  size,
+}: {
+  type: UserAvatarType
+  size: number
+}) {
   if (type === 'algo') {
     // Font Awesome Pro 6.4.0 by @fontawesome -https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc.
     return (
@@ -182,8 +189,8 @@ export function EditableUserAvatar({
   avatar,
   onSelectNewAvatar,
 }: EditableUserAvatarProps) {
-  const store = useStores()
   const pal = usePalette('default')
+  const {_} = useLingui()
   const {requestCameraAccessIfNeeded} = useCameraPermission()
   const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
 
@@ -207,7 +214,7 @@ export function EditableUserAvatar({
       [
         !isWeb && {
           testID: 'changeAvatarCameraBtn',
-          label: 'Camera',
+          label: _(msg`Camera`),
           icon: {
             ios: {
               name: 'camera',
@@ -221,7 +228,7 @@ export function EditableUserAvatar({
             }
 
             onSelectNewAvatar(
-              await openCamera(store, {
+              await openCamera({
                 width: 1000,
                 height: 1000,
                 cropperCircleOverlay: true,
@@ -231,7 +238,7 @@ export function EditableUserAvatar({
         },
         {
           testID: 'changeAvatarLibraryBtn',
-          label: 'Library',
+          label: _(msg`Library`),
           icon: {
             ios: {
               name: 'photo.on.rectangle.angled',
@@ -252,7 +259,7 @@ export function EditableUserAvatar({
               return
             }
 
-            const croppedImage = await openCropper(store, {
+            const croppedImage = await openCropper({
               mediaType: 'photo',
               cropperCircleOverlay: true,
               height: item.height,
@@ -268,7 +275,7 @@ export function EditableUserAvatar({
         },
         !!avatar && {
           testID: 'changeAvatarRemoveBtn',
-          label: 'Remove',
+          label: _(msg`Remove`),
           icon: {
             ios: {
               name: 'trash',
@@ -286,7 +293,7 @@ export function EditableUserAvatar({
       onSelectNewAvatar,
       requestCameraAccessIfNeeded,
       requestPhotoAccessIfNeeded,
-      store,
+      _,
     ],
   )
 
@@ -294,7 +301,7 @@ export function EditableUserAvatar({
     <NativeDropdown
       testID="changeAvatarBtn"
       items={dropdownItems}
-      accessibilityLabel="Image options"
+      accessibilityLabel={_(msg`Image options`)}
       accessibilityHint="">
       {avatar ? (
         <HighPriorityImage
diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx
index 4bdfad06c..b31d7e551 100644
--- a/src/view/com/util/UserBanner.tsx
+++ b/src/view/com/util/UserBanner.tsx
@@ -5,7 +5,6 @@ import {ModerationUI} from '@atproto/api'
 import {Image} from 'expo-image'
 import {colors} from 'lib/styles'
 import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
-import {useStores} from 'state/index'
 import {
   usePhotoLibraryPermission,
   useCameraPermission,
@@ -14,6 +13,8 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb, isAndroid} from 'platform/detection'
 import {Image as RNImage} from 'react-native-image-crop-picker'
 import {NativeDropdown, DropdownItem} from './forms/NativeDropdown'
+import {useLingui} from '@lingui/react'
+import {msg} from '@lingui/macro'
 
 export function UserBanner({
   banner,
@@ -24,8 +25,8 @@ export function UserBanner({
   moderation?: ModerationUI
   onSelectNewBanner?: (img: RNImage | null) => void
 }) {
-  const store = useStores()
   const pal = usePalette('default')
+  const {_} = useLingui()
   const {requestCameraAccessIfNeeded} = useCameraPermission()
   const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
 
@@ -34,7 +35,7 @@ export function UserBanner({
       [
         !isWeb && {
           testID: 'changeBannerCameraBtn',
-          label: 'Camera',
+          label: _(msg`Camera`),
           icon: {
             ios: {
               name: 'camera',
@@ -47,7 +48,7 @@ export function UserBanner({
               return
             }
             onSelectNewBanner?.(
-              await openCamera(store, {
+              await openCamera({
                 width: 3000,
                 height: 1000,
               }),
@@ -56,7 +57,7 @@ export function UserBanner({
         },
         {
           testID: 'changeBannerLibraryBtn',
-          label: 'Library',
+          label: _(msg`Library`),
           icon: {
             ios: {
               name: 'photo.on.rectangle.angled',
@@ -74,7 +75,7 @@ export function UserBanner({
             }
 
             onSelectNewBanner?.(
-              await openCropper(store, {
+              await openCropper({
                 mediaType: 'photo',
                 path: items[0].path,
                 width: 3000,
@@ -85,7 +86,7 @@ export function UserBanner({
         },
         !!banner && {
           testID: 'changeBannerRemoveBtn',
-          label: 'Remove',
+          label: _(msg`Remove`),
           icon: {
             ios: {
               name: 'trash',
@@ -103,7 +104,7 @@ export function UserBanner({
       onSelectNewBanner,
       requestCameraAccessIfNeeded,
       requestPhotoAccessIfNeeded,
-      store,
+      _,
     ],
   )
 
@@ -112,7 +113,7 @@ export function UserBanner({
     <NativeDropdown
       testID="changeBannerBtn"
       items={dropdownItems}
-      accessibilityLabel="Image options"
+      accessibilityLabel={_(msg`Image options`)}
       accessibilityHint="">
       {banner ? (
         <Image
diff --git a/src/view/com/util/UserInfoText.tsx b/src/view/com/util/UserInfoText.tsx
index e4ca981d9..e5d2ceb03 100644
--- a/src/view/com/util/UserInfoText.tsx
+++ b/src/view/com/util/UserInfoText.tsx
@@ -1,14 +1,14 @@
-import React, {useState, useEffect} from 'react'
+import React from 'react'
 import {AppBskyActorGetProfile as GetProfile} from '@atproto/api'
 import {StyleProp, StyleSheet, TextStyle} from 'react-native'
 import {TextLinkOnWebOnly} from './Link'
 import {Text} from './text/Text'
 import {LoadingPlaceholder} from './LoadingPlaceholder'
-import {useStores} from 'state/index'
 import {TypographyVariant} from 'lib/ThemeContext'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {sanitizeHandle} from 'lib/strings/handles'
 import {makeProfileLink} from 'lib/routes/links'
+import {useProfileQuery} from '#/state/queries/profile'
 
 export function UserInfoText({
   type = 'md',
@@ -29,35 +29,10 @@ export function UserInfoText({
   attr = attr || 'handle'
   failed = failed || 'user'
 
-  const store = useStores()
-  const [profile, setProfile] = useState<undefined | GetProfile.OutputSchema>(
-    undefined,
-  )
-  const [didFail, setFailed] = useState<boolean>(false)
-
-  useEffect(() => {
-    let aborted = false
-    store.profiles.getProfile(did).then(
-      v => {
-        if (aborted) {
-          return
-        }
-        setProfile(v.data)
-      },
-      _err => {
-        if (aborted) {
-          return
-        }
-        setFailed(true)
-      },
-    )
-    return () => {
-      aborted = true
-    }
-  }, [did, store.profiles])
+  const {data: profile, isError} = useProfileQuery({did})
 
   let inner
-  if (didFail) {
+  if (isError) {
     inner = (
       <Text type={type} style={style} numberOfLines={1}>
         {failed}
diff --git a/src/view/com/util/UserPreviewLink.tsx b/src/view/com/util/UserPreviewLink.tsx
index f43f9e80b..9c5efe55e 100644
--- a/src/view/com/util/UserPreviewLink.tsx
+++ b/src/view/com/util/UserPreviewLink.tsx
@@ -1,9 +1,9 @@
 import React from 'react'
 import {Pressable, StyleProp, ViewStyle} from 'react-native'
-import {useStores} from 'state/index'
 import {Link} from './Link'
 import {isWeb} from 'platform/detection'
 import {makeProfileLink} from 'lib/routes/links'
+import {useModalControls} from '#/state/modals'
 
 interface UserPreviewLinkProps {
   did: string
@@ -13,7 +13,7 @@ interface UserPreviewLinkProps {
 export function UserPreviewLink(
   props: React.PropsWithChildren<UserPreviewLinkProps>,
 ) {
-  const store = useStores()
+  const {openModal} = useModalControls()
 
   if (isWeb) {
     return (
@@ -29,7 +29,7 @@ export function UserPreviewLink(
   return (
     <Pressable
       onPress={() =>
-        store.shell.openModal({
+        openModal({
           name: 'profile-preview',
           did: props.did,
         })
diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx
index adf2e4f08..082cae59c 100644
--- a/src/view/com/util/ViewHeader.tsx
+++ b/src/view/com/util/ViewHeader.tsx
@@ -1,5 +1,4 @@
 import React from 'react'
-import {observer} from 'mobx-react-lite'
 import {StyleSheet, TouchableOpacity, View} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {useNavigation} from '@react-navigation/native'
@@ -15,7 +14,7 @@ import {useSetDrawerOpen} from '#/state/shell'
 
 const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
 
-export const ViewHeader = observer(function ViewHeaderImpl({
+export function ViewHeader({
   title,
   canGoBack,
   showBackButton = true,
@@ -108,7 +107,7 @@ export const ViewHeader = observer(function ViewHeaderImpl({
       </Container>
     )
   }
-})
+}
 
 function DesktopWebHeader({
   title,
@@ -140,7 +139,7 @@ function DesktopWebHeader({
   )
 }
 
-const Container = observer(function ContainerImpl({
+function Container({
   children,
   hideOnScroll,
   showBorder,
@@ -178,7 +177,7 @@ const Container = observer(function ContainerImpl({
       {children}
     </Animated.View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   header: {
diff --git a/src/view/com/util/Views.web.tsx b/src/view/com/util/Views.web.tsx
index 1c2edc0cc..5a4f266fd 100644
--- a/src/view/com/util/Views.web.tsx
+++ b/src/view/com/util/Views.web.tsx
@@ -108,9 +108,9 @@ export const FlatList = React.forwardRef(function FlatListImpl<ItemT>(
     <Animated.FlatList
       ref={ref}
       contentContainerStyle={[
+        styles.contentContainer,
         contentContainerStyle,
         pal.border,
-        styles.contentContainer,
       ]}
       style={style}
       contentOffset={contentOffset}
@@ -135,9 +135,9 @@ export const ScrollView = React.forwardRef(function ScrollViewImpl(
   return (
     <Animated.ScrollView
       contentContainerStyle={[
+        styles.contentContainer,
         contentContainerStyle,
         pal.border,
-        styles.contentContainer,
       ]}
       // @ts-ignore something is wrong with the reanimated types -prf
       ref={ref}
diff --git a/src/view/com/util/error/ErrorMessage.tsx b/src/view/com/util/error/ErrorMessage.tsx
index 370f10ae3..b4adbb557 100644
--- a/src/view/com/util/error/ErrorMessage.tsx
+++ b/src/view/com/util/error/ErrorMessage.tsx
@@ -13,6 +13,8 @@ import {
 import {Text} from '../text/Text'
 import {useTheme} from 'lib/ThemeContext'
 import {usePalette} from 'lib/hooks/usePalette'
+import {useLingui} from '@lingui/react'
+import {msg} from '@lingui/macro'
 
 export function ErrorMessage({
   message,
@@ -27,6 +29,7 @@ export function ErrorMessage({
 }) {
   const theme = useTheme()
   const pal = usePalette('error')
+  const {_} = useLingui()
   return (
     <View testID="errorMessageView" style={[styles.outer, pal.view, style]}>
       <View
@@ -49,7 +52,7 @@ export function ErrorMessage({
           style={styles.btn}
           onPress={onPressTryAgain}
           accessibilityRole="button"
-          accessibilityLabel="Retry"
+          accessibilityLabel={_(msg`Retry`)}
           accessibilityHint="Retries the last action, which errored out">
           <FontAwesomeIcon
             icon="arrows-rotate"
diff --git a/src/view/com/util/error/ErrorScreen.tsx b/src/view/com/util/error/ErrorScreen.tsx
index a5deeb18f..4cd6dd4b4 100644
--- a/src/view/com/util/error/ErrorScreen.tsx
+++ b/src/view/com/util/error/ErrorScreen.tsx
@@ -9,6 +9,8 @@ import {useTheme} from 'lib/ThemeContext'
 import {usePalette} from 'lib/hooks/usePalette'
 import {Button} from '../forms/Button'
 import {CenteredView} from '../Views'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 
 export function ErrorScreen({
   title,
@@ -25,6 +27,8 @@ export function ErrorScreen({
 }) {
   const theme = useTheme()
   const pal = usePalette('default')
+  const {_} = useLingui()
+
   return (
     <CenteredView testID={testID} style={[styles.outer, pal.view]}>
       <View style={styles.errorIconContainer}>
@@ -58,7 +62,7 @@ export function ErrorScreen({
             type="default"
             style={[styles.btn]}
             onPress={onPressTryAgain}
-            accessibilityLabel="Retry"
+            accessibilityLabel={_(msg`Retry`)}
             accessibilityHint="Retries the last action, which errored out">
             <FontAwesomeIcon
               icon="arrows-rotate"
@@ -66,7 +70,7 @@ export function ErrorScreen({
               size={16}
             />
             <Text type="button" style={[styles.btnText, pal.link]}>
-              Try again
+              <Trans>Try again</Trans>
             </Text>
           </Button>
         </View>
diff --git a/src/view/com/util/fab/FABInner.tsx b/src/view/com/util/fab/FABInner.tsx
index 5b1d5d888..9787d92fb 100644
--- a/src/view/com/util/fab/FABInner.tsx
+++ b/src/view/com/util/fab/FABInner.tsx
@@ -1,5 +1,4 @@
 import React, {ComponentProps} from 'react'
-import {observer} from 'mobx-react-lite'
 import {StyleSheet, TouchableWithoutFeedback} from 'react-native'
 import LinearGradient from 'react-native-linear-gradient'
 import {gradients} from 'lib/styles'
@@ -15,11 +14,7 @@ export interface FABProps
   icon: JSX.Element
 }
 
-export const FABInner = observer(function FABInnerImpl({
-  testID,
-  icon,
-  ...props
-}: FABProps) {
+export function FABInner({testID, icon, ...props}: FABProps) {
   const insets = useSafeAreaInsets()
   const {isMobile, isTablet} = useWebMediaQueries()
   const {fabMinimalShellTransform} = useMinimalShellMode()
@@ -55,7 +50,7 @@ export const FABInner = observer(function FABInnerImpl({
       </Animated.View>
     </TouchableWithoutFeedback>
   )
-})
+}
 
 const styles = StyleSheet.create({
   sizeRegular: {
diff --git a/src/view/com/util/forms/Button.tsx b/src/view/com/util/forms/Button.tsx
index 270d98317..8f24f8288 100644
--- a/src/view/com/util/forms/Button.tsx
+++ b/src/view/com/util/forms/Button.tsx
@@ -52,6 +52,7 @@ export function Button({
   accessibilityLabelledBy,
   onAccessibilityEscape,
   withLoading = false,
+  disabled = false,
 }: React.PropsWithChildren<{
   type?: ButtonType
   label?: string
@@ -65,6 +66,7 @@ export function Button({
   accessibilityLabelledBy?: string
   onAccessibilityEscape?: () => void
   withLoading?: boolean
+  disabled?: boolean
 }>) {
   const theme = useTheme()
   const typeOuterStyle = choose<ViewStyle, Record<ButtonType, ViewStyle>>(
@@ -198,7 +200,7 @@ export function Button({
     <Pressable
       style={getStyle}
       onPress={onPressWrapped}
-      disabled={isLoading}
+      disabled={disabled || isLoading}
       testID={testID}
       accessibilityRole="button"
       accessibilityLabel={accessibilityLabel}
diff --git a/src/view/com/util/forms/DropdownButton.tsx b/src/view/com/util/forms/DropdownButton.tsx
index 1bed60b5d..ad8f50f5e 100644
--- a/src/view/com/util/forms/DropdownButton.tsx
+++ b/src/view/com/util/forms/DropdownButton.tsx
@@ -17,6 +17,8 @@ import {colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useTheme} from 'lib/ThemeContext'
 import {HITSLOP_10} from 'lib/constants'
+import {useLingui} from '@lingui/react'
+import {msg} from '@lingui/macro'
 
 const ESTIMATED_BTN_HEIGHT = 50
 const ESTIMATED_SEP_HEIGHT = 16
@@ -207,6 +209,7 @@ const DropdownItems = ({
 }: DropDownItemProps) => {
   const pal = usePalette('default')
   const theme = useTheme()
+  const {_} = useLingui()
   const dropDownBackgroundColor =
     theme.colorScheme === 'dark' ? pal.btn : pal.view
   const separatorColor =
@@ -224,7 +227,7 @@ const DropdownItems = ({
       {/* This TouchableWithoutFeedback renders the background so if the user clicks outside, the dropdown closes */}
       <TouchableWithoutFeedback
         onPress={onOuterPress}
-        accessibilityLabel="Toggle dropdown"
+        accessibilityLabel={_(msg`Toggle dropdown`)}
         accessibilityHint="">
         <View style={[styles.bg]} />
       </TouchableWithoutFeedback>
diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx
index 1fffa3123..1ba5ae8ae 100644
--- a/src/view/com/util/forms/PostDropdownBtn.tsx
+++ b/src/view/com/util/forms/PostDropdownBtn.tsx
@@ -1,49 +1,101 @@
 import React from 'react'
-import {StyleProp, View, ViewStyle} from 'react-native'
+import {Linking, StyleProp, View, ViewStyle} from 'react-native'
+import Clipboard from '@react-native-clipboard/clipboard'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {AppBskyFeedDefs, AppBskyFeedPost, AtUri} from '@atproto/api'
 import {toShareUrl} from 'lib/strings/url-helpers'
-import {useStores} from 'state/index'
 import {useTheme} from 'lib/ThemeContext'
 import {shareUrl} from 'lib/sharing'
 import {
   NativeDropdown,
   DropdownItem as NativeDropdownItem,
 } from './NativeDropdown'
+import * as Toast from '../Toast'
 import {EventStopper} from '../EventStopper'
+import {useModalControls} from '#/state/modals'
+import {makeProfileLink} from '#/lib/routes/links'
+import {getTranslatorLink} from '#/locale/helpers'
+import {usePostDeleteMutation} from '#/state/queries/post'
+import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
+import {useLanguagePrefs} from '#/state/preferences'
+import {logger} from '#/logger'
+import {Shadow} from '#/state/cache/types'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useSession} from '#/state/session'
+import {isWeb} from '#/platform/detection'
 
 export function PostDropdownBtn({
   testID,
-  itemUri,
-  itemCid,
-  itemHref,
-  isAuthor,
-  isThreadMuted,
-  onCopyPostText,
-  onOpenTranslate,
-  onToggleThreadMute,
-  onDeletePost,
+  post,
+  record,
   style,
 }: {
   testID: string
-  itemUri: string
-  itemCid: string
-  itemHref: string
-  itemTitle: string
-  isAuthor: boolean
-  isThreadMuted: boolean
-  onCopyPostText: () => void
-  onOpenTranslate: () => void
-  onToggleThreadMute: () => void
-  onDeletePost: () => void
+  post: Shadow<AppBskyFeedDefs.PostView>
+  record: AppBskyFeedPost.Record
   style?: StyleProp<ViewStyle>
 }) {
-  const store = useStores()
+  const {hasSession, currentAccount} = useSession()
   const theme = useTheme()
+  const {_} = useLingui()
   const defaultCtrlColor = theme.palette.default.postCtrl
+  const {openModal} = useModalControls()
+  const langPrefs = useLanguagePrefs()
+  const mutedThreads = useMutedThreads()
+  const toggleThreadMute = useToggleThreadMute()
+  const postDeleteMutation = usePostDeleteMutation()
+
+  const rootUri = record.reply?.root?.uri || post.uri
+  const isThreadMuted = mutedThreads.includes(rootUri)
+  const isAuthor = post.author.did === currentAccount?.did
+  const href = React.useMemo(() => {
+    const urip = new AtUri(post.uri)
+    return makeProfileLink(post.author, 'post', urip.rkey)
+  }, [post.uri, post.author])
+
+  const translatorUrl = getTranslatorLink(
+    record.text,
+    langPrefs.primaryLanguage,
+  )
+
+  const onDeletePost = React.useCallback(() => {
+    postDeleteMutation.mutateAsync({uri: post.uri}).then(
+      () => {
+        Toast.show('Post deleted')
+      },
+      e => {
+        logger.error('Failed to delete post', {error: e})
+        Toast.show('Failed to delete post, please try again')
+      },
+    )
+  }, [post, postDeleteMutation])
+
+  const onToggleThreadMute = React.useCallback(() => {
+    try {
+      const muted = toggleThreadMute(rootUri)
+      if (muted) {
+        Toast.show('You will no longer receive notifications for this thread')
+      } else {
+        Toast.show('You will now receive notifications for this thread')
+      }
+    } catch (e) {
+      logger.error('Failed to toggle thread mute', {error: e})
+    }
+  }, [rootUri, toggleThreadMute])
+
+  const onCopyPostText = React.useCallback(() => {
+    Clipboard.setString(record?.text || '')
+    Toast.show('Copied to clipboard')
+  }, [record])
+
+  const onOpenTranslate = React.useCallback(() => {
+    Linking.openURL(translatorUrl)
+  }, [translatorUrl])
 
   const dropdownItems: NativeDropdownItem[] = [
     {
-      label: 'Translate',
+      label: _(msg`Translate`),
       onPress() {
         onOpenTranslate()
       },
@@ -57,7 +109,7 @@ export function PostDropdownBtn({
       },
     },
     {
-      label: 'Copy post text',
+      label: _(msg`Copy post text`),
       onPress() {
         onCopyPostText()
       },
@@ -71,9 +123,9 @@ export function PostDropdownBtn({
       },
     },
     {
-      label: 'Share',
+      label: isWeb ? _(msg`Copy link to post`) : _(msg`Share`),
       onPress() {
-        const url = toShareUrl(itemHref)
+        const url = toShareUrl(href)
         shareUrl(url)
       },
       testID: 'postDropdownShareBtn',
@@ -85,11 +137,11 @@ export function PostDropdownBtn({
         web: 'share',
       },
     },
-    {
+    hasSession && {
       label: 'separator',
     },
-    {
-      label: isThreadMuted ? 'Unmute thread' : 'Mute thread',
+    hasSession && {
+      label: isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`),
       onPress() {
         onToggleThreadMute()
       },
@@ -102,37 +154,38 @@ export function PostDropdownBtn({
         web: 'comment-slash',
       },
     },
-    {
+    hasSession && {
       label: 'separator',
     },
-    !isAuthor && {
-      label: 'Report post',
-      onPress() {
-        store.shell.openModal({
-          name: 'report',
-          uri: itemUri,
-          cid: itemCid,
-        })
-      },
-      testID: 'postDropdownReportBtn',
-      icon: {
-        ios: {
-          name: 'exclamationmark.triangle',
+    !isAuthor &&
+      hasSession && {
+        label: _(msg`Report post`),
+        onPress() {
+          openModal({
+            name: 'report',
+            uri: post.uri,
+            cid: post.cid,
+          })
+        },
+        testID: 'postDropdownReportBtn',
+        icon: {
+          ios: {
+            name: 'exclamationmark.triangle',
+          },
+          android: 'ic_menu_report_image',
+          web: 'circle-exclamation',
         },
-        android: 'ic_menu_report_image',
-        web: 'circle-exclamation',
       },
-    },
     isAuthor && {
       label: 'separator',
     },
     isAuthor && {
-      label: 'Delete post',
+      label: _(msg`Delete post`),
       onPress() {
-        store.shell.openModal({
+        openModal({
           name: 'confirm',
-          title: 'Delete this post?',
-          message: 'Are you sure? This can not be undone.',
+          title: _(msg`Delete this post?`),
+          message: _(msg`Are you sure? This cannot be undone.`),
           onPressConfirm: onDeletePost,
         })
       },
diff --git a/src/view/com/util/forms/SearchInput.tsx b/src/view/com/util/forms/SearchInput.tsx
index c1eb82bd4..02b462b55 100644
--- a/src/view/com/util/forms/SearchInput.tsx
+++ b/src/view/com/util/forms/SearchInput.tsx
@@ -14,6 +14,8 @@ import {
 import {MagnifyingGlassIcon} from 'lib/icons'
 import {useTheme} from 'lib/ThemeContext'
 import {usePalette} from 'lib/hooks/usePalette'
+import {useLingui} from '@lingui/react'
+import {msg} from '@lingui/macro'
 
 interface Props {
   query: string
@@ -33,6 +35,7 @@ export function SearchInput({
 }: Props) {
   const theme = useTheme()
   const pal = usePalette('default')
+  const {_} = useLingui()
   const textInput = React.useRef<TextInput>(null)
 
   const onPressCancelSearchInner = React.useCallback(() => {
@@ -58,7 +61,7 @@ export function SearchInput({
         onChangeText={onChangeQuery}
         onSubmitEditing={onSubmitQuery}
         accessibilityRole="search"
-        accessibilityLabel="Search"
+        accessibilityLabel={_(msg`Search`)}
         accessibilityHint=""
         autoCorrect={false}
         autoCapitalize="none"
@@ -67,7 +70,7 @@ export function SearchInput({
         <TouchableOpacity
           onPress={onPressCancelSearchInner}
           accessibilityRole="button"
-          accessibilityLabel="Clear search query"
+          accessibilityLabel={_(msg`Clear search query`)}
           accessibilityHint="">
           <FontAwesomeIcon
             icon="xmark"
diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx
index 6cbcddc32..b5b6c1b52 100644
--- a/src/view/com/util/images/AutoSizedImage.tsx
+++ b/src/view/com/util/images/AutoSizedImage.tsx
@@ -2,8 +2,8 @@ import React from 'react'
 import {StyleProp, StyleSheet, Pressable, View, ViewStyle} from 'react-native'
 import {Image} from 'expo-image'
 import {clamp} from 'lib/numbers'
-import {useStores} from 'state/index'
 import {Dimensions} from 'lib/media/types'
+import * as imageSizes from 'lib/media/image-sizes'
 
 const MIN_ASPECT_RATIO = 0.33 // 1/3
 const MAX_ASPECT_RATIO = 5 // 5/1
@@ -29,9 +29,8 @@ export function AutoSizedImage({
   style,
   children = null,
 }: Props) {
-  const store = useStores()
   const [dim, setDim] = React.useState<Dimensions | undefined>(
-    dimensionsHint || store.imageSizes.get(uri),
+    dimensionsHint || imageSizes.get(uri),
   )
   const [aspectRatio, setAspectRatio] = React.useState<number>(
     dim ? calc(dim) : 1,
@@ -41,14 +40,14 @@ export function AutoSizedImage({
     if (dim) {
       return
     }
-    store.imageSizes.fetch(uri).then(newDim => {
+    imageSizes.fetch(uri).then(newDim => {
       if (aborted) {
         return
       }
       setDim(newDim)
       setAspectRatio(calc(newDim))
     })
-  }, [dim, setDim, setAspectRatio, store, uri])
+  }, [dim, setDim, setAspectRatio, uri])
 
   if (onPress || onLongPress || onPressIn) {
     return (
diff --git a/src/view/com/util/images/ImageLayoutGrid.tsx b/src/view/com/util/images/ImageLayoutGrid.tsx
index 4aa6f28de..23e807b6a 100644
--- a/src/view/com/util/images/ImageLayoutGrid.tsx
+++ b/src/view/com/util/images/ImageLayoutGrid.tsx
@@ -69,12 +69,12 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
               <GalleryItem {...props} index={0} imageStyle={styles.image} />
             </View>
             <View style={styles.smallItem}>
-              <GalleryItem {...props} index={2} imageStyle={styles.image} />
+              <GalleryItem {...props} index={1} imageStyle={styles.image} />
             </View>
           </View>
           <View style={styles.flexRow}>
             <View style={styles.smallItem}>
-              <GalleryItem {...props} index={1} imageStyle={styles.image} />
+              <GalleryItem {...props} index={2} imageStyle={styles.image} />
             </View>
             <View style={styles.smallItem}>
               <GalleryItem {...props} index={3} imageStyle={styles.image} />
diff --git a/src/view/com/util/layouts/Breakpoints.web.tsx b/src/view/com/util/layouts/Breakpoints.web.tsx
index 5cf73df0c..5106e3e1f 100644
--- a/src/view/com/util/layouts/Breakpoints.web.tsx
+++ b/src/view/com/util/layouts/Breakpoints.web.tsx
@@ -8,13 +8,13 @@ export const TabletOrDesktop = ({children}: React.PropsWithChildren<{}>) => (
   <MediaQuery minWidth={800}>{children}</MediaQuery>
 )
 export const Tablet = ({children}: React.PropsWithChildren<{}>) => (
-  <MediaQuery minWidth={800} maxWidth={1300}>
+  <MediaQuery minWidth={800} maxWidth={1300 - 1}>
     {children}
   </MediaQuery>
 )
 export const TabletOrMobile = ({children}: React.PropsWithChildren<{}>) => (
-  <MediaQuery maxWidth={1300}>{children}</MediaQuery>
+  <MediaQuery maxWidth={1300 - 1}>{children}</MediaQuery>
 )
 export const Mobile = ({children}: React.PropsWithChildren<{}>) => (
-  <MediaQuery maxWidth={800}>{children}</MediaQuery>
+  <MediaQuery maxWidth={800 - 1}>{children}</MediaQuery>
 )
diff --git a/src/view/com/util/load-latest/LoadLatestBtn.tsx b/src/view/com/util/load-latest/LoadLatestBtn.tsx
index f9a9387bb..970d3a73a 100644
--- a/src/view/com/util/load-latest/LoadLatestBtn.tsx
+++ b/src/view/com/util/load-latest/LoadLatestBtn.tsx
@@ -1,6 +1,5 @@
 import React from 'react'
 import {StyleSheet, TouchableOpacity, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@@ -12,7 +11,7 @@ const AnimatedTouchableOpacity =
   Animated.createAnimatedComponent(TouchableOpacity)
 import {isWeb} from 'platform/detection'
 
-export const LoadLatestBtn = observer(function LoadLatestBtnImpl({
+export function LoadLatestBtn({
   onPress,
   label,
   showIndicator,
@@ -44,7 +43,7 @@ export const LoadLatestBtn = observer(function LoadLatestBtnImpl({
       {showIndicator && <View style={[styles.indicator, pal.borderDark]} />}
     </AnimatedTouchableOpacity>
   )
-})
+}
 
 const styles = StyleSheet.create({
   loadLatest: {
diff --git a/src/view/com/util/moderation/ContentHider.tsx b/src/view/com/util/moderation/ContentHider.tsx
index 4f917844a..a13aae2b5 100644
--- a/src/view/com/util/moderation/ContentHider.tsx
+++ b/src/view/com/util/moderation/ContentHider.tsx
@@ -6,7 +6,9 @@ import {ModerationUI} from '@atproto/api'
 import {Text} from '../text/Text'
 import {ShieldExclamation} from 'lib/icons'
 import {describeModerationCause} from 'lib/moderation'
-import {useStores} from 'state/index'
+import {useLingui} from '@lingui/react'
+import {msg} from '@lingui/macro'
+import {useModalControls} from '#/state/modals'
 
 export function ContentHider({
   testID,
@@ -22,10 +24,11 @@ export function ContentHider({
   style?: StyleProp<ViewStyle>
   childContainerStyle?: StyleProp<ViewStyle>
 }>) {
-  const store = useStores()
   const pal = usePalette('default')
+  const {_} = useLingui()
   const {isMobile} = useWebMediaQueries()
   const [override, setOverride] = React.useState(false)
+  const {openModal} = useModalControls()
 
   if (!moderation.blur || (ignoreMute && moderation.cause?.type === 'muted')) {
     return (
@@ -43,7 +46,7 @@ export function ContentHider({
           if (!moderation.noOverride) {
             setOverride(v => !v)
           } else {
-            store.shell.openModal({
+            openModal({
               name: 'moderation-details',
               context: 'content',
               moderation,
@@ -62,14 +65,14 @@ export function ContentHider({
         ]}>
         <Pressable
           onPress={() => {
-            store.shell.openModal({
+            openModal({
               name: 'moderation-details',
               context: 'content',
               moderation,
             })
           }}
           accessibilityRole="button"
-          accessibilityLabel="Learn more about this warning"
+          accessibilityLabel={_(msg`Learn more about this warning`)}
           accessibilityHint="">
           <ShieldExclamation size={18} style={pal.text} />
         </Pressable>
diff --git a/src/view/com/util/moderation/PostAlerts.tsx b/src/view/com/util/moderation/PostAlerts.tsx
index 0dba367fc..bc5bf9b32 100644
--- a/src/view/com/util/moderation/PostAlerts.tsx
+++ b/src/view/com/util/moderation/PostAlerts.tsx
@@ -5,7 +5,9 @@ import {Text} from '../text/Text'
 import {usePalette} from 'lib/hooks/usePalette'
 import {ShieldExclamation} from 'lib/icons'
 import {describeModerationCause} from 'lib/moderation'
-import {useStores} from 'state/index'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
 
 export function PostAlerts({
   moderation,
@@ -15,8 +17,9 @@ export function PostAlerts({
   includeMute?: boolean
   style?: StyleProp<ViewStyle>
 }) {
-  const store = useStores()
   const pal = usePalette('default')
+  const {_} = useLingui()
+  const {openModal} = useModalControls()
 
   const shouldAlert = !!moderation.cause && moderation.alert
   if (!shouldAlert) {
@@ -27,21 +30,21 @@ export function PostAlerts({
   return (
     <Pressable
       onPress={() => {
-        store.shell.openModal({
+        openModal({
           name: 'moderation-details',
           context: 'content',
           moderation,
         })
       }}
       accessibilityRole="button"
-      accessibilityLabel="Learn more about this warning"
+      accessibilityLabel={_(msg`Learn more about this warning`)}
       accessibilityHint=""
       style={[styles.container, pal.viewLight, style]}>
       <ShieldExclamation style={pal.text} size={16} />
       <Text type="lg" style={[pal.text]}>
         {desc.name}{' '}
         <Text type="lg" style={[pal.link, styles.learnMoreBtn]}>
-          Learn More
+          <Trans>Learn More</Trans>
         </Text>
       </Text>
     </Pressable>
diff --git a/src/view/com/util/moderation/PostHider.tsx b/src/view/com/util/moderation/PostHider.tsx
index d224286b0..c2b857f54 100644
--- a/src/view/com/util/moderation/PostHider.tsx
+++ b/src/view/com/util/moderation/PostHider.tsx
@@ -8,7 +8,9 @@ import {Text} from '../text/Text'
 import {addStyle} from 'lib/styles'
 import {describeModerationCause} from 'lib/moderation'
 import {ShieldExclamation} from 'lib/icons'
-import {useStores} from 'state/index'
+import {useLingui} from '@lingui/react'
+import {msg} from '@lingui/macro'
+import {useModalControls} from '#/state/modals'
 
 interface Props extends ComponentProps<typeof Link> {
   // testID?: string
@@ -25,10 +27,11 @@ export function PostHider({
   children,
   ...props
 }: Props) {
-  const store = useStores()
   const pal = usePalette('default')
+  const {_} = useLingui()
   const {isMobile} = useWebMediaQueries()
   const [override, setOverride] = React.useState(false)
+  const {openModal} = useModalControls()
 
   if (!moderation.blur) {
     return (
@@ -63,14 +66,14 @@ export function PostHider({
         ]}>
         <Pressable
           onPress={() => {
-            store.shell.openModal({
+            openModal({
               name: 'moderation-details',
               context: 'content',
               moderation,
             })
           }}
           accessibilityRole="button"
-          accessibilityLabel="Learn more about this warning"
+          accessibilityLabel={_(msg`Learn more about this warning`)}
           accessibilityHint="">
           <ShieldExclamation size={18} style={pal.text} />
         </Pressable>
diff --git a/src/view/com/util/moderation/ProfileHeaderAlerts.tsx b/src/view/com/util/moderation/ProfileHeaderAlerts.tsx
index 6b7f4e7ec..d2675ca54 100644
--- a/src/view/com/util/moderation/ProfileHeaderAlerts.tsx
+++ b/src/view/com/util/moderation/ProfileHeaderAlerts.tsx
@@ -8,7 +8,9 @@ import {
   describeModerationCause,
   getProfileModerationCauses,
 } from 'lib/moderation'
-import {useStores} from 'state/index'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
 
 export function ProfileHeaderAlerts({
   moderation,
@@ -17,8 +19,9 @@ export function ProfileHeaderAlerts({
   moderation: ProfileModeration
   style?: StyleProp<ViewStyle>
 }) {
-  const store = useStores()
   const pal = usePalette('default')
+  const {_} = useLingui()
+  const {openModal} = useModalControls()
 
   const causes = getProfileModerationCauses(moderation)
   if (!causes.length) {
@@ -34,14 +37,14 @@ export function ProfileHeaderAlerts({
             testID="profileHeaderAlert"
             key={desc.name}
             onPress={() => {
-              store.shell.openModal({
+              openModal({
                 name: 'moderation-details',
                 context: 'content',
                 moderation: {cause},
               })
             }}
             accessibilityRole="button"
-            accessibilityLabel="Learn more about this warning"
+            accessibilityLabel={_(msg`Learn more about this warning`)}
             accessibilityHint=""
             style={[styles.container, pal.viewLight, style]}>
             <ShieldExclamation style={pal.text} size={24} />
@@ -49,7 +52,7 @@ export function ProfileHeaderAlerts({
               {desc.name}
             </Text>
             <Text type="lg" style={[pal.link, styles.learnMoreBtn]}>
-              Learn More
+              <Trans>Learn More</Trans>
             </Text>
           </Pressable>
         )
diff --git a/src/view/com/util/moderation/ScreenHider.tsx b/src/view/com/util/moderation/ScreenHider.tsx
index 0224b9fee..946f937e9 100644
--- a/src/view/com/util/moderation/ScreenHider.tsx
+++ b/src/view/com/util/moderation/ScreenHider.tsx
@@ -18,7 +18,10 @@ import {NavigationProp} from 'lib/routes/types'
 import {Text} from '../text/Text'
 import {Button} from '../forms/Button'
 import {describeModerationCause} from 'lib/moderation'
-import {useStores} from 'state/index'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
+import {s} from '#/lib/styles'
 
 export function ScreenHider({
   testID,
@@ -34,12 +37,13 @@ export function ScreenHider({
   style?: StyleProp<ViewStyle>
   containerStyle?: StyleProp<ViewStyle>
 }>) {
-  const store = useStores()
   const pal = usePalette('default')
   const palInverted = usePalette('inverted')
+  const {_} = useLingui()
   const [override, setOverride] = React.useState(false)
   const navigation = useNavigation<NavigationProp>()
   const {isMobile} = useWebMediaQueries()
+  const {openModal} = useModalControls()
 
   if (!moderation.blur || override) {
     return (
@@ -62,27 +66,26 @@ export function ScreenHider({
         </View>
       </View>
       <Text type="title-2xl" style={[styles.title, pal.text]}>
-        Content Warning
+        <Trans>Content Warning</Trans>
       </Text>
       <Text type="2xl" style={[styles.description, pal.textLight]}>
-        This {screenDescription} has been flagged:{' '}
-        <Text type="2xl-medium" style={pal.text}>
-          {desc.name}
+        <Trans>This {screenDescription} has been flagged:</Trans>
+        <Text type="2xl-medium" style={[pal.text, s.ml5]}>
+          {desc.name}.
         </Text>
-        .{' '}
         <TouchableWithoutFeedback
           onPress={() => {
-            store.shell.openModal({
+            openModal({
               name: 'moderation-details',
               context: 'account',
               moderation,
             })
           }}
           accessibilityRole="button"
-          accessibilityLabel="Learn more about this warning"
+          accessibilityLabel={_(msg`Learn more about this warning`)}
           accessibilityHint="">
           <Text type="2xl" style={pal.link}>
-            Learn More
+            <Trans>Learn More</Trans>
           </Text>
         </TouchableWithoutFeedback>
       </Text>
@@ -99,7 +102,7 @@ export function ScreenHider({
           }}
           style={styles.btn}>
           <Text type="button-lg" style={pal.textInverted}>
-            Go back
+            <Trans>Go back</Trans>
           </Text>
         </Button>
         {!moderation.noOverride && (
@@ -108,7 +111,7 @@ export function ScreenHider({
             onPress={() => setOverride(v => !v)}
             style={styles.btn}>
             <Text type="button-lg" style={pal.text}>
-              Show anyway
+              <Trans>Show anyway</Trans>
             </Text>
           </Button>
         )}
diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx
index 5769a478b..e548c45f7 100644
--- a/src/view/com/util/post-ctrls/PostCtrls.tsx
+++ b/src/view/com/util/post-ctrls/PostCtrls.tsx
@@ -6,168 +6,174 @@ import {
   View,
   ViewStyle,
 } from 'react-native'
+import {AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api'
 import {Text} from '../text/Text'
 import {PostDropdownBtn} from '../forms/PostDropdownBtn'
 import {HeartIcon, HeartIconSolid, CommentBottomArrow} from 'lib/icons'
 import {s, colors} from 'lib/styles'
 import {pluralize} from 'lib/strings/helpers'
 import {useTheme} from 'lib/ThemeContext'
-import {useStores} from 'state/index'
 import {RepostButton} from './RepostButton'
 import {Haptics} from 'lib/haptics'
 import {HITSLOP_10, HITSLOP_20} from 'lib/constants'
+import {useModalControls} from '#/state/modals'
+import {
+  usePostLikeMutation,
+  usePostUnlikeMutation,
+  usePostRepostMutation,
+  usePostUnrepostMutation,
+} from '#/state/queries/post'
+import {useComposerControls} from '#/state/shell/composer'
+import {Shadow} from '#/state/cache/types'
+import {useRequireAuth} from '#/state/session'
 
-interface PostCtrlsOpts {
-  itemUri: string
-  itemCid: string
-  itemHref: string
-  itemTitle: string
-  isAuthor: boolean
-  author: {
-    did: string
-    handle: string
-    displayName?: string | undefined
-    avatar?: string | undefined
-  }
-  text: string
-  indexedAt: string
+export function PostCtrls({
+  big,
+  post,
+  record,
+  style,
+  onPressReply,
+}: {
   big?: boolean
+  post: Shadow<AppBskyFeedDefs.PostView>
+  record: AppBskyFeedPost.Record
   style?: StyleProp<ViewStyle>
-  replyCount?: number
-  repostCount?: number
-  likeCount?: number
-  isReposted: boolean
-  isLiked: boolean
-  isThreadMuted: boolean
   onPressReply: () => void
-  onPressToggleRepost: () => Promise<void>
-  onPressToggleLike: () => Promise<void>
-  onCopyPostText: () => void
-  onOpenTranslate: () => void
-  onToggleThreadMute: () => void
-  onDeletePost: () => void
-}
-
-export function PostCtrls(opts: PostCtrlsOpts) {
-  const store = useStores()
+}) {
   const theme = useTheme()
+  const {openComposer} = useComposerControls()
+  const {closeModal} = useModalControls()
+  const postLikeMutation = usePostLikeMutation()
+  const postUnlikeMutation = usePostUnlikeMutation()
+  const postRepostMutation = usePostRepostMutation()
+  const postUnrepostMutation = usePostUnrepostMutation()
+  const requireAuth = useRequireAuth()
+
   const defaultCtrlColor = React.useMemo(
     () => ({
       color: theme.palette.default.postCtrl,
     }),
     [theme],
   ) as StyleProp<ViewStyle>
+
+  const onPressToggleLike = React.useCallback(async () => {
+    if (!post.viewer?.like) {
+      Haptics.default()
+      postLikeMutation.mutate({
+        uri: post.uri,
+        cid: post.cid,
+        likeCount: post.likeCount || 0,
+      })
+    } else {
+      postUnlikeMutation.mutate({
+        postUri: post.uri,
+        likeUri: post.viewer.like,
+        likeCount: post.likeCount || 0,
+      })
+    }
+  }, [post, postLikeMutation, postUnlikeMutation])
+
   const onRepost = useCallback(() => {
-    store.shell.closeModal()
-    if (!opts.isReposted) {
+    closeModal()
+    if (!post.viewer?.repost) {
       Haptics.default()
-      opts.onPressToggleRepost().catch(_e => undefined)
+      postRepostMutation.mutate({
+        uri: post.uri,
+        cid: post.cid,
+        repostCount: post.repostCount || 0,
+      })
     } else {
-      opts.onPressToggleRepost().catch(_e => undefined)
+      postUnrepostMutation.mutate({
+        postUri: post.uri,
+        repostUri: post.viewer.repost,
+        repostCount: post.repostCount || 0,
+      })
     }
-  }, [opts, store.shell])
+  }, [post, closeModal, postRepostMutation, postUnrepostMutation])
 
   const onQuote = useCallback(() => {
-    store.shell.closeModal()
-    store.shell.openComposer({
+    closeModal()
+    openComposer({
       quote: {
-        uri: opts.itemUri,
-        cid: opts.itemCid,
-        text: opts.text,
-        author: opts.author,
-        indexedAt: opts.indexedAt,
+        uri: post.uri,
+        cid: post.cid,
+        text: record.text,
+        author: post.author,
+        indexedAt: post.indexedAt,
       },
     })
     Haptics.default()
-  }, [
-    opts.author,
-    opts.indexedAt,
-    opts.itemCid,
-    opts.itemUri,
-    opts.text,
-    store.shell,
-  ])
-
-  const onPressToggleLikeWrapper = async () => {
-    if (!opts.isLiked) {
-      Haptics.default()
-      await opts.onPressToggleLike().catch(_e => undefined)
-    } else {
-      await opts.onPressToggleLike().catch(_e => undefined)
-    }
-  }
-
+  }, [post, record, openComposer, closeModal])
   return (
-    <View style={[styles.ctrls, opts.style]}>
+    <View style={[styles.ctrls, style]}>
       <TouchableOpacity
         testID="replyBtn"
-        style={[styles.ctrl, !opts.big && styles.ctrlPad, {paddingLeft: 0}]}
-        onPress={opts.onPressReply}
+        style={[styles.ctrl, !big && styles.ctrlPad, {paddingLeft: 0}]}
+        onPress={() => {
+          requireAuth(() => onPressReply())
+        }}
         accessibilityRole="button"
-        accessibilityLabel={`Reply (${opts.replyCount} ${
-          opts.replyCount === 1 ? 'reply' : 'replies'
+        accessibilityLabel={`Reply (${post.replyCount} ${
+          post.replyCount === 1 ? 'reply' : 'replies'
         })`}
         accessibilityHint=""
-        hitSlop={opts.big ? HITSLOP_20 : HITSLOP_10}>
+        hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
         <CommentBottomArrow
-          style={[defaultCtrlColor, opts.big ? s.mt2 : styles.mt1]}
+          style={[defaultCtrlColor, big ? s.mt2 : styles.mt1]}
           strokeWidth={3}
-          size={opts.big ? 20 : 15}
+          size={big ? 20 : 15}
         />
-        {typeof opts.replyCount !== 'undefined' ? (
+        {typeof post.replyCount !== 'undefined' ? (
           <Text style={[defaultCtrlColor, s.ml5, s.f15]}>
-            {opts.replyCount}
+            {post.replyCount}
           </Text>
         ) : undefined}
       </TouchableOpacity>
-      <RepostButton {...opts} onRepost={onRepost} onQuote={onQuote} />
+      <RepostButton
+        big={big}
+        isReposted={!!post.viewer?.repost}
+        repostCount={post.repostCount}
+        onRepost={onRepost}
+        onQuote={onQuote}
+      />
       <TouchableOpacity
         testID="likeBtn"
-        style={[styles.ctrl, !opts.big && styles.ctrlPad]}
-        onPress={onPressToggleLikeWrapper}
+        style={[styles.ctrl, !big && styles.ctrlPad]}
+        onPress={() => {
+          requireAuth(() => onPressToggleLike())
+        }}
         accessibilityRole="button"
-        accessibilityLabel={`${opts.isLiked ? 'Unlike' : 'Like'} (${
-          opts.likeCount
-        } ${pluralize(opts.likeCount || 0, 'like')})`}
+        accessibilityLabel={`${post.viewer?.like ? 'Unlike' : 'Like'} (${
+          post.likeCount
+        } ${pluralize(post.likeCount || 0, 'like')})`}
         accessibilityHint=""
-        hitSlop={opts.big ? HITSLOP_20 : HITSLOP_10}>
-        {opts.isLiked ? (
-          <HeartIconSolid
-            style={styles.ctrlIconLiked}
-            size={opts.big ? 22 : 16}
-          />
+        hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
+        {post.viewer?.like ? (
+          <HeartIconSolid style={styles.ctrlIconLiked} size={big ? 22 : 16} />
         ) : (
           <HeartIcon
-            style={[defaultCtrlColor, opts.big ? styles.mt1 : undefined]}
+            style={[defaultCtrlColor, big ? styles.mt1 : undefined]}
             strokeWidth={3}
-            size={opts.big ? 20 : 16}
+            size={big ? 20 : 16}
           />
         )}
-        {typeof opts.likeCount !== 'undefined' ? (
+        {typeof post.likeCount !== 'undefined' ? (
           <Text
             testID="likeCount"
             style={
-              opts.isLiked
+              post.viewer?.like
                 ? [s.bold, s.red3, s.f15, s.ml5]
                 : [defaultCtrlColor, s.f15, s.ml5]
             }>
-            {opts.likeCount}
+            {post.likeCount}
           </Text>
         ) : undefined}
       </TouchableOpacity>
-      {opts.big ? undefined : (
+      {big ? undefined : (
         <PostDropdownBtn
           testID="postDropdownBtn"
-          itemUri={opts.itemUri}
-          itemCid={opts.itemCid}
-          itemHref={opts.itemHref}
-          itemTitle={opts.itemTitle}
-          isAuthor={opts.isAuthor}
-          isThreadMuted={opts.isThreadMuted}
-          onCopyPostText={opts.onCopyPostText}
-          onOpenTranslate={opts.onOpenTranslate}
-          onToggleThreadMute={opts.onToggleThreadMute}
-          onDeletePost={opts.onDeletePost}
+          post={post}
+          record={record}
           style={styles.ctrlPad}
         />
       )}
diff --git a/src/view/com/util/post-ctrls/RepostButton.tsx b/src/view/com/util/post-ctrls/RepostButton.tsx
index 9c4ed8e5d..1d34a88ab 100644
--- a/src/view/com/util/post-ctrls/RepostButton.tsx
+++ b/src/view/com/util/post-ctrls/RepostButton.tsx
@@ -5,8 +5,9 @@ import {s, colors} from 'lib/styles'
 import {useTheme} from 'lib/ThemeContext'
 import {Text} from '../text/Text'
 import {pluralize} from 'lib/strings/helpers'
-import {useStores} from 'state/index'
 import {HITSLOP_10, HITSLOP_20} from 'lib/constants'
+import {useModalControls} from '#/state/modals'
+import {useRequireAuth} from '#/state/session'
 
 interface Props {
   isReposted: boolean
@@ -23,8 +24,9 @@ export const RepostButton = ({
   onRepost,
   onQuote,
 }: Props) => {
-  const store = useStores()
   const theme = useTheme()
+  const {openModal} = useModalControls()
+  const requireAuth = useRequireAuth()
 
   const defaultControlColor = React.useMemo(
     () => ({
@@ -34,18 +36,20 @@ export const RepostButton = ({
   )
 
   const onPressToggleRepostWrapper = useCallback(() => {
-    store.shell.openModal({
+    openModal({
       name: 'repost',
       onRepost: onRepost,
       onQuote: onQuote,
       isReposted,
     })
-  }, [onRepost, onQuote, isReposted, store.shell])
+  }, [onRepost, onQuote, isReposted, openModal])
 
   return (
     <TouchableOpacity
       testID="repostBtn"
-      onPress={onPressToggleRepostWrapper}
+      onPress={() => {
+        requireAuth(() => onPressToggleRepostWrapper())
+      }}
       style={[styles.control, !big && styles.controlPad]}
       accessibilityRole="button"
       accessibilityLabel={`${
diff --git a/src/view/com/util/post-ctrls/RepostButton.web.tsx b/src/view/com/util/post-ctrls/RepostButton.web.tsx
index 57f544d41..329382132 100644
--- a/src/view/com/util/post-ctrls/RepostButton.web.tsx
+++ b/src/view/com/util/post-ctrls/RepostButton.web.tsx
@@ -1,5 +1,5 @@
 import React from 'react'
-import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
+import {StyleProp, StyleSheet, View, ViewStyle, Pressable} from 'react-native'
 import {RepostIcon} from 'lib/icons'
 import {colors} from 'lib/styles'
 import {useTheme} from 'lib/ThemeContext'
@@ -10,6 +10,10 @@ import {
   DropdownItem as NativeDropdownItem,
 } from '../forms/NativeDropdown'
 import {EventStopper} from '../EventStopper'
+import {useLingui} from '@lingui/react'
+import {msg} from '@lingui/macro'
+import {useRequireAuth} from '#/state/session'
+import {useSession} from '#/state/session'
 
 interface Props {
   isReposted: boolean
@@ -28,6 +32,9 @@ export const RepostButton = ({
   onQuote,
 }: Props) => {
   const theme = useTheme()
+  const {_} = useLingui()
+  const {hasSession} = useSession()
+  const requireAuth = useRequireAuth()
 
   const defaultControlColor = React.useMemo(
     () => ({
@@ -38,7 +45,7 @@ export const RepostButton = ({
 
   const dropdownItems: NativeDropdownItem[] = [
     {
-      label: isReposted ? 'Undo repost' : 'Repost',
+      label: isReposted ? _(msg`Undo repost`) : _(msg`Repost`),
       testID: 'repostDropdownRepostBtn',
       icon: {
         ios: {name: 'repeat'},
@@ -48,7 +55,7 @@ export const RepostButton = ({
       onPress: onRepost,
     },
     {
-      label: 'Quote post',
+      label: _(msg`Quote post`),
       testID: 'repostDropdownQuoteBtn',
       icon: {
         ios: {name: 'quote.bubble'},
@@ -59,32 +66,46 @@ export const RepostButton = ({
     },
   ]
 
-  return (
+  const inner = (
+    <View
+      style={[
+        styles.control,
+        !big && styles.controlPad,
+        (isReposted
+          ? styles.reposted
+          : defaultControlColor) as StyleProp<ViewStyle>,
+      ]}>
+      <RepostIcon strokeWidth={2.2} size={big ? 24 : 20} />
+      {typeof repostCount !== 'undefined' ? (
+        <Text
+          testID="repostCount"
+          type={isReposted ? 'md-bold' : 'md'}
+          style={styles.repostCount}>
+          {repostCount ?? 0}
+        </Text>
+      ) : undefined}
+    </View>
+  )
+
+  return hasSession ? (
     <EventStopper>
       <NativeDropdown
         items={dropdownItems}
-        accessibilityLabel="Repost or quote post"
+        accessibilityLabel={_(msg`Repost or quote post`)}
         accessibilityHint="">
-        <View
-          style={[
-            styles.control,
-            !big && styles.controlPad,
-            (isReposted
-              ? styles.reposted
-              : defaultControlColor) as StyleProp<ViewStyle>,
-          ]}>
-          <RepostIcon strokeWidth={2.2} size={big ? 24 : 20} />
-          {typeof repostCount !== 'undefined' ? (
-            <Text
-              testID="repostCount"
-              type={isReposted ? 'md-bold' : 'md'}
-              style={styles.repostCount}>
-              {repostCount ?? 0}
-            </Text>
-          ) : undefined}
-        </View>
+        {inner}
       </NativeDropdown>
     </EventStopper>
+  ) : (
+    <Pressable
+      accessibilityRole="button"
+      onPress={() => {
+        requireAuth(() => {})
+      }}
+      accessibilityLabel={_(msg`Repost or quote post`)}
+      accessibilityHint="">
+      {inner}
+    </Pressable>
   )
 }
 
diff --git a/src/view/com/util/post-embeds/CustomFeedEmbed.tsx b/src/view/com/util/post-embeds/CustomFeedEmbed.tsx
deleted file mode 100644
index 624157436..000000000
--- a/src/view/com/util/post-embeds/CustomFeedEmbed.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import React, {useMemo} from 'react'
-import {AppBskyFeedDefs} from '@atproto/api'
-import {usePalette} from 'lib/hooks/usePalette'
-import {StyleSheet} from 'react-native'
-import {useStores} from 'state/index'
-import {FeedSourceModel} from 'state/models/content/feed-source'
-import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
-
-export function CustomFeedEmbed({
-  record,
-}: {
-  record: AppBskyFeedDefs.GeneratorView
-}) {
-  const pal = usePalette('default')
-  const store = useStores()
-  const item = useMemo(() => {
-    const model = new FeedSourceModel(store, record.uri)
-    model.hydrateFeedGenerator(record)
-    return model
-  }, [store, record])
-  return (
-    <FeedSourceCard
-      item={item}
-      style={[pal.view, pal.border, styles.customFeedOuter]}
-      showLikes
-    />
-  )
-}
-
-const styles = StyleSheet.create({
-  customFeedOuter: {
-    borderWidth: 1,
-    borderRadius: 8,
-    marginTop: 4,
-    paddingHorizontal: 12,
-    paddingVertical: 12,
-  },
-})
diff --git a/src/view/com/util/post-embeds/ListEmbed.tsx b/src/view/com/util/post-embeds/ListEmbed.tsx
index dbf350039..fc5ad270f 100644
--- a/src/view/com/util/post-embeds/ListEmbed.tsx
+++ b/src/view/com/util/post-embeds/ListEmbed.tsx
@@ -1,12 +1,11 @@
 import React from 'react'
 import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
 import {usePalette} from 'lib/hooks/usePalette'
-import {observer} from 'mobx-react-lite'
 import {ListCard} from 'view/com/lists/ListCard'
 import {AppBskyGraphDefs} from '@atproto/api'
 import {s} from 'lib/styles'
 
-export const ListEmbed = observer(function ListEmbedImpl({
+export function ListEmbed({
   item,
   style,
 }: {
@@ -20,7 +19,7 @@ export const ListEmbed = observer(function ListEmbedImpl({
       <ListCard list={item} style={[style, styles.card]} />
     </View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   container: {
diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx
index f82b5b7df..e793f983e 100644
--- a/src/view/com/util/post-embeds/QuoteEmbed.tsx
+++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx
@@ -12,7 +12,7 @@ import {PostMeta} from '../PostMeta'
 import {Link} from '../Link'
 import {Text} from '../text/Text'
 import {usePalette} from 'lib/hooks/usePalette'
-import {ComposerOptsQuote} from 'state/models/ui/shell'
+import {ComposerOptsQuote} from 'state/shell/composer'
 import {PostEmbeds} from '.'
 import {PostAlerts} from '../moderation/PostAlerts'
 import {makeProfileLink} from 'lib/routes/links'
diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index 6c13bc2bb..ca3bf1104 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -19,8 +19,7 @@ import {
 } from '@atproto/api'
 import {Link} from '../Link'
 import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
-import {ImagesLightbox} from 'state/models/ui/shell'
-import {useStores} from 'state/index'
+import {useLightboxControls, ImagesLightbox} from '#/state/lightbox'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {YoutubeEmbed} from './YoutubeEmbed'
@@ -28,9 +27,9 @@ import {ExternalLinkEmbed} from './ExternalLinkEmbed'
 import {getYoutubeVideoId} from 'lib/strings/url-helpers'
 import {MaybeQuoteEmbed} from './QuoteEmbed'
 import {AutoSizedImage} from '../images/AutoSizedImage'
-import {CustomFeedEmbed} from './CustomFeedEmbed'
 import {ListEmbed} from './ListEmbed'
 import {isCauseALabelOnUri} from 'lib/moderation'
+import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
 
 type Embed =
   | AppBskyEmbedRecord.View
@@ -49,7 +48,7 @@ export function PostEmbeds({
   style?: StyleProp<ViewStyle>
 }) {
   const pal = usePalette('default')
-  const store = useStores()
+  const {openLightbox} = useLightboxControls()
   const {isMobile} = useWebMediaQueries()
 
   // quote post with media
@@ -72,7 +71,13 @@ export function PostEmbeds({
     // custom feed embed (i.e. generator view)
     // =
     if (AppBskyFeedDefs.isGeneratorView(embed.record)) {
-      return <CustomFeedEmbed record={embed.record} />
+      return (
+        <FeedSourceCard
+          feedUri={embed.record.uri}
+          style={[pal.view, pal.border, styles.customFeedOuter]}
+          showLikes
+        />
+      )
     }
 
     // list embed
@@ -98,8 +103,8 @@ export function PostEmbeds({
         alt: img.alt,
         aspectRatio: img.aspectRatio,
       }))
-      const openLightbox = (index: number) => {
-        store.shell.openLightbox(new ImagesLightbox(items, index))
+      const _openLightbox = (index: number) => {
+        openLightbox(new ImagesLightbox(items, index))
       }
       const onPressIn = (_: number) => {
         InteractionManager.runAfterInteractions(() => {
@@ -115,7 +120,7 @@ export function PostEmbeds({
               alt={alt}
               uri={thumb}
               dimensionsHint={aspectRatio}
-              onPress={() => openLightbox(0)}
+              onPress={() => _openLightbox(0)}
               onPressIn={() => onPressIn(0)}
               style={[
                 styles.singleImage,
@@ -137,7 +142,7 @@ export function PostEmbeds({
         <View style={[styles.imagesContainer, style]}>
           <ImageLayoutGrid
             images={embed.images}
-            onPress={openLightbox}
+            onPress={_openLightbox}
             onPressIn={onPressIn}
             style={
               embed.images.length === 1
@@ -206,4 +211,11 @@ const styles = StyleSheet.create({
     fontSize: 10,
     fontWeight: 'bold',
   },
+  customFeedOuter: {
+    borderWidth: 1,
+    borderRadius: 8,
+    marginTop: 4,
+    paddingHorizontal: 12,
+    paddingVertical: 12,
+  },
 })