about summary refs log tree commit diff
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2023-05-18 16:22:11 -0500
committerPaul Frazee <pfrazee@gmail.com>2023-05-18 16:22:11 -0500
commit1ecf0da81b6e5eaf7959e1416df1e8f004e2566f (patch)
treeeeb039b8671e0a693961613c595202b97c56ea99
parent84990c509e9feb0cd44921a318aedcbad92b1da7 (diff)
downloadvoidsky-1ecf0da81b6e5eaf7959e1416df1e8f004e2566f.tar.zst
Add feed sharing
-rw-r--r--src/lib/api/index.ts93
-rw-r--r--src/lib/link-meta/bsky.ts27
-rw-r--r--src/lib/strings/url-helpers.ts12
-rw-r--r--src/view/com/composer/useExternalLinkFetch.ts22
-rw-r--r--src/view/screens/CustomFeed.tsx35
5 files changed, 141 insertions, 48 deletions
diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts
index 3877b3ef7..81b61a444 100644
--- a/src/lib/api/index.ts
+++ b/src/lib/api/index.ts
@@ -18,6 +18,7 @@ export interface ExternalEmbedDraft {
   uri: string
   isLoading: boolean
   meta?: LinkMeta
+  embed?: AppBskyEmbedRecord.Main
   localThumb?: ImageModel
 }
 
@@ -135,40 +136,54 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
   }
 
   if (opts.extLink && !opts.images?.length) {
-    let thumb
-    if (opts.extLink.localThumb) {
-      opts.onStateChange?.('Uploading link thumbnail...')
-      let encoding
-      if (opts.extLink.localThumb.mime) {
-        encoding = opts.extLink.localThumb.mime
-      } else if (opts.extLink.localThumb.path.endsWith('.png')) {
-        encoding = 'image/png'
-      } else if (
-        opts.extLink.localThumb.path.endsWith('.jpeg') ||
-        opts.extLink.localThumb.path.endsWith('.jpg')
-      ) {
-        encoding = 'image/jpeg'
-      } else {
-        store.log.warn(
-          'Unexpected image format for thumbnail, skipping',
-          opts.extLink.localThumb.path,
-        )
-      }
-      if (encoding) {
-        const thumbUploadRes = await uploadBlob(
-          store,
-          opts.extLink.localThumb.path,
-          encoding,
-        )
-        thumb = thumbUploadRes.data.blob
+    if (opts.extLink.embed) {
+      embed = opts.extLink.embed
+    } else {
+      let thumb
+      if (opts.extLink.localThumb) {
+        opts.onStateChange?.('Uploading link thumbnail...')
+        let encoding
+        if (opts.extLink.localThumb.mime) {
+          encoding = opts.extLink.localThumb.mime
+        } else if (opts.extLink.localThumb.path.endsWith('.png')) {
+          encoding = 'image/png'
+        } else if (
+          opts.extLink.localThumb.path.endsWith('.jpeg') ||
+          opts.extLink.localThumb.path.endsWith('.jpg')
+        ) {
+          encoding = 'image/jpeg'
+        } else {
+          store.log.warn(
+            'Unexpected image format for thumbnail, skipping',
+            opts.extLink.localThumb.path,
+          )
+        }
+        if (encoding) {
+          const thumbUploadRes = await uploadBlob(
+            store,
+            opts.extLink.localThumb.path,
+            encoding,
+          )
+          thumb = thumbUploadRes.data.blob
+        }
       }
-    }
 
-    if (opts.quote) {
-      embed = {
-        $type: 'app.bsky.embed.recordWithMedia',
-        record: embed,
-        media: {
+      if (opts.quote) {
+        embed = {
+          $type: 'app.bsky.embed.recordWithMedia',
+          record: embed,
+          media: {
+            $type: 'app.bsky.embed.external',
+            external: {
+              uri: opts.extLink.uri,
+              title: opts.extLink.meta?.title || '',
+              description: opts.extLink.meta?.description || '',
+              thumb,
+            },
+          } as AppBskyEmbedExternal.Main,
+        } as AppBskyEmbedRecordWithMedia.Main
+      } else {
+        embed = {
           $type: 'app.bsky.embed.external',
           external: {
             uri: opts.extLink.uri,
@@ -176,18 +191,8 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
             description: opts.extLink.meta?.description || '',
             thumb,
           },
-        } as AppBskyEmbedExternal.Main,
-      } as AppBskyEmbedRecordWithMedia.Main
-    } else {
-      embed = {
-        $type: 'app.bsky.embed.external',
-        external: {
-          uri: opts.extLink.uri,
-          title: opts.extLink.meta?.title || '',
-          description: opts.extLink.meta?.description || '',
-          thumb,
-        },
-      } as AppBskyEmbedExternal.Main
+        } as AppBskyEmbedExternal.Main
+      }
     }
   }
 
diff --git a/src/lib/link-meta/bsky.ts b/src/lib/link-meta/bsky.ts
index f4a96a22f..cf43feca8 100644
--- a/src/lib/link-meta/bsky.ts
+++ b/src/lib/link-meta/bsky.ts
@@ -1,3 +1,4 @@
+import * as apilib from 'lib/api/index'
 import {LikelyType, LinkMeta} from './link-meta'
 // import {match as matchRoute} from 'view/routes'
 import {convertBskyAppUrlIfNeeded, makeRecordUri} from '../strings/url-helpers'
@@ -128,3 +129,29 @@ export async function getPostAsQuote(
     },
   }
 }
+
+export async function getFeedAsEmbed(
+  store: RootStoreModel,
+  url: string,
+): Promise<apilib.ExternalEmbedDraft> {
+  url = convertBskyAppUrlIfNeeded(url)
+  const [_0, user, _1, rkey] = url.split('/').filter(Boolean)
+  const feed = makeRecordUri(user, 'app.bsky.feed.generator', rkey)
+  const res = await store.agent.app.bsky.feed.getFeedGenerator({feed})
+  return {
+    isLoading: false,
+    uri: feed,
+    meta: {
+      url: feed,
+      likelyType: LikelyType.AtpData,
+      title: res.data.view.displayName,
+    },
+    embed: {
+      $type: 'app.bsky.embed.record',
+      record: {
+        uri: res.data.view.uri,
+        cid: res.data.view.cid,
+      },
+    },
+  }
+}
diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts
index a5412920e..d6d43b89d 100644
--- a/src/lib/strings/url-helpers.ts
+++ b/src/lib/strings/url-helpers.ts
@@ -82,6 +82,18 @@ export function isBskyPostUrl(url: string): boolean {
   return false
 }
 
+export function isBskyCustomFeedUrl(url: string): boolean {
+  if (isBskyAppUrl(url)) {
+    try {
+      const urlp = new URL(url)
+      return /profile\/(?<name>[^/]+)\/feed\/(?<rkey>[^/]+)/i.test(
+        urlp.pathname,
+      )
+    } catch {}
+  }
+  return false
+}
+
 export function convertBskyAppUrlIfNeeded(url: string): string {
   if (isBskyAppUrl(url)) {
     try {
diff --git a/src/view/com/composer/useExternalLinkFetch.ts b/src/view/com/composer/useExternalLinkFetch.ts
index 45c2dfd0d..8d3b8cac2 100644
--- a/src/view/com/composer/useExternalLinkFetch.ts
+++ b/src/view/com/composer/useExternalLinkFetch.ts
@@ -2,9 +2,9 @@ import {useState, useEffect} from 'react'
 import {useStores} from 'state/index'
 import * as apilib from 'lib/api/index'
 import {getLinkMeta} from 'lib/link-meta/link-meta'
-import {getPostAsQuote} from 'lib/link-meta/bsky'
+import {getPostAsQuote, getFeedAsEmbed} from 'lib/link-meta/bsky'
 import {downloadAndResize} from 'lib/media/manip'
-import {isBskyPostUrl} from 'lib/strings/url-helpers'
+import {isBskyPostUrl, isBskyCustomFeedUrl} from 'lib/strings/url-helpers'
 import {ComposerOpts} from 'state/models/ui/shell'
 import {POST_IMG_MAX} from 'lib/constants'
 
@@ -41,6 +41,24 @@ export function useExternalLinkFetch({
             setExtLink(undefined)
           },
         )
+      } else if (isBskyCustomFeedUrl(extLink.uri)) {
+        getFeedAsEmbed(store, extLink.uri).then(
+          ({embed, meta}) => {
+            if (aborted) {
+              return
+            }
+            setExtLink({
+              uri: extLink.uri,
+              isLoading: false,
+              meta,
+              embed,
+            })
+          },
+          err => {
+            store.log.error('Failed to fetch feed for embedding', {err})
+            setExtLink(undefined)
+          },
+        )
       } else {
         getLinkMeta(store, extLink.uri).then(meta => {
           if (aborted) {
diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx
index bbcc08513..d2b9041f9 100644
--- a/src/view/screens/CustomFeed.tsx
+++ b/src/view/screens/CustomFeed.tsx
@@ -1,5 +1,6 @@
 import React, {useMemo, useRef} from 'react'
 import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {usePalette} from 'lib/hooks/usePalette'
 import {HeartIcon, HeartIconSolid} from 'lib/icons'
 import {CommonNavigatorParams} from 'lib/routes/types'
@@ -21,6 +22,8 @@ import {Text} from 'view/com/util/text/Text'
 import * as Toast from 'view/com/util/Toast'
 import {isDesktopWeb} from 'platform/detection'
 import {useSetTitle} from 'lib/hooks/useSetTitle'
+import {shareUrl} from 'lib/sharing'
+import {toShareUrl} from 'lib/strings/url-helpers'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'CustomFeed'>
 export const CustomFeedScreen = withAuthRequired(
@@ -73,11 +76,23 @@ export const CustomFeedScreen = withAuthRequired(
         store.log.error('Failed up toggle like', {err})
       }
     }, [store, currentFeed])
+    const onPressShare = React.useCallback(() => {
+      const url = toShareUrl(`/profile/${name}/feed/${rkey}`)
+      shareUrl(url)
+    }, [name, rkey])
 
     const renderHeaderBtns = React.useCallback(() => {
       return (
         <View style={styles.headerBtns}>
           <Button
+            testID="shareBtn"
+            type="default"
+            accessibilityLabel="Share this feed"
+            accessibilityHint=""
+            onPress={onPressShare}>
+            <FontAwesomeIcon icon="share" size={18} color={pal.colors.icon} />
+          </Button>
+          <Button
             type="default"
             testID="toggleLikeBtn"
             accessibilityLabel="Like this feed"
@@ -108,6 +123,7 @@ export const CustomFeedScreen = withAuthRequired(
       currentFeed?.isLiked,
       onToggleSaved,
       onToggleLiked,
+      onPressShare,
     ])
 
     const renderListHeaderComponent = React.useCallback(() => {
@@ -151,14 +167,28 @@ export const CustomFeedScreen = withAuthRequired(
                         : 'Add to My Feeds'
                     }
                   />
-
-                  <Button type="default" onPress={onToggleLiked}>
+                  <Button
+                    type="default"
+                    accessibilityLabel="Like this feed"
+                    accessibilityHint=""
+                    onPress={onToggleLiked}>
                     {currentFeed?.isLiked ? (
                       <HeartIconSolid size={18} style={styles.liked} />
                     ) : (
                       <HeartIcon strokeWidth={3} size={18} style={pal.icon} />
                     )}
                   </Button>
+                  <Button
+                    type="default"
+                    accessibilityLabel="Share this feed"
+                    accessibilityHint=""
+                    onPress={onPressShare}>
+                    <FontAwesomeIcon
+                      icon="share"
+                      size={18}
+                      color={pal.colors.icon}
+                    />
+                  </Button>
                 </View>
               )}
             </View>
@@ -202,6 +232,7 @@ export const CustomFeedScreen = withAuthRequired(
       currentFeed,
       onToggleLiked,
       onToggleSaved,
+      onPressShare,
       name,
       rkey,
     ])