about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorAnsh <anshnanda10@gmail.com>2023-06-26 10:15:39 -0700
committerGitHub <noreply@github.com>2023-06-26 12:15:39 -0500
commitb9abd444e5c49420ba7a4e93bd781403349076ef (patch)
tree2f6c122d912921700ec3c98b70b579cb9b6eafe0 /src
parent1666a747eb346e3d6f9d64866881da4404fb3f56 (diff)
downloadvoidsky-b9abd444e5c49420ba7a4e93bd781403349076ef.tar.zst
[APP-657] Add share list functionality (#863)
* replace delete list button text with icon

* fix mute list styling on desktop

* add share button to nav bar on a list

* fix styling when on profile

* bug: add key to ImageHorzList

* clean up code & refactor

* fix styling for ListItems

* create a reusable ListActions component for actions on a list

* remove dead styles

* add keys to ListActions

* add helpers to set list embed

* render list embeds

* fix list sharing on web

* make style prop optional in ListCard

* update `@atproto/api` to `0.3.13`
Diffstat (limited to 'src')
-rw-r--r--src/lib/link-meta/bsky.ts26
-rw-r--r--src/lib/strings/url-helpers.ts14
-rw-r--r--src/state/models/content/list.ts4
-rw-r--r--src/view/com/composer/useExternalLinkFetch.ts30
-rw-r--r--src/view/com/lists/ListActions.tsx83
-rw-r--r--src/view/com/lists/ListCard.tsx5
-rw-r--r--src/view/com/lists/ListItems.tsx78
-rw-r--r--src/view/com/lists/ListsList.tsx13
-rw-r--r--src/view/com/util/images/ImageHorzList.tsx1
-rw-r--r--src/view/com/util/post-embeds/CustomFeedEmbed.tsx37
-rw-r--r--src/view/com/util/post-embeds/ListEmbed.tsx35
-rw-r--r--src/view/com/util/post-embeds/index.tsx54
-rw-r--r--src/view/screens/ModerationMuteLists.tsx2
-rw-r--r--src/view/screens/ProfileList.tsx71
-rw-r--r--src/view/shell/bottom-bar/BottomBar.tsx1
-rw-r--r--src/view/shell/bottom-bar/BottomBarStyles.tsx1
16 files changed, 315 insertions, 140 deletions
diff --git a/src/lib/link-meta/bsky.ts b/src/lib/link-meta/bsky.ts
index cf43feca8..aed103894 100644
--- a/src/lib/link-meta/bsky.ts
+++ b/src/lib/link-meta/bsky.ts
@@ -155,3 +155,29 @@ export async function getFeedAsEmbed(
     },
   }
 }
+
+export async function getListAsEmbed(
+  store: RootStoreModel,
+  url: string,
+): Promise<apilib.ExternalEmbedDraft> {
+  url = convertBskyAppUrlIfNeeded(url)
+  const [_0, user, _1, rkey] = url.split('/').filter(Boolean)
+  const list = makeRecordUri(user, 'app.bsky.graph.list', rkey)
+  const res = await store.agent.app.bsky.graph.getList({list})
+  return {
+    isLoading: false,
+    uri: list,
+    meta: {
+      url: list,
+      likelyType: LikelyType.AtpData,
+      title: res.data.list.name,
+    },
+    embed: {
+      $type: 'app.bsky.embed.record',
+      record: {
+        uri: res.data.list.uri,
+        cid: res.data.list.cid,
+      },
+    },
+  }
+}
diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts
index d6d43b89d..ec1292e94 100644
--- a/src/lib/strings/url-helpers.ts
+++ b/src/lib/strings/url-helpers.ts
@@ -94,6 +94,20 @@ export function isBskyCustomFeedUrl(url: string): boolean {
   return false
 }
 
+export function isBskyListUrl(url: string): boolean {
+  if (isBskyAppUrl(url)) {
+    try {
+      const urlp = new URL(url)
+      return /profile\/(?<name>[^/]+)\/lists\/(?<rkey>[^/]+)/i.test(
+        urlp.pathname,
+      )
+    } catch {
+      console.error('Unexpected error in isBskyListUrl()', url)
+    }
+  }
+  return false
+}
+
 export function convertBskyAppUrlIfNeeded(url: string): string {
   if (isBskyAppUrl(url)) {
     try {
diff --git a/src/state/models/content/list.ts b/src/state/models/content/list.ts
index 3913d3e62..038e9fc30 100644
--- a/src/state/models/content/list.ts
+++ b/src/state/models/content/list.ts
@@ -97,6 +97,10 @@ export class ListModel {
     return this.list?.creator.did === this.rootStore.me.did
   }
 
+  get isSubscribed() {
+    return this.list?.viewer?.muted
+  }
+
   // public api
   // =
 
diff --git a/src/view/com/composer/useExternalLinkFetch.ts b/src/view/com/composer/useExternalLinkFetch.ts
index 91f4da059..6592ed572 100644
--- a/src/view/com/composer/useExternalLinkFetch.ts
+++ b/src/view/com/composer/useExternalLinkFetch.ts
@@ -3,9 +3,17 @@ import {useStores} from 'state/index'
 import {ImageModel} from 'state/models/media/image'
 import * as apilib from 'lib/api/index'
 import {getLinkMeta} from 'lib/link-meta/link-meta'
-import {getPostAsQuote, getFeedAsEmbed} from 'lib/link-meta/bsky'
+import {
+  getPostAsQuote,
+  getFeedAsEmbed,
+  getListAsEmbed,
+} from 'lib/link-meta/bsky'
 import {downloadAndResize} from 'lib/media/manip'
-import {isBskyPostUrl, isBskyCustomFeedUrl} from 'lib/strings/url-helpers'
+import {
+  isBskyPostUrl,
+  isBskyCustomFeedUrl,
+  isBskyListUrl,
+} from 'lib/strings/url-helpers'
 import {ComposerOpts} from 'state/models/ui/shell'
 import {POST_IMG_MAX} from 'lib/constants'
 
@@ -60,6 +68,24 @@ export function useExternalLinkFetch({
             setExtLink(undefined)
           },
         )
+      } else if (isBskyListUrl(extLink.uri)) {
+        getListAsEmbed(store, extLink.uri).then(
+          ({embed, meta}) => {
+            if (aborted) {
+              return
+            }
+            setExtLink({
+              uri: extLink.uri,
+              isLoading: false,
+              meta,
+              embed,
+            })
+          },
+          err => {
+            store.log.error('Failed to fetch list for embedding', {err})
+            setExtLink(undefined)
+          },
+        )
       } else {
         getLinkMeta(store, extLink.uri).then(meta => {
           if (aborted) {
diff --git a/src/view/com/lists/ListActions.tsx b/src/view/com/lists/ListActions.tsx
new file mode 100644
index 000000000..4e6f7e6b9
--- /dev/null
+++ b/src/view/com/lists/ListActions.tsx
@@ -0,0 +1,83 @@
+import React from 'react'
+import {StyleSheet, View} from 'react-native'
+import {Button} from '../util/forms/Button'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {usePalette} from 'lib/hooks/usePalette'
+
+export const ListActions = ({
+  muted,
+  onToggleSubscribed,
+  onPressEditList,
+  isOwner,
+  onPressDeleteList,
+  onPressShareList,
+  reversed = false, // Default value of reversed is false
+}: {
+  isOwner: boolean
+  muted?: boolean
+  onToggleSubscribed?: () => void
+  onPressEditList?: () => void
+  onPressDeleteList?: () => void
+  onPressShareList?: () => void
+  reversed?: boolean // New optional prop
+}) => {
+  const pal = usePalette('default')
+
+  let buttons = [
+    <Button
+      key="subscribeButton"
+      type={muted ? 'inverted' : 'primary'}
+      label={muted ? 'Unsubscribe' : 'Subscribe & Mute'}
+      accessibilityLabel={muted ? 'Unsubscribe' : 'Subscribe and mute'}
+      accessibilityHint=""
+      onPress={onToggleSubscribed}
+    />,
+    isOwner && (
+      <Button
+        key="editListButton"
+        type="default"
+        label="Edit List"
+        accessibilityLabel="Edit list"
+        accessibilityHint=""
+        onPress={onPressEditList}
+      />
+    ),
+    isOwner && (
+      <Button
+        key="deleteListButton"
+        type="default"
+        testID="deleteListBtn"
+        accessibilityLabel="Delete list"
+        accessibilityHint=""
+        onPress={onPressDeleteList}>
+        <FontAwesomeIcon icon={['far', 'trash-can']} style={[pal.text]} />
+      </Button>
+    ),
+    <Button
+      key="shareListButton"
+      type="default"
+      testID="shareListBtn"
+      accessibilityLabel="Share list"
+      accessibilityHint=""
+      onPress={onPressShareList}>
+      <FontAwesomeIcon icon={'share'} style={[pal.text]} />
+    </Button>,
+  ]
+
+  // If reversed is true, reverse the array to reverse the order of the buttons
+  if (reversed) {
+    buttons = buttons.filter(Boolean).reverse() // filterting out any falsey values and reversing the array
+  } else {
+    buttons = buttons.filter(Boolean) // filterting out any falsey values
+  }
+
+  return <View style={styles.headerBtns}>{buttons}</View>
+}
+
+const styles = StyleSheet.create({
+  headerBtns: {
+    flexDirection: 'row',
+    gap: 8,
+    marginTop: 12,
+  },
+})
diff --git a/src/view/com/lists/ListCard.tsx b/src/view/com/lists/ListCard.tsx
index 0e13ca333..2293dbeca 100644
--- a/src/view/com/lists/ListCard.tsx
+++ b/src/view/com/lists/ListCard.tsx
@@ -1,5 +1,5 @@
 import React from 'react'
-import {StyleSheet, View} from 'react-native'
+import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
 import {AtUri, AppBskyGraphDefs, RichText} from '@atproto/api'
 import {Link} from '../util/Link'
 import {Text} from '../util/text/Text'
@@ -16,12 +16,14 @@ export const ListCard = ({
   noBg,
   noBorder,
   renderButton,
+  style,
 }: {
   testID?: string
   list: AppBskyGraphDefs.ListView
   noBg?: boolean
   noBorder?: boolean
   renderButton?: () => JSX.Element
+  style?: StyleProp<ViewStyle>
 }) => {
   const pal = usePalette('default')
   const store = useStores()
@@ -53,6 +55,7 @@ export const ListCard = ({
         pal.border,
         noBorder && styles.outerNoBorder,
         !noBg && pal.view,
+        style,
       ]}
       href={`/profile/${list.creator.did}/lists/${rkey}`}
       title={list.name}
diff --git a/src/view/com/lists/ListItems.tsx b/src/view/com/lists/ListItems.tsx
index 42965981b..47fa4a943 100644
--- a/src/view/com/lists/ListItems.tsx
+++ b/src/view/com/lists/ListItems.tsx
@@ -6,10 +6,10 @@ import {
   StyleSheet,
   View,
   ViewStyle,
+  FlatList,
 } from 'react-native'
 import {AppBskyActorDefs, AppBskyGraphDefs, RichText} from '@atproto/api'
 import {observer} from 'mobx-react-lite'
-import {FlatList} from '../util/Views'
 import {ProfileCardFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
@@ -25,6 +25,7 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {useStores} from 'state/index'
 import {s} from 'lib/styles'
 import {isDesktopWeb} from 'platform/detection'
+import {ListActions} from './ListActions'
 
 const LOADING_ITEM = {_reactKey: '__loading__'}
 const HEADER_ITEM = {_reactKey: '__header__'}
@@ -41,6 +42,7 @@ export const ListItems = observer(
     onToggleSubscribed,
     onPressEditList,
     onPressDeleteList,
+    onPressShareList,
     renderEmptyState,
     testID,
     headerOffset = 0,
@@ -49,9 +51,10 @@ export const ListItems = observer(
     style?: StyleProp<ViewStyle>
     scrollElRef?: MutableRefObject<FlatList<any> | null>
     onPressTryAgain?: () => void
-    onToggleSubscribed?: () => void
-    onPressEditList?: () => void
-    onPressDeleteList?: () => void
+    onToggleSubscribed: () => void
+    onPressEditList: () => void
+    onPressDeleteList: () => void
+    onPressShareList: () => void
     renderEmptyState?: () => JSX.Element
     testID?: string
     headerOffset?: number
@@ -163,6 +166,7 @@ export const ListItems = observer(
               onToggleSubscribed={onToggleSubscribed}
               onPressEditList={onPressEditList}
               onPressDeleteList={onPressDeleteList}
+              onPressShareList={onPressShareList}
             />
           ) : null
         } else if (item === ERROR_ITEM) {
@@ -193,14 +197,17 @@ export const ListItems = observer(
         )
       },
       [
-        list,
-        onPressTryAgain,
-        onPressRetryLoadMore,
         renderMemberButton,
+        renderEmptyState,
+        list.list,
+        list.isOwner,
+        list.error,
+        onToggleSubscribed,
         onPressEditList,
         onPressDeleteList,
-        onToggleSubscribed,
-        renderEmptyState,
+        onPressShareList,
+        onPressTryAgain,
+        onPressRetryLoadMore,
       ],
     )
 
@@ -257,12 +264,14 @@ const ListHeader = observer(
     onToggleSubscribed,
     onPressEditList,
     onPressDeleteList,
+    onPressShareList,
   }: {
     list: AppBskyGraphDefs.ListView
     isOwner: boolean
-    onToggleSubscribed?: () => void
-    onPressEditList?: () => void
-    onPressDeleteList?: () => void
+    onToggleSubscribed: () => void
+    onPressEditList: () => void
+    onPressDeleteList: () => void
+    onPressShareList: () => void
   }) => {
     const pal = usePalette('default')
     const store = useStores()
@@ -301,43 +310,14 @@ const ListHeader = observer(
               />
             )}
             {isDesktopWeb && (
-              <View style={styles.headerBtns}>
-                {list.viewer?.muted ? (
-                  <Button
-                    type="inverted"
-                    label="Unsubscribe"
-                    accessibilityLabel="Unsubscribe"
-                    accessibilityHint=""
-                    onPress={onToggleSubscribed}
-                  />
-                ) : (
-                  <Button
-                    type="primary"
-                    label="Subscribe & Mute"
-                    accessibilityLabel="Subscribe and mute"
-                    accessibilityHint=""
-                    onPress={onToggleSubscribed}
-                  />
-                )}
-                {isOwner && (
-                  <Button
-                    type="default"
-                    label="Edit List"
-                    accessibilityLabel="Edit list"
-                    accessibilityHint=""
-                    onPress={onPressEditList}
-                  />
-                )}
-                {isOwner && (
-                  <Button
-                    type="default"
-                    label="Delete List"
-                    accessibilityLabel="Delete list"
-                    accessibilityHint=""
-                    onPress={onPressDeleteList}
-                  />
-                )}
-              </View>
+              <ListActions
+                isOwner={isOwner}
+                muted={list.viewer?.muted}
+                onPressDeleteList={onPressDeleteList}
+                onPressEditList={onPressEditList}
+                onToggleSubscribed={onToggleSubscribed}
+                onPressShareList={onPressShareList}
+              />
             )}
           </View>
           <View>
diff --git a/src/view/com/lists/ListsList.tsx b/src/view/com/lists/ListsList.tsx
index 09e3a501c..2b6f74c2b 100644
--- a/src/view/com/lists/ListsList.tsx
+++ b/src/view/com/lists/ListsList.tsx
@@ -6,6 +6,7 @@ import {
   StyleSheet,
   View,
   ViewStyle,
+  FlatList,
 } from 'react-native'
 import {observer} from 'mobx-react-lite'
 import {
@@ -13,7 +14,6 @@ import {
   FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
 import {AppBskyGraphDefs as GraphDefs} from '@atproto/api'
-import {FlatList} from '../util/Views'
 import {ListCard} from './ListCard'
 import {ProfileCardFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
 import {ErrorMessage} from '../util/error/ErrorMessage'
@@ -149,7 +149,11 @@ export const ListsList = observer(
         return renderItem ? (
           renderItem(item)
         ) : (
-          <ListCard list={item} testID={`list-${item.name}`} />
+          <ListCard
+            list={item}
+            testID={`list-${item.name}`}
+            style={styles.item}
+          />
         )
       },
       [
@@ -193,7 +197,7 @@ export const ListsList = observer(
                 progressViewOffset={headerOffset}
               />
             }
-            contentContainerStyle={s.contentContainer}
+            contentContainerStyle={[s.contentContainer]}
             style={{paddingTop: headerOffset}}
             onEndReached={onEndReached}
             onEndReachedThreshold={0.6}
@@ -237,4 +241,7 @@ const styles = StyleSheet.create({
     gap: 8,
   },
   feedFooter: {paddingTop: 20},
+  item: {
+    paddingHorizontal: 18,
+  },
 })
diff --git a/src/view/com/util/images/ImageHorzList.tsx b/src/view/com/util/images/ImageHorzList.tsx
index 14a8dd7e7..e37f8af1b 100644
--- a/src/view/com/util/images/ImageHorzList.tsx
+++ b/src/view/com/util/images/ImageHorzList.tsx
@@ -13,6 +13,7 @@ export function ImageHorzList({images, style}: Props) {
     <View style={[styles.flexRow, style]}>
       {images.map(({thumb, alt}) => (
         <Image
+          key={thumb}
           source={{uri: thumb}}
           style={styles.image}
           accessible={true}
diff --git a/src/view/com/util/post-embeds/CustomFeedEmbed.tsx b/src/view/com/util/post-embeds/CustomFeedEmbed.tsx
new file mode 100644
index 000000000..5abdf2f77
--- /dev/null
+++ b/src/view/com/util/post-embeds/CustomFeedEmbed.tsx
@@ -0,0 +1,37 @@
+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 {CustomFeedModel} from 'state/models/feeds/custom-feed'
+import {CustomFeed} from 'view/com/feeds/CustomFeed'
+
+export function CustomFeedEmbed({
+  record,
+}: {
+  record: AppBskyFeedDefs.GeneratorView
+}) {
+  const pal = usePalette('default')
+  const store = useStores()
+  const item = useMemo(
+    () => new CustomFeedModel(store, record),
+    [store, record],
+  )
+  return (
+    <CustomFeed
+      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
new file mode 100644
index 000000000..6f40b3e1a
--- /dev/null
+++ b/src/view/com/util/post-embeds/ListEmbed.tsx
@@ -0,0 +1,35 @@
+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(
+  ({
+    item,
+    style,
+  }: {
+    item: AppBskyGraphDefs.ListView
+    style?: StyleProp<ViewStyle>
+  }) => {
+    const pal = usePalette('default')
+
+    return (
+      <View style={[pal.view, pal.border, s.border1, styles.container]}>
+        <ListCard list={item} style={[style, styles.card]} />
+      </View>
+    )
+  },
+)
+
+const styles = StyleSheet.create({
+  container: {
+    borderRadius: 8,
+  },
+  card: {
+    borderTopWidth: 0,
+    borderRadius: 8,
+  },
+})
diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index 8156d7732..53ef17318 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -14,6 +14,7 @@ import {
   AppBskyEmbedRecordWithMedia,
   AppBskyFeedPost,
   AppBskyFeedDefs,
+  AppBskyGraphDefs,
 } from '@atproto/api'
 import {Link} from '../Link'
 import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
@@ -25,8 +26,8 @@ import {ExternalLinkEmbed} from './ExternalLinkEmbed'
 import {getYoutubeVideoId} from 'lib/strings/url-helpers'
 import QuoteEmbed from './QuoteEmbed'
 import {AutoSizedImage} from '../images/AutoSizedImage'
-import {CustomFeed} from 'view/com/feeds/CustomFeed'
-import {CustomFeedModel} from 'state/models/feeds/custom-feed'
+import {CustomFeedEmbed} from './CustomFeedEmbed'
+import {ListEmbed} from './ListEmbed'
 
 type Embed =
   | AppBskyEmbedRecord.View
@@ -144,6 +145,23 @@ export function PostEmbeds({
     }
   }
 
+  // custom feed embed (i.e. generator view)
+  // =
+  if (
+    AppBskyEmbedRecord.isView(embed) &&
+    AppBskyFeedDefs.isGeneratorView(embed.record)
+  ) {
+    return <CustomFeedEmbed record={embed.record} />
+  }
+
+  // list embed (e.g. mute lists; i.e. ListView)
+  if (
+    AppBskyEmbedRecord.isView(embed) &&
+    AppBskyGraphDefs.isListView(embed.record)
+  ) {
+    return <ListEmbed item={embed.record} />
+  }
+
   // external link embed
   // =
   if (AppBskyEmbedExternal.isView(embed)) {
@@ -164,34 +182,9 @@ export function PostEmbeds({
     )
   }
 
-  // custom feed embed (i.e. generator view)
-  // =
-  if (
-    AppBskyEmbedRecord.isView(embed) &&
-    AppBskyFeedDefs.isGeneratorView(embed.record)
-  ) {
-    return <CustomFeedEmbed record={embed.record} />
-  }
-
   return <View />
 }
 
-function CustomFeedEmbed({record}: {record: AppBskyFeedDefs.GeneratorView}) {
-  const pal = usePalette('default')
-  const store = useStores()
-  const item = React.useMemo(
-    () => new CustomFeedModel(store, record),
-    [store, record],
-  )
-  return (
-    <CustomFeed
-      item={item}
-      style={[pal.view, pal.border, styles.customFeedOuter]}
-      showLikes
-    />
-  )
-}
-
 const styles = StyleSheet.create({
   stackContainer: {
     gap: 6,
@@ -208,13 +201,6 @@ const styles = StyleSheet.create({
     borderRadius: 8,
     marginTop: 4,
   },
-  customFeedOuter: {
-    borderWidth: 1,
-    borderRadius: 8,
-    marginTop: 4,
-    paddingHorizontal: 12,
-    paddingVertical: 12,
-  },
   alt: {
     backgroundColor: 'rgba(0, 0, 0, 0.75)',
     borderRadius: 6,
diff --git a/src/view/screens/ModerationMuteLists.tsx b/src/view/screens/ModerationMuteLists.tsx
index 0b81f432f..c2771290f 100644
--- a/src/view/screens/ModerationMuteLists.tsx
+++ b/src/view/screens/ModerationMuteLists.tsx
@@ -87,9 +87,9 @@ export const ModerationMuteListsScreen = withAuthRequired(({}: Props) => {
     <CenteredView
       style={[
         styles.container,
-        isDesktopWeb && styles.containerDesktop,
         pal.view,
         pal.border,
+        isDesktopWeb && styles.containerDesktop,
       ]}
       testID="moderationMutelistsScreen">
       <ViewHeader
diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx
index 7c3ed831c..98a8d0ac4 100644
--- a/src/view/screens/ProfileList.tsx
+++ b/src/view/screens/ProfileList.tsx
@@ -1,5 +1,5 @@
 import React from 'react'
-import {StyleSheet, View} from 'react-native'
+import {StyleSheet} from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
 import {useNavigation} from '@react-navigation/native'
@@ -9,7 +9,6 @@ import {ViewHeader} from 'view/com/util/ViewHeader'
 import {CenteredView} from 'view/com/util/Views'
 import {ListItems} from 'view/com/lists/ListItems'
 import {EmptyState} from 'view/com/util/EmptyState'
-import {Button} from 'view/com/util/forms/Button'
 import * as Toast from 'view/com/util/Toast'
 import {ListModel} from 'state/models/content/list'
 import {useStores} from 'state/index'
@@ -17,6 +16,9 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {useSetTitle} from 'lib/hooks/useSetTitle'
 import {NavigationProp} from 'lib/routes/types'
 import {isDesktopWeb} from 'platform/detection'
+import {toShareUrl} from 'lib/strings/url-helpers'
+import {shareUrl} from 'lib/sharing'
+import {ListActions} from 'view/com/lists/ListActions'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileList'>
 export const ProfileListScreen = withAuthRequired(
@@ -71,7 +73,7 @@ export const ProfileListScreen = withAuthRequired(
       store.shell.openModal({
         name: 'confirm',
         title: 'Delete List',
-        message: 'Are you sure?',
+        message: 'Are you sure',
         async onPressConfirm() {
           await list.delete()
           if (navigation.canGoBack()) {
@@ -83,59 +85,33 @@ export const ProfileListScreen = withAuthRequired(
       })
     }, [store, list, navigation])
 
+    const onPressShareList = React.useCallback(() => {
+      const url = toShareUrl(`/profile/${name}/lists/${rkey}`)
+      shareUrl(url)
+    }, [name, rkey])
+
     const renderEmptyState = React.useCallback(() => {
       return <EmptyState icon="users-slash" message="This list is empty!" />
     }, [])
 
     const renderHeaderBtns = React.useCallback(() => {
       return (
-        <View style={styles.headerBtns}>
-          {list?.isOwner && (
-            <Button
-              type="default"
-              label="Delete List"
-              testID="deleteListBtn"
-              accessibilityLabel="Delete list"
-              accessibilityHint=""
-              onPress={onPressDeleteList}
-            />
-          )}
-          {list?.isOwner && (
-            <Button
-              type="default"
-              label="Edit List"
-              testID="editListBtn"
-              accessibilityLabel="Edit list"
-              accessibilityHint=""
-              onPress={onPressEditList}
-            />
-          )}
-          {list.list?.viewer?.muted ? (
-            <Button
-              type="inverted"
-              label="Unsubscribe"
-              testID="unsubscribeListBtn"
-              accessibilityLabel="Unsubscribe from list"
-              accessibilityHint=""
-              onPress={onToggleSubscribed}
-            />
-          ) : (
-            <Button
-              type="primary"
-              label="Subscribe & Mute"
-              testID="subscribeListBtn"
-              accessibilityLabel="Subscribe to this list"
-              accessibilityHint="Mutes the users included in this list"
-              onPress={onToggleSubscribed}
-            />
-          )}
-        </View>
+        <ListActions
+          muted={list.list?.viewer?.muted}
+          isOwner={list.isOwner}
+          onPressDeleteList={onPressDeleteList}
+          onPressEditList={onPressEditList}
+          onToggleSubscribed={onToggleSubscribed}
+          onPressShareList={onPressShareList}
+          reversed={true}
+        />
       )
     }, [
-      list?.isOwner,
+      list.isOwner,
       list.list?.viewer?.muted,
       onPressDeleteList,
       onPressEditList,
+      onPressShareList,
       onToggleSubscribed,
     ])
 
@@ -155,6 +131,7 @@ export const ProfileListScreen = withAuthRequired(
           onToggleSubscribed={onToggleSubscribed}
           onPressEditList={onPressEditList}
           onPressDeleteList={onPressDeleteList}
+          onPressShareList={onPressShareList}
         />
       </CenteredView>
     )
@@ -162,10 +139,6 @@ export const ProfileListScreen = withAuthRequired(
 )
 
 const styles = StyleSheet.create({
-  headerBtns: {
-    flexDirection: 'row',
-    gap: 8,
-  },
   container: {
     flex: 1,
     paddingBottom: isDesktopWeb ? 0 : 100,
diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx
index 09b6f9f65..60a6c8e67 100644
--- a/src/view/shell/bottom-bar/BottomBar.tsx
+++ b/src/view/shell/bottom-bar/BottomBar.tsx
@@ -189,6 +189,7 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
                   pal.text,
                   styles.profileIcon,
                   styles.onProfile,
+                  {borderColor: pal.text.color},
                 ]}>
                 <UserAvatar avatar={store.me.avatar} size={27} />
               </View>
diff --git a/src/view/shell/bottom-bar/BottomBarStyles.tsx b/src/view/shell/bottom-bar/BottomBarStyles.tsx
index 2414b9911..f31ab44cf 100644
--- a/src/view/shell/bottom-bar/BottomBarStyles.tsx
+++ b/src/view/shell/bottom-bar/BottomBarStyles.tsx
@@ -59,7 +59,6 @@ export const styles = StyleSheet.create({
     top: -4,
   },
   onProfile: {
-    borderColor: colors.black,
     borderWidth: 1,
     borderRadius: 100,
   },