diff --git a/package.json b/package.json index 17cfc4c5..0fe3e7f6 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all" }, "dependencies": { - "@atproto/api": "0.3.8", + "@atproto/api": "^0.3.13", "@bam.tech/react-native-image-resizer": "^3.0.4", "@braintree/sanitize-url": "^6.0.2", "@expo/html-elements": "^0.4.2", diff --git a/src/lib/link-meta/bsky.ts b/src/lib/link-meta/bsky.ts index cf43feca..aed10389 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 { + 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 d6d43b89..ec1292e9 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\/(?[^/]+)\/lists\/(?[^/]+)/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 3913d3e6..038e9fc3 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 91f4da05..6592ed57 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 00000000..4e6f7e6b --- /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 = [ + + ), + , + ] + + // 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 {buttons} +} + +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 0e13ca33..2293dbec 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 }) => { 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 42965981..47fa4a94 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 scrollElRef?: MutableRefObject | 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 && ( - - {list.viewer?.muted ? ( -