about summary refs log tree commit diff
path: root/src/screens/Messages/Conversation
diff options
context:
space:
mode:
Diffstat (limited to 'src/screens/Messages/Conversation')
-rw-r--r--src/screens/Messages/Conversation/ChatDisabled.tsx150
-rw-r--r--src/screens/Messages/Conversation/MessageInput.tsx180
-rw-r--r--src/screens/Messages/Conversation/MessageInput.web.tsx238
-rw-r--r--src/screens/Messages/Conversation/MessageInputEmbed.tsx219
-rw-r--r--src/screens/Messages/Conversation/MessageListError.tsx61
-rw-r--r--src/screens/Messages/Conversation/MessagesList.tsx454
-rw-r--r--src/screens/Messages/Conversation/index.tsx205
7 files changed, 0 insertions, 1507 deletions
diff --git a/src/screens/Messages/Conversation/ChatDisabled.tsx b/src/screens/Messages/Conversation/ChatDisabled.tsx
deleted file mode 100644
index c768d2504..000000000
--- a/src/screens/Messages/Conversation/ChatDisabled.tsx
+++ /dev/null
@@ -1,150 +0,0 @@
-import React, {useCallback, useState} from 'react'
-import {View} from 'react-native'
-import {ComAtprotoModerationDefs} from '@atproto/api'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useMutation} from '@tanstack/react-query'
-
-import {logger} from '#/logger'
-import {useAgent, useSession} from '#/state/session'
-import * as Toast from '#/view/com/util/Toast'
-import {atoms as a, useBreakpoints, useTheme} from '#/alf'
-import {Button, ButtonIcon, ButtonText} from '#/components/Button'
-import * as Dialog from '#/components/Dialog'
-import {Loader} from '#/components/Loader'
-import {Text} from '#/components/Typography'
-
-export function ChatDisabled() {
-  const t = useTheme()
-  return (
-    <View style={[a.p_md]}>
-      <View
-        style={[a.align_start, a.p_xl, a.rounded_md, t.atoms.bg_contrast_25]}>
-        <Text
-          style={[a.text_md, a.font_bold, a.pb_sm, t.atoms.text_contrast_high]}>
-          <Trans>Your chats have been disabled</Trans>
-        </Text>
-        <Text style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
-          <Trans>
-            Our moderators have reviewed reports and decided to disable your
-            access to chats on Bluesky.
-          </Trans>
-        </Text>
-        <AppealDialog />
-      </View>
-    </View>
-  )
-}
-
-function AppealDialog() {
-  const control = Dialog.useDialogControl()
-  const {_} = useLingui()
-
-  return (
-    <>
-      <Button
-        testID="appealDisabledChatBtn"
-        variant="ghost"
-        color="secondary"
-        size="small"
-        onPress={control.open}
-        label={_(msg`Appeal this decision`)}
-        style={a.mt_sm}>
-        <ButtonText>{_(msg`Appeal this decision`)}</ButtonText>
-      </Button>
-
-      <Dialog.Outer control={control}>
-        <Dialog.Handle />
-        <DialogInner />
-      </Dialog.Outer>
-    </>
-  )
-}
-
-function DialogInner() {
-  const {_} = useLingui()
-  const control = Dialog.useDialogContext()
-  const [details, setDetails] = useState('')
-  const {gtMobile} = useBreakpoints()
-  const agent = useAgent()
-  const {currentAccount} = useSession()
-
-  const {mutate, isPending} = useMutation({
-    mutationFn: async () => {
-      if (!currentAccount)
-        throw new Error('No current account, should be unreachable')
-      await agent.createModerationReport({
-        reasonType: ComAtprotoModerationDefs.REASONAPPEAL,
-        subject: {
-          $type: 'com.atproto.admin.defs#repoRef',
-          did: currentAccount.did,
-        },
-        reason: details,
-      })
-    },
-    onError: err => {
-      logger.error('Failed to submit chat appeal', {message: err})
-      Toast.show(_(msg`Failed to submit appeal, please try again.`), 'xmark')
-    },
-    onSuccess: () => {
-      control.close()
-      Toast.show(_(msg`Appeal submitted`))
-    },
-  })
-
-  const onSubmit = useCallback(() => mutate(), [mutate])
-  const onBack = useCallback(() => control.close(), [control])
-
-  return (
-    <Dialog.ScrollableInner label={_(msg`Appeal this decision`)}>
-      <Text style={[a.text_2xl, a.font_bold, a.pb_xs, a.leading_tight]}>
-        <Trans>Appeal this decision</Trans>
-      </Text>
-      <Text style={[a.text_md, a.leading_snug]}>
-        <Trans>This appeal will be sent to Bluesky's moderation service.</Trans>
-      </Text>
-      <View style={[a.my_md]}>
-        <Dialog.Input
-          label={_(msg`Text input field`)}
-          placeholder={_(
-            msg`Please explain why you think your chats were incorrectly disabled`,
-          )}
-          value={details}
-          onChangeText={setDetails}
-          autoFocus={true}
-          numberOfLines={3}
-          multiline
-          maxLength={300}
-        />
-      </View>
-
-      <View
-        style={
-          gtMobile
-            ? [a.flex_row, a.justify_between]
-            : [{flexDirection: 'column-reverse'}, a.gap_sm]
-        }>
-        <Button
-          testID="backBtn"
-          variant="solid"
-          color="secondary"
-          size="large"
-          onPress={onBack}
-          label={_(msg`Back`)}>
-          <ButtonText>{_(msg`Back`)}</ButtonText>
-        </Button>
-        <Button
-          testID="submitBtn"
-          variant="solid"
-          color="primary"
-          size="large"
-          onPress={onSubmit}
-          label={_(msg`Submit`)}>
-          <ButtonText>{_(msg`Submit`)}</ButtonText>
-          {isPending && <ButtonIcon icon={Loader} />}
-        </Button>
-      </View>
-      <Dialog.Close />
-    </Dialog.ScrollableInner>
-  )
-}
diff --git a/src/screens/Messages/Conversation/MessageInput.tsx b/src/screens/Messages/Conversation/MessageInput.tsx
deleted file mode 100644
index 674edc41e..000000000
--- a/src/screens/Messages/Conversation/MessageInput.tsx
+++ /dev/null
@@ -1,180 +0,0 @@
-import React from 'react'
-import {Pressable, TextInput, useWindowDimensions, View} from 'react-native'
-import {
-  useFocusedInputHandler,
-  useReanimatedKeyboardAnimation,
-} from 'react-native-keyboard-controller'
-import Animated, {
-  measure,
-  useAnimatedProps,
-  useAnimatedRef,
-  useAnimatedStyle,
-  useSharedValue,
-} from 'react-native-reanimated'
-import {useSafeAreaInsets} from 'react-native-safe-area-context'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import Graphemer from 'graphemer'
-
-import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants'
-import {useHaptics} from '#/lib/haptics'
-import {
-  useMessageDraft,
-  useSaveMessageDraft,
-} from '#/state/messages/message-drafts'
-import {isIOS} from 'platform/detection'
-import {EmojiPickerPosition} from '#/view/com/composer/text-input/web/EmojiPicker.web'
-import * as Toast from '#/view/com/util/Toast'
-import {atoms as a, useTheme} from '#/alf'
-import {useSharedInputStyles} from '#/components/forms/TextField'
-import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
-import {useExtractEmbedFromFacets} from './MessageInputEmbed'
-
-const AnimatedTextInput = Animated.createAnimatedComponent(TextInput)
-
-export function MessageInput({
-  onSendMessage,
-  hasEmbed,
-  setEmbed,
-  children,
-}: {
-  onSendMessage: (message: string) => void
-  hasEmbed: boolean
-  setEmbed: (embedUrl: string | undefined) => void
-  children?: React.ReactNode
-  openEmojiPicker?: (pos: EmojiPickerPosition) => void
-}) {
-  const {_} = useLingui()
-  const t = useTheme()
-  const playHaptic = useHaptics()
-  const {getDraft, clearDraft} = useMessageDraft()
-
-  // Input layout
-  const {top: topInset} = useSafeAreaInsets()
-  const {height: windowHeight} = useWindowDimensions()
-  const {height: keyboardHeight} = useReanimatedKeyboardAnimation()
-  const maxHeight = useSharedValue<undefined | number>(undefined)
-  const isInputScrollable = useSharedValue(false)
-
-  const inputStyles = useSharedInputStyles()
-  const [isFocused, setIsFocused] = React.useState(false)
-  const [message, setMessage] = React.useState(getDraft)
-  const inputRef = useAnimatedRef<TextInput>()
-
-  useSaveMessageDraft(message)
-  useExtractEmbedFromFacets(message, setEmbed)
-
-  const onSubmit = React.useCallback(() => {
-    if (!hasEmbed && message.trim() === '') {
-      return
-    }
-    if (new Graphemer().countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) {
-      Toast.show(_(msg`Message is too long`), 'xmark')
-      return
-    }
-    clearDraft()
-    onSendMessage(message)
-    playHaptic()
-    setMessage('')
-    setEmbed(undefined)
-
-    // Pressing the send button causes the text input to lose focus, so we need to
-    // re-focus it after sending
-    setTimeout(() => {
-      inputRef.current?.focus()
-    }, 100)
-  }, [
-    hasEmbed,
-    message,
-    clearDraft,
-    onSendMessage,
-    playHaptic,
-    setEmbed,
-    _,
-    inputRef,
-  ])
-
-  useFocusedInputHandler(
-    {
-      onChangeText: () => {
-        'worklet'
-        const measurement = measure(inputRef)
-        if (!measurement) return
-
-        const max = windowHeight - -keyboardHeight.value - topInset - 150
-        const availableSpace = max - measurement.height
-
-        maxHeight.value = max
-        isInputScrollable.value = availableSpace < 30
-      },
-    },
-    [windowHeight, topInset],
-  )
-
-  const animatedStyle = useAnimatedStyle(() => ({
-    maxHeight: maxHeight.value,
-  }))
-
-  const animatedProps = useAnimatedProps(() => ({
-    scrollEnabled: isInputScrollable.value,
-  }))
-
-  return (
-    <View style={[a.px_md, a.pb_sm, a.pt_xs]}>
-      {children}
-      <View
-        style={[
-          a.w_full,
-          a.flex_row,
-          t.atoms.bg_contrast_25,
-          {
-            padding: a.p_sm.padding - 2,
-            paddingLeft: a.p_md.padding - 2,
-            borderWidth: 1,
-            borderRadius: 23,
-            borderColor: 'transparent',
-          },
-          isFocused && inputStyles.chromeFocus,
-        ]}>
-        <AnimatedTextInput
-          accessibilityLabel={_(msg`Message input field`)}
-          accessibilityHint={_(msg`Type your message here`)}
-          placeholder={_(msg`Write a message`)}
-          placeholderTextColor={t.palette.contrast_500}
-          value={message}
-          multiline={true}
-          onChangeText={setMessage}
-          style={[
-            a.flex_1,
-            a.text_md,
-            a.px_sm,
-            t.atoms.text,
-            {paddingBottom: isIOS ? 5 : 0},
-            animatedStyle,
-          ]}
-          keyboardAppearance={t.name === 'light' ? 'light' : 'dark'}
-          blurOnSubmit={false}
-          onFocus={() => setIsFocused(true)}
-          onBlur={() => setIsFocused(false)}
-          ref={inputRef}
-          hitSlop={HITSLOP_10}
-          animatedProps={animatedProps}
-        />
-        <Pressable
-          accessibilityRole="button"
-          accessibilityLabel={_(msg`Send message`)}
-          accessibilityHint=""
-          hitSlop={HITSLOP_10}
-          style={[
-            a.rounded_full,
-            a.align_center,
-            a.justify_center,
-            {height: 30, width: 30, backgroundColor: t.palette.primary_500},
-          ]}
-          onPress={onSubmit}>
-          <PaperPlane fill={t.palette.white} style={[a.relative, {left: 1}]} />
-        </Pressable>
-      </View>
-    </View>
-  )
-}
diff --git a/src/screens/Messages/Conversation/MessageInput.web.tsx b/src/screens/Messages/Conversation/MessageInput.web.tsx
deleted file mode 100644
index 0b7e47920..000000000
--- a/src/screens/Messages/Conversation/MessageInput.web.tsx
+++ /dev/null
@@ -1,238 +0,0 @@
-import React from 'react'
-import {Pressable, StyleSheet, View} from 'react-native'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import Graphemer from 'graphemer'
-import TextareaAutosize from 'react-textarea-autosize'
-
-import {MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants'
-import {
-  useMessageDraft,
-  useSaveMessageDraft,
-} from '#/state/messages/message-drafts'
-import {isSafari, isTouchDevice} from 'lib/browser'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter'
-import {
-  Emoji,
-  EmojiPickerPosition,
-} from '#/view/com/composer/text-input/web/EmojiPicker.web'
-import * as Toast from '#/view/com/util/Toast'
-import {atoms as a, useTheme} from '#/alf'
-import {Button} from '#/components/Button'
-import {useSharedInputStyles} from '#/components/forms/TextField'
-import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
-import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
-import {useExtractEmbedFromFacets} from './MessageInputEmbed'
-
-export function MessageInput({
-  onSendMessage,
-  hasEmbed,
-  setEmbed,
-  children,
-  openEmojiPicker,
-}: {
-  onSendMessage: (message: string) => void
-  hasEmbed: boolean
-  setEmbed: (embedUrl: string | undefined) => void
-  children?: React.ReactNode
-  openEmojiPicker?: (pos: EmojiPickerPosition) => void
-}) {
-  const {isTabletOrDesktop} = useWebMediaQueries()
-  const {_} = useLingui()
-  const t = useTheme()
-  const {getDraft, clearDraft} = useMessageDraft()
-  const [message, setMessage] = React.useState(getDraft)
-
-  const inputStyles = useSharedInputStyles()
-  const isComposing = React.useRef(false)
-  const [isFocused, setIsFocused] = React.useState(false)
-  const [isHovered, setIsHovered] = React.useState(false)
-  const [textAreaHeight, setTextAreaHeight] = React.useState(38)
-  const textAreaRef = React.useRef<HTMLTextAreaElement>(null)
-
-  const onSubmit = React.useCallback(() => {
-    if (!hasEmbed && message.trim() === '') {
-      return
-    }
-    if (new Graphemer().countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) {
-      Toast.show(_(msg`Message is too long`), 'xmark')
-      return
-    }
-    clearDraft()
-    onSendMessage(message)
-    setMessage('')
-    setEmbed(undefined)
-  }, [message, onSendMessage, _, clearDraft, hasEmbed, setEmbed])
-
-  const onKeyDown = React.useCallback(
-    (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
-      // Don't submit the form when the Japanese or any other IME is composing
-      if (isComposing.current) return
-
-      // see https://github.com/bluesky-social/social-app/issues/4178
-      // see https://www.stum.de/2016/06/24/handling-ime-events-in-javascript/
-      // see https://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html
-      //
-      // On Safari, the final keydown event to dismiss the IME - which is the enter key - is also "Enter" below.
-      // Obviously, this causes problems because the final dismissal should _not_ submit the text, but should just
-      // stop the IME editing. This is the behavior of Chrome and Firefox, but not Safari.
-      //
-      // Keycode is deprecated, however the alternative seems to only be to compare the timestamp from the
-      // onCompositionEnd event to the timestamp of the keydown event, which is not reliable. For example, this hack
-      // uses that method: https://github.com/ProseMirror/prosemirror-view/pull/44. However, from my 500ms resulted in
-      // far too long of a delay, and a subsequent enter press would often just end up doing nothing. A shorter time
-      // frame was also not great, since it was too short to be reliable (i.e. an older system might have a larger
-      // time gap between the two events firing.
-      if (isSafari && e.key === 'Enter' && e.keyCode === 229) {
-        return
-      }
-
-      if (e.key === 'Enter') {
-        if (e.shiftKey) return
-        e.preventDefault()
-        onSubmit()
-      }
-    },
-    [onSubmit],
-  )
-
-  const onChange = React.useCallback(
-    (e: React.ChangeEvent<HTMLTextAreaElement>) => {
-      setMessage(e.target.value)
-    },
-    [],
-  )
-
-  const onEmojiInserted = React.useCallback(
-    (emoji: Emoji) => {
-      const position = textAreaRef.current?.selectionStart ?? 0
-      setMessage(
-        message =>
-          message.slice(0, position) + emoji.native + message.slice(position),
-      )
-    },
-    [setMessage],
-  )
-  React.useEffect(() => {
-    textInputWebEmitter.addListener('emoji-inserted', onEmojiInserted)
-    return () => {
-      textInputWebEmitter.removeListener('emoji-inserted', onEmojiInserted)
-    }
-  }, [onEmojiInserted])
-
-  useSaveMessageDraft(message)
-  useExtractEmbedFromFacets(message, setEmbed)
-
-  return (
-    <View style={a.p_sm}>
-      {children}
-      <View
-        style={[
-          a.flex_row,
-          t.atoms.bg_contrast_25,
-          {
-            paddingRight: a.p_sm.padding - 2,
-            paddingLeft: a.p_sm.padding - 2,
-            borderWidth: 1,
-            borderRadius: 23,
-            borderColor: 'transparent',
-            height: textAreaHeight + 23,
-          },
-          isHovered && inputStyles.chromeHover,
-          isFocused && inputStyles.chromeFocus,
-        ]}
-        // @ts-expect-error web only
-        onMouseEnter={() => setIsHovered(true)}
-        onMouseLeave={() => setIsHovered(false)}>
-        <Button
-          onPress={e => {
-            e.currentTarget.measure((_fx, _fy, _width, _height, px, py) => {
-              openEmojiPicker?.({top: py, left: px, right: px, bottom: py})
-            })
-          }}
-          style={[
-            a.rounded_full,
-            a.overflow_hidden,
-            a.align_center,
-            a.justify_center,
-            {
-              marginTop: 5,
-              height: 30,
-              width: 30,
-            },
-          ]}
-          label={_(msg`Open emoji picker`)}>
-          {state => (
-            <View
-              style={[
-                a.absolute,
-                a.inset_0,
-                a.align_center,
-                a.justify_center,
-                {
-                  backgroundColor:
-                    state.hovered || state.focused || state.pressed
-                      ? t.atoms.bg.backgroundColor
-                      : undefined,
-                },
-              ]}>
-              <EmojiSmile size="lg" />
-            </View>
-          )}
-        </Button>
-        <TextareaAutosize
-          ref={textAreaRef}
-          style={StyleSheet.flatten([
-            a.flex_1,
-            a.px_sm,
-            a.border_0,
-            t.atoms.text,
-            {
-              paddingTop: 10,
-              backgroundColor: 'transparent',
-              resize: 'none',
-            },
-          ])}
-          maxRows={12}
-          placeholder={_(msg`Write a message`)}
-          defaultValue=""
-          value={message}
-          dirName="ltr"
-          autoFocus={true}
-          onFocus={() => setIsFocused(true)}
-          onBlur={() => setIsFocused(false)}
-          onCompositionStart={() => {
-            isComposing.current = true
-          }}
-          onCompositionEnd={() => {
-            isComposing.current = false
-          }}
-          onHeightChange={height => setTextAreaHeight(height)}
-          onChange={onChange}
-          // On mobile web phones, we want to keep the same behavior as the native app. Do not submit the message
-          // in these cases.
-          onKeyDown={isTouchDevice && isTabletOrDesktop ? undefined : onKeyDown}
-        />
-        <Pressable
-          accessibilityRole="button"
-          accessibilityLabel={_(msg`Send message`)}
-          accessibilityHint=""
-          style={[
-            a.rounded_full,
-            a.align_center,
-            a.justify_center,
-            {
-              height: 30,
-              width: 30,
-              marginTop: 5,
-              backgroundColor: t.palette.primary_500,
-            },
-          ]}
-          onPress={onSubmit}>
-          <PaperPlane fill={t.palette.white} style={[a.relative, {left: 1}]} />
-        </Pressable>
-      </View>
-    </View>
-  )
-}
diff --git a/src/screens/Messages/Conversation/MessageInputEmbed.tsx b/src/screens/Messages/Conversation/MessageInputEmbed.tsx
deleted file mode 100644
index 2d1551019..000000000
--- a/src/screens/Messages/Conversation/MessageInputEmbed.tsx
+++ /dev/null
@@ -1,219 +0,0 @@
-import React, {useCallback, useEffect, useMemo, useState} from 'react'
-import {LayoutAnimation, View} from 'react-native'
-import {
-  AppBskyFeedPost,
-  AppBskyRichtextFacet,
-  AtUri,
-  RichText as RichTextAPI,
-} from '@atproto/api'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {RouteProp, useNavigation, useRoute} from '@react-navigation/native'
-
-import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
-import {makeProfileLink} from '#/lib/routes/links'
-import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
-import {
-  convertBskyAppUrlIfNeeded,
-  isBskyPostUrl,
-  makeRecordUri,
-} from '#/lib/strings/url-helpers'
-import {useModerationOpts} from '#/state/preferences/moderation-opts'
-import {usePostQuery} from '#/state/queries/post'
-import {PostMeta} from '#/view/com/util/PostMeta'
-import {atoms as a, useTheme} from '#/alf'
-import {Button, ButtonIcon} from '#/components/Button'
-import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
-import {Loader} from '#/components/Loader'
-import * as MediaPreview from '#/components/MediaPreview'
-import {ContentHider} from '#/components/moderation/ContentHider'
-import {PostAlerts} from '#/components/moderation/PostAlerts'
-import {RichText} from '#/components/RichText'
-import {Text} from '#/components/Typography'
-
-export function useMessageEmbed() {
-  const route =
-    useRoute<RouteProp<CommonNavigatorParams, 'MessagesConversation'>>()
-  const navigation = useNavigation<NavigationProp>()
-  const embedFromParams = route.params.embed
-
-  const [embedUri, setEmbed] = useState(embedFromParams)
-
-  if (embedFromParams && embedUri !== embedFromParams) {
-    setEmbed(embedFromParams)
-  }
-
-  return {
-    embedUri,
-    setEmbed: useCallback(
-      (embedUrl: string | undefined) => {
-        if (!embedUrl) {
-          navigation.setParams({embed: ''})
-          setEmbed(undefined)
-          return
-        }
-
-        if (embedFromParams) return
-
-        const url = convertBskyAppUrlIfNeeded(embedUrl)
-        const [_0, user, _1, rkey] = url.split('/').filter(Boolean)
-        const uri = makeRecordUri(user, 'app.bsky.feed.post', rkey)
-
-        setEmbed(uri)
-      },
-      [embedFromParams, navigation],
-    ),
-  }
-}
-
-export function useExtractEmbedFromFacets(
-  message: string,
-  setEmbed: (embedUrl: string | undefined) => void,
-) {
-  const rt = new RichTextAPI({text: message})
-  rt.detectFacetsWithoutResolution()
-
-  let uriFromFacet: string | undefined
-
-  for (const facet of rt.facets ?? []) {
-    for (const feature of facet.features) {
-      if (AppBskyRichtextFacet.isLink(feature) && isBskyPostUrl(feature.uri)) {
-        uriFromFacet = feature.uri
-        break
-      }
-    }
-  }
-
-  useEffect(() => {
-    if (uriFromFacet) {
-      setEmbed(uriFromFacet)
-    }
-  }, [uriFromFacet, setEmbed])
-}
-
-export function MessageInputEmbed({
-  embedUri,
-  setEmbed,
-}: {
-  embedUri: string | undefined
-  setEmbed: (embedUrl: string | undefined) => void
-}) {
-  const t = useTheme()
-  const {_} = useLingui()
-
-  const {data: post, status} = usePostQuery(embedUri)
-
-  const moderationOpts = useModerationOpts()
-  const moderation = useMemo(
-    () =>
-      moderationOpts && post ? moderatePost(post, moderationOpts) : undefined,
-    [moderationOpts, post],
-  )
-
-  const {rt, record} = useMemo(() => {
-    if (
-      post &&
-      AppBskyFeedPost.isRecord(post.record) &&
-      AppBskyFeedPost.validateRecord(post.record).success
-    ) {
-      return {
-        rt: new RichTextAPI({
-          text: post.record.text,
-          facets: post.record.facets,
-        }),
-        record: post.record,
-      }
-    }
-
-    return {rt: undefined, record: undefined}
-  }, [post])
-
-  if (!embedUri) {
-    return null
-  }
-
-  let content = null
-  switch (status) {
-    case 'pending':
-      content = (
-        <View
-          style={[a.flex_1, {minHeight: 64}, a.justify_center, a.align_center]}>
-          <Loader />
-        </View>
-      )
-      break
-    case 'error':
-      content = (
-        <View
-          style={[a.flex_1, {minHeight: 64}, a.justify_center, a.align_center]}>
-          <Text style={a.text_center}>Could not fetch post</Text>
-        </View>
-      )
-      break
-    case 'success':
-      const itemUrip = new AtUri(post.uri)
-      const itemHref = makeProfileLink(post.author, 'post', itemUrip.rkey)
-
-      if (!post || !moderation || !rt || !record) {
-        return null
-      }
-
-      content = (
-        <View
-          style={[
-            a.flex_1,
-            t.atoms.bg,
-            t.atoms.border_contrast_low,
-            a.rounded_md,
-            a.border,
-            a.p_sm,
-            a.mb_sm,
-          ]}
-          pointerEvents="none">
-          <PostMeta
-            showAvatar
-            author={post.author}
-            moderation={moderation}
-            timestamp={post.indexedAt}
-            postHref={itemHref}
-            style={a.flex_0}
-          />
-          <ContentHider modui={moderation.ui('contentView')}>
-            <PostAlerts modui={moderation.ui('contentView')} style={a.py_xs} />
-            {rt.text && (
-              <View style={a.mt_xs}>
-                <RichText
-                  enableTags
-                  testID="postText"
-                  value={rt}
-                  style={[a.text_sm, t.atoms.text_contrast_high]}
-                  authorHandle={post.author.handle}
-                  numberOfLines={3}
-                />
-              </View>
-            )}
-            <MediaPreview.Embed embed={post.embed} style={a.mt_sm} />
-          </ContentHider>
-        </View>
-      )
-      break
-  }
-
-  return (
-    <View style={[a.flex_row, a.gap_sm]}>
-      {content}
-      <Button
-        label={_(msg`Remove embed`)}
-        onPress={() => {
-          LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
-          setEmbed(undefined)
-        }}
-        size="tiny"
-        variant="solid"
-        color="secondary"
-        shape="round">
-        <ButtonIcon icon={X} />
-      </Button>
-    </View>
-  )
-}
diff --git a/src/screens/Messages/Conversation/MessageListError.tsx b/src/screens/Messages/Conversation/MessageListError.tsx
deleted file mode 100644
index 6f50948df..000000000
--- a/src/screens/Messages/Conversation/MessageListError.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import React from 'react'
-import {View} from 'react-native'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {ConvoItem, ConvoItemError} from '#/state/messages/convo/types'
-import {atoms as a, useTheme} from '#/alf'
-import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
-import {InlineLinkText} from '#/components/Link'
-import {Text} from '#/components/Typography'
-
-export function MessageListError({item}: {item: ConvoItem & {type: 'error'}}) {
-  const t = useTheme()
-  const {_} = useLingui()
-  const {description, help, cta} = React.useMemo(() => {
-    return {
-      [ConvoItemError.FirehoseFailed]: {
-        description: _(msg`This chat was disconnected`),
-        help: _(msg`Press to attempt reconnection`),
-        cta: _(msg`Reconnect`),
-      },
-      [ConvoItemError.HistoryFailed]: {
-        description: _(msg`Failed to load past messages`),
-        help: _(msg`Press to retry`),
-        cta: _(msg`Retry`),
-      },
-    }[item.code]
-  }, [_, item.code])
-
-  return (
-    <View style={[a.py_md, a.w_full, a.flex_row, a.justify_center]}>
-      <View
-        style={[
-          a.flex_1,
-          a.flex_row,
-          a.align_center,
-          a.justify_center,
-          a.gap_sm,
-          {maxWidth: 400},
-        ]}>
-        <CircleInfo size="sm" fill={t.palette.negative_400} />
-
-        <Text style={[a.leading_snug, t.atoms.text_contrast_medium]}>
-          {description} &middot;{' '}
-          {item.retry && (
-            <InlineLinkText
-              to="#"
-              label={help}
-              onPress={e => {
-                e.preventDefault()
-                item.retry?.()
-                return false
-              }}>
-              {cta}
-            </InlineLinkText>
-          )}
-        </Text>
-      </View>
-    </View>
-  )
-}
diff --git a/src/screens/Messages/Conversation/MessagesList.tsx b/src/screens/Messages/Conversation/MessagesList.tsx
deleted file mode 100644
index 3034f0290..000000000
--- a/src/screens/Messages/Conversation/MessagesList.tsx
+++ /dev/null
@@ -1,454 +0,0 @@
-import React, {useCallback, useRef} from 'react'
-import {FlatList, LayoutChangeEvent, View} from 'react-native'
-import {
-  KeyboardStickyView,
-  useKeyboardHandler,
-} from 'react-native-keyboard-controller'
-import {
-  runOnJS,
-  scrollTo,
-  useAnimatedRef,
-  useAnimatedStyle,
-  useSharedValue,
-} from 'react-native-reanimated'
-import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/reanimated2/hook/commonTypes'
-import {useSafeAreaInsets} from 'react-native-safe-area-context'
-import {AppBskyEmbedRecord, AppBskyRichtextFacet, RichText} from '@atproto/api'
-
-import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
-import {
-  convertBskyAppUrlIfNeeded,
-  isBskyPostUrl,
-} from '#/lib/strings/url-helpers'
-import {logger} from '#/logger'
-import {isNative} from '#/platform/detection'
-import {isConvoActive, useConvoActive} from '#/state/messages/convo'
-import {ConvoItem, ConvoStatus} from '#/state/messages/convo/types'
-import {useGetPost} from '#/state/queries/post'
-import {useAgent} from '#/state/session'
-import {clamp} from 'lib/numbers'
-import {ScrollProvider} from 'lib/ScrollContext'
-import {isWeb} from 'platform/detection'
-import {
-  EmojiPicker,
-  EmojiPickerState,
-} from '#/view/com/composer/text-input/web/EmojiPicker.web'
-import {List} from 'view/com/util/List'
-import {ChatDisabled} from '#/screens/Messages/Conversation/ChatDisabled'
-import {MessageInput} from '#/screens/Messages/Conversation/MessageInput'
-import {MessageListError} from '#/screens/Messages/Conversation/MessageListError'
-import {ChatEmptyPill} from '#/components/dms/ChatEmptyPill'
-import {MessageItem} from '#/components/dms/MessageItem'
-import {NewMessagesPill} from '#/components/dms/NewMessagesPill'
-import {Loader} from '#/components/Loader'
-import {Text} from '#/components/Typography'
-import {MessageInputEmbed, useMessageEmbed} from './MessageInputEmbed'
-
-function MaybeLoader({isLoading}: {isLoading: boolean}) {
-  return (
-    <View
-      style={{
-        height: 50,
-        width: '100%',
-        alignItems: 'center',
-        justifyContent: 'center',
-      }}>
-      {isLoading && <Loader size="xl" />}
-    </View>
-  )
-}
-
-function renderItem({item}: {item: ConvoItem}) {
-  if (item.type === 'message' || item.type === 'pending-message') {
-    return <MessageItem item={item} />
-  } else if (item.type === 'deleted-message') {
-    return <Text>Deleted message</Text>
-  } else if (item.type === 'error') {
-    return <MessageListError item={item} />
-  }
-
-  return null
-}
-
-function keyExtractor(item: ConvoItem) {
-  return item.key
-}
-
-function onScrollToIndexFailed() {
-  // Placeholder function. You have to give FlatList something or else it will error.
-}
-
-export function MessagesList({
-  hasScrolled,
-  setHasScrolled,
-  blocked,
-  footer,
-}: {
-  hasScrolled: boolean
-  setHasScrolled: React.Dispatch<React.SetStateAction<boolean>>
-  blocked?: boolean
-  footer?: React.ReactNode
-}) {
-  const convoState = useConvoActive()
-  const agent = useAgent()
-  const getPost = useGetPost()
-  const {embedUri, setEmbed} = useMessageEmbed()
-
-  const flatListRef = useAnimatedRef<FlatList>()
-
-  const [newMessagesPill, setNewMessagesPill] = React.useState({
-    show: false,
-    startContentOffset: 0,
-  })
-
-  const [emojiPickerState, setEmojiPickerState] =
-    React.useState<EmojiPickerState>({
-      isOpen: false,
-      pos: {top: 0, left: 0, right: 0, bottom: 0},
-    })
-
-  // We need to keep track of when the scroll offset is at the bottom of the list to know when to scroll as new items
-  // are added to the list. For example, if the user is scrolled up to 1iew older messages, we don't want to scroll to
-  // the bottom.
-  const isAtBottom = useSharedValue(true)
-
-  // This will be used on web to assist in determining if we need to maintain the content offset
-  const isAtTop = useSharedValue(true)
-
-  // Used to keep track of the current content height. We'll need this in `onScroll` so we know when to start allowing
-  // onStartReached to fire.
-  const prevContentHeight = useRef(0)
-  const prevItemCount = useRef(0)
-
-  // -- Keep track of background state and positioning for new pill
-  const layoutHeight = useSharedValue(0)
-  const didBackground = React.useRef(false)
-  React.useEffect(() => {
-    if (convoState.status === ConvoStatus.Backgrounded) {
-      didBackground.current = true
-    }
-  }, [convoState.status])
-
-  // -- Scroll handling
-
-  // Every time the content size changes, that means one of two things is happening:
-  // 1. New messages are being added from the log or from a message you have sent
-  // 2. Old messages are being prepended to the top
-  //
-  // The first time that the content size changes is when the initial items are rendered. Because we cannot rely on
-  // `initialScrollIndex`, we need to immediately scroll to the bottom of the list. That scroll will not be animated.
-  //
-  // Subsequent resizes will only scroll to the bottom if the user is at the bottom of the list (within 100 pixels of
-  // the bottom). Therefore, any new messages that come in or are sent will result in an animated scroll to end. However
-  // we will not scroll whenever new items get prepended to the top.
-  const onContentSizeChange = useCallback(
-    (_: number, height: number) => {
-      // Because web does not have `maintainVisibleContentPosition` support, we will need to manually scroll to the
-      // previous off whenever we add new content to the previous offset whenever we add new content to the list.
-      if (isWeb && isAtTop.value && hasScrolled) {
-        flatListRef.current?.scrollToOffset({
-          offset: height - prevContentHeight.current,
-          animated: false,
-        })
-      }
-
-      // This number _must_ be the height of the MaybeLoader component
-      if (height > 50 && isAtBottom.value) {
-        // If the size of the content is changing by more than the height of the screen, then we don't
-        // want to scroll further than the start of all the new content. Since we are storing the previous offset,
-        // we can just scroll the user to that offset and add a little bit of padding. We'll also show the pill
-        // that can be pressed to immediately scroll to the end.
-        if (
-          didBackground.current &&
-          hasScrolled &&
-          height - prevContentHeight.current > layoutHeight.value - 50 &&
-          convoState.items.length - prevItemCount.current > 1
-        ) {
-          flatListRef.current?.scrollToOffset({
-            offset: prevContentHeight.current - 65,
-            animated: true,
-          })
-          setNewMessagesPill({
-            show: true,
-            startContentOffset: prevContentHeight.current - 65,
-          })
-        } else {
-          flatListRef.current?.scrollToOffset({
-            offset: height,
-            animated: hasScrolled && height > prevContentHeight.current,
-          })
-
-          // HACK Unfortunately, we need to call `setHasScrolled` after a brief delay,
-          // because otherwise there is too much of a delay between the time the content
-          // scrolls and the time the screen appears, causing a flicker.
-          // We cannot actually use a synchronous scroll here, because `onContentSizeChange`
-          // is actually async itself - all the info has to come across the bridge first.
-          if (!hasScrolled && !convoState.isFetchingHistory) {
-            setTimeout(() => {
-              setHasScrolled(true)
-            }, 100)
-          }
-        }
-      }
-
-      prevContentHeight.current = height
-      prevItemCount.current = convoState.items.length
-      didBackground.current = false
-    },
-    [
-      hasScrolled,
-      setHasScrolled,
-      convoState.isFetchingHistory,
-      convoState.items.length,
-      // these are stable
-      flatListRef,
-      isAtTop.value,
-      isAtBottom.value,
-      layoutHeight.value,
-    ],
-  )
-
-  const onStartReached = useCallback(() => {
-    if (hasScrolled && prevContentHeight.current > layoutHeight.value) {
-      convoState.fetchMessageHistory()
-    }
-  }, [convoState, hasScrolled, layoutHeight.value])
-
-  const onScroll = React.useCallback(
-    (e: ReanimatedScrollEvent) => {
-      'worklet'
-      layoutHeight.value = e.layoutMeasurement.height
-      const bottomOffset = e.contentOffset.y + e.layoutMeasurement.height
-
-      // Most apps have a little bit of space the user can scroll past while still automatically scrolling ot the bottom
-      // when a new message is added, hence the 100 pixel offset
-      isAtBottom.value = e.contentSize.height - 100 < bottomOffset
-      isAtTop.value = e.contentOffset.y <= 1
-
-      if (
-        newMessagesPill.show &&
-        (e.contentOffset.y > newMessagesPill.startContentOffset + 200 ||
-          isAtBottom.value)
-      ) {
-        runOnJS(setNewMessagesPill)({
-          show: false,
-          startContentOffset: 0,
-        })
-      }
-    },
-    [layoutHeight, newMessagesPill, isAtBottom, isAtTop],
-  )
-
-  // -- Keyboard animation handling
-  const {bottom: bottomInset} = useSafeAreaInsets()
-  const bottomOffset = isWeb ? 0 : clamp(60 + bottomInset, 60, 75)
-
-  const keyboardHeight = useSharedValue(0)
-  const keyboardIsOpening = useSharedValue(false)
-
-  // In some cases - like when the emoji piker opens - we don't want to animate the scroll in the list onLayout event.
-  // We use this value to keep track of when we want to disable the animation.
-  const layoutScrollWithoutAnimation = useSharedValue(false)
-
-  useKeyboardHandler({
-    onStart: e => {
-      'worklet'
-      // Immediate updates - like opening the emoji picker - will have a duration of zero. In those cases, we should
-      // just update the height here instead of having the `onMove` event do it (that event will not fire!)
-      if (e.duration === 0) {
-        layoutScrollWithoutAnimation.value = true
-        keyboardHeight.value = e.height
-      } else {
-        keyboardIsOpening.value = true
-      }
-    },
-    onMove: e => {
-      'worklet'
-      keyboardHeight.value = e.height
-      if (e.height > bottomOffset) {
-        scrollTo(flatListRef, 0, 1e7, false)
-      }
-    },
-    onEnd: () => {
-      'worklet'
-      keyboardIsOpening.value = false
-    },
-  })
-
-  const animatedListStyle = useAnimatedStyle(() => ({
-    marginBottom:
-      keyboardHeight.value > bottomOffset ? keyboardHeight.value : bottomOffset,
-  }))
-
-  // -- Message sending
-  const onSendMessage = useCallback(
-    async (text: string) => {
-      let rt = new RichText({text: text.trimEnd()}, {cleanNewlines: true})
-
-      // detect facets without resolution first - this is used to see if there's
-      // any post links in the text that we can embed. We do this first because
-      // we want to remove the post link from the text, re-trim, then detect facets
-      rt.detectFacetsWithoutResolution()
-
-      let embed: AppBskyEmbedRecord.Main | undefined
-
-      if (embedUri) {
-        try {
-          const post = await getPost({uri: embedUri})
-          if (post) {
-            embed = {
-              $type: 'app.bsky.embed.record',
-              record: {
-                uri: post.uri,
-                cid: post.cid,
-              },
-            }
-
-            // look for the embed uri in the facets, so we can remove it from the text
-            const postLinkFacet = rt.facets?.find(facet => {
-              return facet.features.find(feature => {
-                if (AppBskyRichtextFacet.isLink(feature)) {
-                  if (isBskyPostUrl(feature.uri)) {
-                    const url = convertBskyAppUrlIfNeeded(feature.uri)
-                    const [_0, _1, _2, rkey] = url.split('/').filter(Boolean)
-
-                    // this might have a handle instead of a DID
-                    // so just compare the rkey - not particularly dangerous
-                    return post.uri.endsWith(rkey)
-                  }
-                }
-                return false
-              })
-            })
-
-            if (postLinkFacet) {
-              const isAtStart = postLinkFacet.index.byteStart === 0
-              const isAtEnd =
-                postLinkFacet.index.byteEnd === rt.unicodeText.graphemeLength
-
-              // remove the post link from the text
-              if (isAtStart || isAtEnd) {
-                rt.delete(
-                  postLinkFacet.index.byteStart,
-                  postLinkFacet.index.byteEnd,
-                )
-              }
-
-              rt = new RichText({text: rt.text.trim()}, {cleanNewlines: true})
-            }
-          }
-        } catch (error) {
-          logger.error('Failed to get post as quote for DM', {error})
-        }
-      }
-
-      await rt.detectFacets(agent)
-
-      rt = shortenLinks(rt)
-      rt = stripInvalidMentions(rt)
-
-      if (!hasScrolled) {
-        setHasScrolled(true)
-      }
-
-      convoState.sendMessage({
-        text: rt.text,
-        facets: rt.facets,
-        embed,
-      })
-    },
-    [agent, convoState, embedUri, getPost, hasScrolled, setHasScrolled],
-  )
-
-  // -- List layout changes (opening emoji keyboard, etc.)
-  const onListLayout = React.useCallback(
-    (e: LayoutChangeEvent) => {
-      layoutHeight.value = e.nativeEvent.layout.height
-
-      if (isWeb || !keyboardIsOpening.value) {
-        flatListRef.current?.scrollToEnd({
-          animated: !layoutScrollWithoutAnimation.value,
-        })
-        layoutScrollWithoutAnimation.value = false
-      }
-    },
-    [
-      flatListRef,
-      keyboardIsOpening.value,
-      layoutScrollWithoutAnimation,
-      layoutHeight,
-    ],
-  )
-
-  const scrollToEndOnPress = React.useCallback(() => {
-    flatListRef.current?.scrollToOffset({
-      offset: prevContentHeight.current,
-      animated: true,
-    })
-  }, [flatListRef])
-
-  return (
-    <>
-      {/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */}
-      <ScrollProvider onScroll={onScroll}>
-        <List
-          ref={flatListRef}
-          data={convoState.items}
-          renderItem={renderItem}
-          keyExtractor={keyExtractor}
-          disableFullWindowScroll={true}
-          disableVirtualization={true}
-          style={animatedListStyle}
-          // The extra two items account for the header and the footer components
-          initialNumToRender={isNative ? 32 : 62}
-          maxToRenderPerBatch={isWeb ? 32 : 62}
-          keyboardDismissMode="on-drag"
-          keyboardShouldPersistTaps="handled"
-          maintainVisibleContentPosition={{
-            minIndexForVisible: 0,
-          }}
-          removeClippedSubviews={false}
-          sideBorders={false}
-          onContentSizeChange={onContentSizeChange}
-          onLayout={onListLayout}
-          onStartReached={onStartReached}
-          onScrollToIndexFailed={onScrollToIndexFailed}
-          scrollEventThrottle={100}
-          ListHeaderComponent={
-            <MaybeLoader isLoading={convoState.isFetchingHistory} />
-          }
-        />
-      </ScrollProvider>
-      <KeyboardStickyView offset={{closed: -bottomOffset, opened: 0}}>
-        {convoState.status === ConvoStatus.Disabled ? (
-          <ChatDisabled />
-        ) : blocked ? (
-          footer
-        ) : (
-          <>
-            {isConvoActive(convoState) &&
-              !convoState.isFetchingHistory &&
-              convoState.items.length === 0 && <ChatEmptyPill />}
-            <MessageInput
-              onSendMessage={onSendMessage}
-              hasEmbed={!!embedUri}
-              setEmbed={setEmbed}
-              openEmojiPicker={pos => setEmojiPickerState({isOpen: true, pos})}>
-              <MessageInputEmbed embedUri={embedUri} setEmbed={setEmbed} />
-            </MessageInput>
-          </>
-        )}
-      </KeyboardStickyView>
-
-      {isWeb && (
-        <EmojiPicker
-          pinToTop
-          state={emojiPickerState}
-          close={() => setEmojiPickerState(prev => ({...prev, isOpen: false}))}
-        />
-      )}
-
-      {newMessagesPill.show && <NewMessagesPill onPress={scrollToEndOnPress} />}
-    </>
-  )
-}
diff --git a/src/screens/Messages/Conversation/index.tsx b/src/screens/Messages/Conversation/index.tsx
deleted file mode 100644
index d14ed160a..000000000
--- a/src/screens/Messages/Conversation/index.tsx
+++ /dev/null
@@ -1,205 +0,0 @@
-import React, {useCallback} from 'react'
-import {View} from 'react-native'
-import {useKeyboardController} from 'react-native-keyboard-controller'
-import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useFocusEffect} from '@react-navigation/native'
-import {NativeStackScreenProps} from '@react-navigation/native-stack'
-
-import {CommonNavigatorParams} from '#/lib/routes/types'
-import {useCurrentConvoId} from '#/state/messages/current-convo-id'
-import {useModerationOpts} from '#/state/preferences/moderation-opts'
-import {useProfileQuery} from '#/state/queries/profile'
-import {isWeb} from 'platform/detection'
-import {useProfileShadow} from 'state/cache/profile-shadow'
-import {ConvoProvider, isConvoActive, useConvo} from 'state/messages/convo'
-import {ConvoStatus} from 'state/messages/convo/types'
-import {useSetMinimalShellMode} from 'state/shell'
-import {CenteredView} from 'view/com/util/Views'
-import {MessagesList} from '#/screens/Messages/Conversation/MessagesList'
-import {atoms as a, useBreakpoints, useTheme} from '#/alf'
-import {MessagesListBlockedFooter} from '#/components/dms/MessagesListBlockedFooter'
-import {MessagesListHeader} from '#/components/dms/MessagesListHeader'
-import {Error} from '#/components/Error'
-import {Loader} from '#/components/Loader'
-
-type Props = NativeStackScreenProps<
-  CommonNavigatorParams,
-  'MessagesConversation'
->
-export function MessagesConversationScreen({route}: Props) {
-  const {gtMobile} = useBreakpoints()
-  const setMinimalShellMode = useSetMinimalShellMode()
-
-  const convoId = route.params.conversation
-  const {setCurrentConvoId} = useCurrentConvoId()
-
-  const {setEnabled} = useKeyboardController()
-  useFocusEffect(
-    useCallback(() => {
-      if (isWeb) return
-      setEnabled(true)
-      return () => {
-        setEnabled(false)
-      }
-    }, [setEnabled]),
-  )
-
-  useFocusEffect(
-    useCallback(() => {
-      setCurrentConvoId(convoId)
-
-      if (isWeb && !gtMobile) {
-        setMinimalShellMode(true)
-      } else {
-        setMinimalShellMode(false)
-      }
-
-      return () => {
-        setCurrentConvoId(undefined)
-        setMinimalShellMode(false)
-      }
-    }, [gtMobile, convoId, setCurrentConvoId, setMinimalShellMode]),
-  )
-
-  return (
-    <ConvoProvider key={convoId} convoId={convoId}>
-      <Inner />
-    </ConvoProvider>
-  )
-}
-
-function Inner() {
-  const t = useTheme()
-  const convoState = useConvo()
-  const {_} = useLingui()
-
-  const moderationOpts = useModerationOpts()
-  const {data: recipient} = useProfileQuery({
-    did: convoState.recipients?.[0].did,
-  })
-
-  // Because we want to give the list a chance to asynchronously scroll to the end before it is visible to the user,
-  // we use `hasScrolled` to determine when to render. With that said however, there is a chance that the chat will be
-  // empty. So, we also check for that possible state as well and render once we can.
-  const [hasScrolled, setHasScrolled] = React.useState(false)
-  const readyToShow =
-    hasScrolled ||
-    (isConvoActive(convoState) &&
-      !convoState.isFetchingHistory &&
-      convoState.items.length === 0)
-
-  // Any time that we re-render the `Initializing` state, we have to reset `hasScrolled` to false. After entering this
-  // state, we know that we're resetting the list of messages and need to re-scroll to the bottom when they get added.
-  React.useEffect(() => {
-    if (convoState.status === ConvoStatus.Initializing) {
-      setHasScrolled(false)
-    }
-  }, [convoState.status])
-
-  if (convoState.status === ConvoStatus.Error) {
-    return (
-      <CenteredView style={a.flex_1} sideBorders>
-        <MessagesListHeader />
-        <Error
-          title={_(msg`Something went wrong`)}
-          message={_(msg`We couldn't load this conversation`)}
-          onRetry={() => convoState.error.retry()}
-          sideBorders={false}
-        />
-      </CenteredView>
-    )
-  }
-
-  return (
-    <CenteredView style={[a.flex_1]} sideBorders>
-      {!readyToShow && <MessagesListHeader />}
-      <View style={[a.flex_1]}>
-        {moderationOpts && recipient ? (
-          <InnerReady
-            moderationOpts={moderationOpts}
-            recipient={recipient}
-            hasScrolled={hasScrolled}
-            setHasScrolled={setHasScrolled}
-          />
-        ) : (
-          <>
-            <View style={[a.align_center, a.gap_sm, a.flex_1]} />
-          </>
-        )}
-        {!readyToShow && (
-          <View
-            style={[
-              a.absolute,
-              a.z_10,
-              a.w_full,
-              a.h_full,
-              a.justify_center,
-              a.align_center,
-              t.atoms.bg,
-            ]}>
-            <View style={[{marginBottom: 75}]}>
-              <Loader size="xl" />
-            </View>
-          </View>
-        )}
-      </View>
-    </CenteredView>
-  )
-}
-
-function InnerReady({
-  moderationOpts,
-  recipient: recipientUnshadowed,
-  hasScrolled,
-  setHasScrolled,
-}: {
-  moderationOpts: ModerationOpts
-  recipient: AppBskyActorDefs.ProfileViewBasic
-  hasScrolled: boolean
-  setHasScrolled: React.Dispatch<React.SetStateAction<boolean>>
-}) {
-  const convoState = useConvo()
-  const recipient = useProfileShadow(recipientUnshadowed)
-
-  const moderation = React.useMemo(() => {
-    return moderateProfile(recipient, moderationOpts)
-  }, [recipient, moderationOpts])
-
-  const blockInfo = React.useMemo(() => {
-    const modui = moderation.ui('profileView')
-    const blocks = modui.alerts.filter(alert => alert.type === 'blocking')
-    const listBlocks = blocks.filter(alert => alert.source.type === 'list')
-    const userBlock = blocks.find(alert => alert.source.type === 'user')
-    return {
-      listBlocks,
-      userBlock,
-    }
-  }, [moderation])
-
-  return (
-    <>
-      <MessagesListHeader
-        profile={recipient}
-        moderation={moderation}
-        blockInfo={blockInfo}
-      />
-      {isConvoActive(convoState) && (
-        <MessagesList
-          hasScrolled={hasScrolled}
-          setHasScrolled={setHasScrolled}
-          blocked={moderation?.blocked}
-          footer={
-            <MessagesListBlockedFooter
-              recipient={recipient}
-              convoId={convoState.convo.id}
-              hasMessages={convoState.items.length > 0}
-              blockInfo={blockInfo}
-            />
-          }
-        />
-      )}
-    </>
-  )
-}