about summary refs log tree commit diff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/Dialog/index.web.tsx2
-rw-r--r--src/components/Layout.tsx100
-rw-r--r--src/components/Layout/Header/index.tsx199
-rw-r--r--src/components/Layout/README.md172
-rw-r--r--src/components/Layout/const.ts16
-rw-r--r--src/components/Layout/context.ts5
-rw-r--r--src/components/Layout/index.tsx188
-rw-r--r--src/components/LikedByList.tsx13
-rw-r--r--src/components/Lists.tsx34
-rw-r--r--src/components/dms/MessagesListHeader.tsx36
-rw-r--r--src/components/forms/DateField/index.android.tsx1
-rw-r--r--src/components/icons/FloppyDisk.tsx5
12 files changed, 617 insertions, 154 deletions
diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx
index 6b92eee3e..e45133dc5 100644
--- a/src/components/Dialog/index.web.tsx
+++ b/src/components/Dialog/index.web.tsx
@@ -12,6 +12,7 @@ import {useLingui} from '@lingui/react'
 import {DismissableLayer} from '@radix-ui/react-dismissable-layer'
 import {useFocusGuards} from '@radix-ui/react-focus-guards'
 import {FocusScope} from '@radix-ui/react-focus-scope'
+import {RemoveScrollBar} from 'react-remove-scroll-bar'
 
 import {logger} from '#/logger'
 import {useDialogStateControlContext} from '#/state/dialogs'
@@ -103,6 +104,7 @@ export function Outer({
       {isOpen && (
         <Portal>
           <Context.Provider value={context}>
+            <RemoveScrollBar />
             <TouchableWithoutFeedback
               accessibilityHint={undefined}
               accessibilityLabel={_(msg`Close active dialog`)}
diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx
deleted file mode 100644
index ea11e2217..000000000
--- a/src/components/Layout.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-import React, {useContext, useMemo} from 'react'
-import {View, ViewStyle} from 'react-native'
-import {StyleProp} from 'react-native'
-import {useSafeAreaInsets} from 'react-native-safe-area-context'
-
-import {ViewHeader} from '#/view/com/util/ViewHeader'
-import {ScrollView} from '#/view/com/util/Views'
-import {CenteredView} from '#/view/com/util/Views'
-import {atoms as a} from '#/alf'
-
-// Every screen should have a Layout component wrapping it.
-// This component provides a default padding for the top of the screen.
-// This allows certain screens to avoid the top padding if they want to.
-
-const LayoutContext = React.createContext({
-  withinScreen: false,
-  topPaddingDisabled: false,
-  withinScrollView: false,
-})
-
-/**
- * Every screen should have a Layout.Screen component wrapping it.
- * This component provides a default padding for the top of the screen
- * and height/minHeight
- */
-let Screen = ({
-  disableTopPadding = false,
-  style,
-  ...props
-}: React.ComponentProps<typeof View> & {
-  disableTopPadding?: boolean
-  style?: StyleProp<ViewStyle>
-}): React.ReactNode => {
-  const {top} = useSafeAreaInsets()
-  const context = useMemo(
-    () => ({
-      withinScreen: true,
-      topPaddingDisabled: disableTopPadding,
-      withinScrollView: false,
-    }),
-    [disableTopPadding],
-  )
-  return (
-    <LayoutContext.Provider value={context}>
-      <View
-        style={[
-          {paddingTop: disableTopPadding ? 0 : top},
-          a.util_screen_outer,
-          style,
-        ]}
-        {...props}
-      />
-    </LayoutContext.Provider>
-  )
-}
-Screen = React.memo(Screen)
-export {Screen}
-
-let Header = (
-  props: React.ComponentProps<typeof ViewHeader>,
-): React.ReactNode => {
-  const {withinScrollView} = useContext(LayoutContext)
-  if (!withinScrollView) {
-    return (
-      <CenteredView topBorder={false} sideBorders>
-        <ViewHeader showOnDesktop showBorder {...props} />
-      </CenteredView>
-    )
-  } else {
-    return <ViewHeader showOnDesktop showBorder {...props} />
-  }
-}
-Header = React.memo(Header)
-export {Header}
-
-let Content = ({
-  style,
-  contentContainerStyle,
-  ...props
-}: React.ComponentProps<typeof ScrollView> & {
-  style?: StyleProp<ViewStyle>
-  contentContainerStyle?: StyleProp<ViewStyle>
-}): React.ReactNode => {
-  const context = useContext(LayoutContext)
-  const newContext = useMemo(
-    () => ({...context, withinScrollView: true}),
-    [context],
-  )
-  return (
-    <LayoutContext.Provider value={newContext}>
-      <ScrollView
-        style={[a.flex_1, style]}
-        contentContainerStyle={[{paddingBottom: 100}, contentContainerStyle]}
-        {...props}
-      />
-    </LayoutContext.Provider>
-  )
-}
-Content = React.memo(Content)
-export {Content}
diff --git a/src/components/Layout/Header/index.tsx b/src/components/Layout/Header/index.tsx
new file mode 100644
index 000000000..a35a09537
--- /dev/null
+++ b/src/components/Layout/Header/index.tsx
@@ -0,0 +1,199 @@
+import {createContext, useCallback, useContext} from 'react'
+import {GestureResponderEvent, View} from 'react-native'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useNavigation} from '@react-navigation/native'
+
+import {HITSLOP_30} from '#/lib/constants'
+import {NavigationProp} from '#/lib/routes/types'
+import {isIOS} from '#/platform/detection'
+import {useSetDrawerOpen} from '#/state/shell'
+import {
+  atoms as a,
+  platform,
+  TextStyleProp,
+  useBreakpoints,
+  useGutterStyles,
+  useTheme,
+} from '#/alf'
+import {Button, ButtonIcon, ButtonProps} from '#/components/Button'
+import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons/Arrow'
+import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
+import {
+  BUTTON_VISUAL_ALIGNMENT_OFFSET,
+  HEADER_SLOT_SIZE,
+} from '#/components/Layout/const'
+import {ScrollbarOffsetContext} from '#/components/Layout/context'
+import {Text} from '#/components/Typography'
+
+export function Outer({
+  children,
+  noBottomBorder,
+}: {
+  children: React.ReactNode
+  noBottomBorder?: boolean
+}) {
+  const t = useTheme()
+  const gutter = useGutterStyles()
+  const {gtMobile} = useBreakpoints()
+  const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
+
+  return (
+    <View
+      style={[
+        a.w_full,
+        !noBottomBorder && a.border_b,
+        a.flex_row,
+        a.align_center,
+        a.gap_sm,
+        gutter,
+        platform({
+          native: [a.pb_sm, a.pt_xs],
+          web: [a.py_sm],
+        }),
+        t.atoms.border_contrast_low,
+        gtMobile && [a.mx_auto, {maxWidth: 600}],
+        !isWithinOffsetView && a.scrollbar_offset,
+      ]}>
+      {children}
+    </View>
+  )
+}
+
+const AlignmentContext = createContext<'platform' | 'left'>('platform')
+
+export function Content({
+  children,
+  align = 'platform',
+}: {
+  children?: React.ReactNode
+  align?: 'platform' | 'left'
+}) {
+  return (
+    <View
+      style={[
+        a.flex_1,
+        a.justify_center,
+        isIOS && align === 'platform' && a.align_center,
+        {minHeight: HEADER_SLOT_SIZE},
+      ]}>
+      <AlignmentContext.Provider value={align}>
+        {children}
+      </AlignmentContext.Provider>
+    </View>
+  )
+}
+
+export function Slot({children}: {children?: React.ReactNode}) {
+  return (
+    <View
+      style={[
+        a.z_50,
+        {
+          width: HEADER_SLOT_SIZE,
+        },
+      ]}>
+      {children}
+    </View>
+  )
+}
+
+export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) {
+  const {_} = useLingui()
+  const navigation = useNavigation<NavigationProp>()
+
+  const onPressBack = useCallback(
+    (evt: GestureResponderEvent) => {
+      onPress?.(evt)
+      if (evt.defaultPrevented) return
+      if (navigation.canGoBack()) {
+        navigation.goBack()
+      } else {
+        navigation.navigate('Home')
+      }
+    },
+    [onPress, navigation],
+  )
+
+  return (
+    <Slot>
+      <Button
+        label={_(msg`Go back`)}
+        size="small"
+        variant="ghost"
+        color="secondary"
+        shape="square"
+        onPress={onPressBack}
+        hitSlop={HITSLOP_30}
+        style={[{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET}, style]}
+        {...props}>
+        <ButtonIcon icon={ArrowLeft} size="lg" />
+      </Button>
+    </Slot>
+  )
+}
+
+export function MenuButton() {
+  const {_} = useLingui()
+  const setDrawerOpen = useSetDrawerOpen()
+  const {gtMobile} = useBreakpoints()
+
+  const onPress = useCallback(() => {
+    setDrawerOpen(true)
+  }, [setDrawerOpen])
+
+  return gtMobile ? null : (
+    <Slot>
+      <Button
+        label={_(msg`Open drawer menu`)}
+        size="small"
+        variant="ghost"
+        color="secondary"
+        shape="square"
+        onPress={onPress}
+        hitSlop={HITSLOP_30}
+        style={[{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET}]}>
+        <ButtonIcon icon={Menu} size="lg" />
+      </Button>
+    </Slot>
+  )
+}
+
+export function TitleText({
+  children,
+  style,
+}: {children: React.ReactNode} & TextStyleProp) {
+  const {gtMobile} = useBreakpoints()
+  const align = useContext(AlignmentContext)
+  return (
+    <Text
+      style={[
+        a.text_lg,
+        a.font_heavy,
+        a.leading_tight,
+        isIOS && align === 'platform' && a.text_center,
+        gtMobile && a.text_xl,
+        style,
+      ]}
+      numberOfLines={2}>
+      {children}
+    </Text>
+  )
+}
+
+export function SubtitleText({children}: {children: React.ReactNode}) {
+  const t = useTheme()
+  const align = useContext(AlignmentContext)
+  return (
+    <Text
+      style={[
+        a.text_sm,
+        a.leading_snug,
+        isIOS && align === 'platform' && a.text_center,
+        t.atoms.text_contrast_medium,
+      ]}
+      numberOfLines={2}>
+      {children}
+    </Text>
+  )
+}
diff --git a/src/components/Layout/README.md b/src/components/Layout/README.md
new file mode 100644
index 000000000..1bcc3489e
--- /dev/null
+++ b/src/components/Layout/README.md
@@ -0,0 +1,172 @@
+# Layout
+
+This directory contains our core layout components. Use these when creating new
+screens, or when supplementing other components with functionality like
+centering.
+
+## Usage
+
+If we aren't talking about the `shell` components, layouts on individual screens
+look like more or less like this:
+
+```tsx
+<Outer>
+  <Header>...</Header>
+  <Content>...</Content>
+</Outer>
+```
+
+I'll map these words to real components.
+
+### `Layout.Screen`
+
+Provides the "Outer" functionality for a screen, like taking up the full height
+of the screen. **All screens should be wrapped with this component,** probably
+as the outermost component.
+
+> [!NOTE]
+> On web, `Layout.Screen` also provides the side borders on our central content
+> column. These borders are fixed position, 1px outside our center column width
+> of 600px.
+>
+> What this effectively means is that _nothing inside the center content column
+> needs (or should) define left/right borders._ That is now handled in one
+> place: within `Layout.Screen`.
+
+### `Layout.Header.*`
+
+The `Layout.Header` component actually contains multiple sub-components. Use
+this to compose different versions of the header. The most basic version looks
+like this:
+
+```tsx
+<Layout.Header.Outer>
+  <Layout.Header.BackButton /> {/* or <Layout.Header.MenuButton /> */}
+
+  <Layout.Header.Content>
+    <Layout.Header.TitleText>Account</Layout.Header.TitleText>
+
+    {/* Optional subtitle */}
+    <Layout.Header.SubtitleText>Settings for @esb.lol</Layout.Header.SubtitleText>
+  </Layout.Header.Content>
+
+  <Layout.Header.Slot />
+</Layout.Header.Outer>
+```
+
+Note the additional `Slot` component. This is here to keep the header balanced
+and provide correct spacing on all platforms. The `Slot` is 34px wide, which
+matches the `BackButton` and `MenuButton`.
+
+> If anyone has better ideas, I'm all ears, but this was simple and the small
+> amount of boilerplate is only incurred when creating a new screen, which is
+> infrequent.
+
+It can also function as a "slot" for a button positioned on the right side. See
+the `Hashtag` screen for an example, abbreviated below:
+
+```tsx
+<Layout.Header.Slot>
+  <Button size='small' shape='round'>...</Button>
+</Layout.Header.Slot>
+```
+
+If you need additional customization, simply use the components that are helpful
+and create new ones as needed. A good example is the `SavedFeeds` screen, which
+looks roughly like this:
+
+```tsx
+<Layout.Header.Outer>
+  <Layout.Header.BackButton />
+
+  {/* Override to align content to the left, making room for the button */}
+  <Layout.Header.Content align='left'>
+    <Layout.Header.TitleText>Edit My Feeds</Layout.Header.TitleText>
+  </Layout.Header.Content>
+
+  {/* Custom button, wider than 34px */}
+  <Button size='small'>...</Button>
+</Layout.Header.Outer>
+```
+
+> [!TIP]
+> The `Header` should be _outside_ the `Content` component in order to be
+> fixed on scroll on native. Placing it inside will make it scroll with the rest
+> of the page.
+
+### `Layout.Content`
+
+This provides the "Content" functionality for a screen. This component is
+actually an `Animated.ScrollView`, and accepts props for that component. It
+provides a little default styling as well. On web, it also _centers the content
+inside our center content column of 600px_.
+
+> [!NOTE]
+> What about flatlists or pagers? Those components are not colocated here (yet).
+> But those components serve the same purpose of "Content".
+
+## Examples
+
+The most basic layout available to us looks like this:
+
+```tsx
+<Layout.Screen>
+  <Layout.Header.Outer>
+    <Layout.Header.BackButton /> {/* or <Layout.Header.MenuButton /> */}
+
+    <Layout.Header.Content>
+      <Layout.Header.TitleText>Account</Layout.Header.TitleText>
+
+      {/* Optional subtitle */}
+      <Layout.Header.SubtitleText>Settings for @esb.lol</Layout.Header.SubtitleText>
+    </Layout.Header.Content>
+
+    <Layout.Header.Slot />
+  </Layout.Header.Outer>
+
+  <Layout.Content>
+    ...
+  </Layout.Content>
+</Layout.Screen>
+```
+
+**For `List` views,** you'd sub in `List` for `Layout.Content` and it will
+function the same. See `Feeds` screen for an example.
+
+**For `Pager` views,** including `PagerWithHeader`, do the same. See `Hashtag`
+screen for an example.
+
+## Utilities
+
+### `Layout.Center`
+
+This component behaves like our old `CenteredView` component.
+
+### `Layout.SCROLLBAR_OFFSET` and `Layout.SCROLLBAR_OFFSET_POSITIVE`
+
+Provide a pre-configured CSS vars for use when aligning fixed position elements.
+More on this below.
+
+## Scrollbar gutter handling
+
+Operating systems allow users to configure if their browser _always_ shows
+scrollbars not. Some OSs also don't allow configuration.
+
+The presence of scrollbars affects layout, particularly fixed position elements.
+Browsers support `scrollbar-gutter`, but each behaves differently. Our approach
+is to use the default `scrollbar-gutter: auto`. Basically, we start from a clean
+slate.
+
+This handling becomes particularly thorny when we need to lock scroll, like when
+opening a dialog or dropdown. Radix uses the library `react-remove-scroll`
+internally, which in turn depends on
+[`react-remove-scroll-bar`](https://github.com/theKashey/react-remove-scroll-bar).
+We've opted to rely on this transient dependency. This library adds some utility
+classes and CSS vars to the page when scroll is locked.
+
+**It is this CSS variable that we use in `SCROLLBAR_OFFSET` values.** This
+ensures that elements do not shift relative to the screen when opening a
+dropdown or dialog.
+
+These styles are applied where needed and we should have very little need of
+adjusting them often.
diff --git a/src/components/Layout/const.ts b/src/components/Layout/const.ts
new file mode 100644
index 000000000..11825d323
--- /dev/null
+++ b/src/components/Layout/const.ts
@@ -0,0 +1,16 @@
+export const SCROLLBAR_OFFSET =
+  'calc(-1 * var(--removed-body-scroll-bar-size, 0px) / 2)' as any
+export const SCROLLBAR_OFFSET_POSITIVE =
+  'calc(var(--removed-body-scroll-bar-size, 0px) / 2)' as any
+
+/**
+ * Useful for visually aligning icons within header buttons with the elements
+ * below them on the screen. Apply positively or negatively depending on side
+ * of the screen you're on.
+ */
+export const BUTTON_VISUAL_ALIGNMENT_OFFSET = 3
+
+/**
+ * Corresponds to the width of a small square or round button
+ */
+export const HEADER_SLOT_SIZE = 34
diff --git a/src/components/Layout/context.ts b/src/components/Layout/context.ts
new file mode 100644
index 000000000..8e0c5445e
--- /dev/null
+++ b/src/components/Layout/context.ts
@@ -0,0 +1,5 @@
+import React from 'react'
+
+export const ScrollbarOffsetContext = React.createContext({
+  isWithinOffsetView: false,
+})
diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx
new file mode 100644
index 000000000..d08505fbf
--- /dev/null
+++ b/src/components/Layout/index.tsx
@@ -0,0 +1,188 @@
+import React, {useContext, useMemo} from 'react'
+import {StyleSheet, View, ViewProps, ViewStyle} from 'react-native'
+import {StyleProp} from 'react-native'
+import {
+  KeyboardAwareScrollView,
+  KeyboardAwareScrollViewProps,
+} from 'react-native-keyboard-controller'
+import Animated, {
+  AnimatedScrollViewProps,
+  useAnimatedProps,
+} from 'react-native-reanimated'
+import {useSafeAreaInsets} from 'react-native-safe-area-context'
+
+import {isWeb} from '#/platform/detection'
+import {useShellLayout} from '#/state/shell/shell-layout'
+import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
+import {ScrollbarOffsetContext} from '#/components/Layout/context'
+
+export * from '#/components/Layout/const'
+export * as Header from '#/components/Layout/Header'
+
+export type ScreenProps = React.ComponentProps<typeof View> & {
+  style?: StyleProp<ViewStyle>
+}
+
+/**
+ * Outermost component of every screen
+ */
+export const Screen = React.memo(function Screen({
+  style,
+  ...props
+}: ScreenProps) {
+  const {top} = useSafeAreaInsets()
+  return (
+    <>
+      {isWeb && <WebCenterBorders />}
+      <View
+        style={[a.util_screen_outer, {paddingTop: top}, style]}
+        {...props}
+      />
+    </>
+  )
+})
+
+export type ContentProps = AnimatedScrollViewProps & {
+  style?: StyleProp<ViewStyle>
+  contentContainerStyle?: StyleProp<ViewStyle>
+}
+
+/**
+ * Default scroll view for simple pages
+ */
+export const Content = React.memo(function Content({
+  children,
+  style,
+  contentContainerStyle,
+  ...props
+}: ContentProps) {
+  const {footerHeight} = useShellLayout()
+  const animatedProps = useAnimatedProps(() => {
+    return {
+      scrollIndicatorInsets: {
+        bottom: footerHeight.get(),
+        top: 0,
+        right: 1,
+      },
+    } satisfies AnimatedScrollViewProps
+  })
+
+  return (
+    <Animated.ScrollView
+      id="content"
+      automaticallyAdjustsScrollIndicatorInsets={false}
+      // sets the scroll inset to the height of the footer
+      animatedProps={animatedProps}
+      style={[scrollViewStyles.common, style]}
+      contentContainerStyle={[
+        scrollViewStyles.contentContainer,
+        contentContainerStyle,
+      ]}
+      {...props}>
+      {isWeb ? (
+        // @ts-ignore web only -esb
+        <Center>{children}</Center>
+      ) : (
+        children
+      )}
+    </Animated.ScrollView>
+  )
+})
+
+const scrollViewStyles = StyleSheet.create({
+  common: {
+    width: '100%',
+  },
+  contentContainer: {
+    paddingBottom: 100,
+  },
+})
+
+export type KeyboardAwareContentProps = KeyboardAwareScrollViewProps & {
+  children: React.ReactNode
+  contentContainerStyle?: StyleProp<ViewStyle>
+}
+
+/**
+ * Default scroll view for simple pages.
+ *
+ * BE SURE TO TEST THIS WHEN USING, it's untested as of writing this comment.
+ */
+export const KeyboardAwareContent = React.memo(function LayoutScrollView({
+  children,
+  style,
+  contentContainerStyle,
+  ...props
+}: KeyboardAwareContentProps) {
+  return (
+    <KeyboardAwareScrollView
+      style={[scrollViewStyles.common, style]}
+      contentContainerStyle={[
+        scrollViewStyles.contentContainer,
+        contentContainerStyle,
+      ]}
+      keyboardShouldPersistTaps="handled"
+      {...props}>
+      {isWeb ? <Center>{children}</Center> : children}
+    </KeyboardAwareScrollView>
+  )
+})
+
+/**
+ * Utility component to center content within the screen
+ */
+export const Center = React.memo(function LayoutContent({
+  children,
+  style,
+  ...props
+}: ViewProps) {
+  const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
+  const {gtMobile} = useBreakpoints()
+  const ctx = useMemo(() => ({isWithinOffsetView: true}), [])
+  return (
+    <View
+      style={[
+        a.w_full,
+        a.mx_auto,
+        gtMobile && {
+          maxWidth: 600,
+        },
+        style,
+        !isWithinOffsetView && a.scrollbar_offset,
+      ]}
+      {...props}>
+      <ScrollbarOffsetContext.Provider value={ctx}>
+        {children}
+      </ScrollbarOffsetContext.Provider>
+    </View>
+  )
+})
+
+/**
+ * Only used within `Layout.Screen`, not for reuse
+ */
+const WebCenterBorders = React.memo(function LayoutContent() {
+  const t = useTheme()
+  const {gtMobile} = useBreakpoints()
+  return gtMobile ? (
+    <View
+      style={[
+        a.fixed,
+        a.inset_0,
+        a.border_l,
+        a.border_r,
+        t.atoms.border_contrast_low,
+        web({
+          width: 602,
+          left: '50%',
+          transform: [
+            {
+              translateX: '-50%',
+            },
+            ...a.scrollbar_offset.transform,
+          ],
+        }),
+      ]}
+    />
+  ) : null
+})
diff --git a/src/components/LikedByList.tsx b/src/components/LikedByList.tsx
index a83f98258..b369bd76e 100644
--- a/src/components/LikedByList.tsx
+++ b/src/components/LikedByList.tsx
@@ -12,8 +12,14 @@ import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
 import {List} from '#/view/com/util/List'
 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
 
-function renderItem({item}: {item: GetLikes.Like}) {
-  return <ProfileCardWithFollowBtn key={item.actor.did} profile={item.actor} />
+function renderItem({item, index}: {item: GetLikes.Like; index: number}) {
+  return (
+    <ProfileCardWithFollowBtn
+      key={item.actor.did}
+      profile={item.actor}
+      noBorder={index === 0}
+    />
+  )
 }
 
 function keyExtractor(item: GetLikes.Like) {
@@ -81,6 +87,8 @@ export function LikedByList({uri}: {uri: string}) {
         )}
         errorMessage={cleanError(resolveError || error)}
         onRetry={isError ? refetch : undefined}
+        topBorder={false}
+        sideBorders={false}
       />
     )
   }
@@ -103,6 +111,7 @@ export function LikedByList({uri}: {uri: string}) {
       onEndReachedThreshold={3}
       initialNumToRender={initialNumToRender}
       windowSize={11}
+      sideBorders={false}
     />
   )
 }
diff --git a/src/components/Lists.tsx b/src/components/Lists.tsx
index 16bd6a9ea..2d7b13b25 100644
--- a/src/components/Lists.tsx
+++ b/src/components/Lists.tsx
@@ -109,38 +109,6 @@ function ListFooterMaybeError({
   )
 }
 
-export function ListHeaderDesktop({
-  title,
-  subtitle,
-}: {
-  title: string
-  subtitle?: string
-}) {
-  const {gtTablet} = useBreakpoints()
-  const t = useTheme()
-
-  if (!gtTablet) return null
-
-  return (
-    <View
-      style={[
-        a.w_full,
-        a.py_sm,
-        a.px_xl,
-        a.gap_xs,
-        a.justify_center,
-        {minHeight: 50},
-      ]}>
-      <Text style={[a.text_2xl, a.font_bold]}>{title}</Text>
-      {subtitle ? (
-        <Text style={[a.text_md, t.atoms.text_contrast_medium]}>
-          {subtitle}
-        </Text>
-      ) : undefined}
-    </View>
-  )
-}
-
 let ListMaybePlaceholder = ({
   isLoading,
   noEmpty,
@@ -154,7 +122,7 @@ let ListMaybePlaceholder = ({
   onGoBack,
   hideBackButton,
   sideBorders,
-  topBorder = true,
+  topBorder = false,
 }: {
   isLoading: boolean
   noEmpty?: boolean
diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx
index 6c3bbf216..acffa0c2b 100644
--- a/src/components/dms/MessagesListHeader.tsx
+++ b/src/components/dms/MessagesListHeader.tsx
@@ -65,25 +65,23 @@ export let MessagesListHeader = ({
         a.pr_lg,
         a.py_sm,
       ]}>
-      {!gtTablet && (
-        <TouchableOpacity
-          testID="conversationHeaderBackBtn"
-          onPress={onPressBack}
-          hitSlop={BACK_HITSLOP}
-          style={{width: 30, height: 30, marginTop: isWeb ? 6 : 4}}
-          accessibilityRole="button"
-          accessibilityLabel={_(msg`Back`)}
-          accessibilityHint="">
-          <FontAwesomeIcon
-            size={18}
-            icon="angle-left"
-            style={{
-              marginTop: 6,
-            }}
-            color={t.atoms.text.color}
-          />
-        </TouchableOpacity>
-      )}
+      <TouchableOpacity
+        testID="conversationHeaderBackBtn"
+        onPress={onPressBack}
+        hitSlop={BACK_HITSLOP}
+        style={{width: 30, height: 30, marginTop: isWeb ? 6 : 4}}
+        accessibilityRole="button"
+        accessibilityLabel={_(msg`Back`)}
+        accessibilityHint="">
+        <FontAwesomeIcon
+          size={18}
+          icon="angle-left"
+          style={{
+            marginTop: 6,
+          }}
+          color={t.atoms.text.color}
+        />
+      </TouchableOpacity>
 
       {profile && moderation && blockInfo ? (
         <HeaderReady
diff --git a/src/components/forms/DateField/index.android.tsx b/src/components/forms/DateField/index.android.tsx
index b64c0e9fa..58f4d4f89 100644
--- a/src/components/forms/DateField/index.android.tsx
+++ b/src/components/forms/DateField/index.android.tsx
@@ -57,6 +57,7 @@ export function DateField({
           open
           timeZoneOffsetInMinutes={0}
           theme={t.scheme}
+          // @ts-ignore TODO
           buttonColor={t.name === 'light' ? '#000000' : '#ffffff'}
           date={new Date(value)}
           onConfirm={onChangeInternal}
diff --git a/src/components/icons/FloppyDisk.tsx b/src/components/icons/FloppyDisk.tsx
new file mode 100644
index 000000000..7fb938089
--- /dev/null
+++ b/src/components/icons/FloppyDisk.tsx
@@ -0,0 +1,5 @@
+import {createSinglePathSVG} from './TEMPLATE'
+
+export const FloppyDisk_Stroke2_Corner0_Rounded = createSinglePathSVG({
+  path: 'M3 4a1 1 0 0 1 1-1h13a1 1 0 0 1 .707.293l3 3A1 1 0 0 1 21 7v13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm6 15h6v-5H9v5Zm8 0v-6a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v6H5V5h2v3a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V5.414l2 2V19h-2ZM15 5H9v2h6V5Z',
+})