diff options
Diffstat (limited to 'src/components')
-rw-r--r-- | src/components/FeedCard.tsx | 198 | ||||
-rw-r--r-- | src/components/KnownFollowers.tsx | 2 | ||||
-rw-r--r-- | src/components/Prompt.tsx | 17 | ||||
-rw-r--r-- | src/components/dms/LeaveConvoPrompt.tsx | 2 | ||||
-rw-r--r-- | src/components/icons/Arrow.tsx | 4 |
5 files changed, 214 insertions, 9 deletions
diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx new file mode 100644 index 000000000..2745ed7c9 --- /dev/null +++ b/src/components/FeedCard.tsx @@ -0,0 +1,198 @@ +import React from 'react' +import {GestureResponderEvent, View} from 'react-native' +import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api' +import {msg, plural, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {logger} from '#/logger' +import { + useAddSavedFeedsMutation, + usePreferencesQuery, + useRemoveFeedMutation, +} from '#/state/queries/preferences' +import {sanitizeHandle} from 'lib/strings/handles' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import * as Toast from 'view/com/util/Toast' +import {useTheme} from '#/alf' +import {atoms as a} from '#/alf' +import {Button, ButtonIcon} from '#/components/Button' +import {useRichText} from '#/components/hooks/useRichText' +import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' +import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' +import {Link as InternalLink} from '#/components/Link' +import {Loader} from '#/components/Loader' +import * as Prompt from '#/components/Prompt' +import {RichText} from '#/components/RichText' +import {Text} from '#/components/Typography' + +export function Default({feed}: {feed: AppBskyFeedDefs.GeneratorView}) { + return ( + <Link feed={feed}> + <Outer> + <Header> + <Avatar src={feed.avatar} /> + <TitleAndByline title={feed.displayName} creator={feed.creator} /> + <Action uri={feed.uri} pin /> + </Header> + <Description description={feed.description} /> + <Likes count={feed.likeCount || 0} /> + </Outer> + </Link> + ) +} + +export function Link({ + children, + feed, +}: { + children: React.ReactElement + feed: AppBskyFeedDefs.GeneratorView +}) { + const href = React.useMemo(() => { + const urip = new AtUri(feed.uri) + const handleOrDid = feed.creator.handle || feed.creator.did + return `/profile/${handleOrDid}/feed/${urip.rkey}` + }, [feed]) + return <InternalLink to={href}>{children}</InternalLink> +} + +export function Outer({children}: {children: React.ReactNode}) { + return <View style={[a.flex_1, a.gap_md]}>{children}</View> +} + +export function Header({children}: {children: React.ReactNode}) { + return <View style={[a.flex_row, a.align_center, a.gap_md]}>{children}</View> +} + +export function Avatar({src}: {src: string | undefined}) { + return <UserAvatar type="algo" size={40} avatar={src} /> +} + +export function TitleAndByline({ + title, + creator, +}: { + title: string + creator: AppBskyActorDefs.ProfileViewBasic +}) { + const t = useTheme() + + return ( + <View style={[a.flex_1]}> + <Text + style={[a.text_md, a.font_bold, a.flex_1, a.leading_snug]} + numberOfLines={1}> + {title} + </Text> + <Text + style={[a.flex_1, a.leading_snug, t.atoms.text_contrast_medium]} + numberOfLines={1}> + <Trans>Feed by {sanitizeHandle(creator.handle, '@')}</Trans> + </Text> + </View> + ) +} + +export function Description({description}: {description?: string}) { + const [rt, isResolving] = useRichText(description || '') + if (!description) return null + return isResolving ? ( + <RichText value={description} style={[a.leading_snug]} /> + ) : ( + <RichText value={rt} style={[a.leading_snug]} /> + ) +} + +export function Likes({count}: {count: number}) { + const t = useTheme() + return ( + <Text style={[a.text_sm, t.atoms.text_contrast_medium]}> + {plural(count || 0, { + one: 'Liked by # user', + other: 'Liked by # users', + })} + </Text> + ) +} + +export function Action({uri, pin}: {uri: string; pin?: boolean}) { + const {_} = useLingui() + const {data: preferences} = usePreferencesQuery() + const {isPending: isAddSavedFeedPending, mutateAsync: saveFeeds} = + useAddSavedFeedsMutation() + const {isPending: isRemovePending, mutateAsync: removeFeed} = + useRemoveFeedMutation() + const savedFeedConfig = React.useMemo(() => { + return preferences?.savedFeeds?.find( + feed => feed.type === 'feed' && feed.value === uri, + ) + }, [preferences?.savedFeeds, uri]) + const removePromptControl = Prompt.usePromptControl() + const isPending = isAddSavedFeedPending || isRemovePending + + const toggleSave = React.useCallback( + async (e: GestureResponderEvent) => { + e.preventDefault() + e.stopPropagation() + + try { + if (savedFeedConfig) { + await removeFeed(savedFeedConfig) + } else { + await saveFeeds([ + { + type: 'feed', + value: uri, + pinned: pin || false, + }, + ]) + } + Toast.show(_(msg`Feeds updated!`)) + } catch (e: any) { + logger.error(e, {context: `FeedCard: failed to update feeds`, pin}) + Toast.show(_(msg`Failed to update feeds`)) + } + }, + [_, pin, saveFeeds, removeFeed, uri, savedFeedConfig], + ) + + const onPrompRemoveFeed = React.useCallback( + async (e: GestureResponderEvent) => { + e.preventDefault() + e.stopPropagation() + + removePromptControl.open() + }, + [removePromptControl], + ) + + return ( + <> + <Button + disabled={isPending} + label={_(msg`Add this feed to your feeds`)} + size="small" + variant="ghost" + color="secondary" + shape="square" + onPress={savedFeedConfig ? onPrompRemoveFeed : toggleSave}> + {savedFeedConfig ? ( + <ButtonIcon size="md" icon={isPending ? Loader : Trash} /> + ) : ( + <ButtonIcon size="md" icon={isPending ? Loader : Plus} /> + )} + </Button> + + <Prompt.Basic + control={removePromptControl} + title={_(msg`Remove from my feeds?`)} + description={_( + msg`Are you sure you want to remove this from your feeds?`, + )} + onConfirm={toggleSave} + confirmButtonCta={_(msg`Remove`)} + confirmButtonColor="negative" + /> + </> + ) +} diff --git a/src/components/KnownFollowers.tsx b/src/components/KnownFollowers.tsx index a8bdb763d..63f61ce85 100644 --- a/src/components/KnownFollowers.tsx +++ b/src/components/KnownFollowers.tsx @@ -100,7 +100,7 @@ function KnownFollowersInner({ moderation, } }) - const count = cachedKnownFollowers.count - Math.min(slice.length, 2) + const count = cachedKnownFollowers.count return ( <Link diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx index d05cab5ab..315ad0dfd 100644 --- a/src/components/Prompt.tsx +++ b/src/components/Prompt.tsx @@ -1,10 +1,10 @@ import React from 'react' -import {View} from 'react-native' +import {GestureResponderEvent, View} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {atoms as a, useBreakpoints, useTheme} from '#/alf' -import {Button, ButtonColor, ButtonText} from '#/components/Button' +import {Button, ButtonColor, ButtonProps, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {Text} from '#/components/Typography' @@ -136,7 +136,7 @@ export function Action({ * Note: The dialog will close automatically when the action is pressed, you * should NOT close the dialog as a side effect of this method. */ - onPress: () => void + onPress: ButtonProps['onPress'] color?: ButtonColor /** * Optional i18n string. If undefined, it will default to "Confirm". @@ -147,9 +147,12 @@ export function Action({ const {_} = useLingui() const {gtMobile} = useBreakpoints() const {close} = Dialog.useDialogContext() - const handleOnPress = React.useCallback(() => { - close(onPress) - }, [close, onPress]) + const handleOnPress = React.useCallback( + (e: GestureResponderEvent) => { + close(() => onPress?.(e)) + }, + [close, onPress], + ) return ( <Button @@ -186,7 +189,7 @@ export function Basic({ * Note: The dialog will close automatically when the action is pressed, you * should NOT close the dialog as a side effect of this method. */ - onConfirm: () => void + onConfirm: ButtonProps['onPress'] confirmButtonColor?: ButtonColor showCancel?: boolean }>) { diff --git a/src/components/dms/LeaveConvoPrompt.tsx b/src/components/dms/LeaveConvoPrompt.tsx index 1c42dbca0..7abc76f34 100644 --- a/src/components/dms/LeaveConvoPrompt.tsx +++ b/src/components/dms/LeaveConvoPrompt.tsx @@ -49,7 +49,7 @@ export function LeaveConvoPrompt({ )} confirmButtonCta={_(msg`Leave`)} confirmButtonColor="negative" - onConfirm={leaveConvo} + onConfirm={() => leaveConvo()} /> ) } diff --git a/src/components/icons/Arrow.tsx b/src/components/icons/Arrow.tsx index eb753e549..d6fb635e9 100644 --- a/src/components/icons/Arrow.tsx +++ b/src/components/icons/Arrow.tsx @@ -7,3 +7,7 @@ export const ArrowTopRight_Stroke2_Corner0_Rounded = createSinglePathSVG({ export const ArrowLeft_Stroke2_Corner0_Rounded = createSinglePathSVG({ path: 'M3 12a1 1 0 0 1 .293-.707l6-6a1 1 0 0 1 1.414 1.414L6.414 11H20a1 1 0 1 1 0 2H6.414l4.293 4.293a1 1 0 0 1-1.414 1.414l-6-6A1 1 0 0 1 3 12Z', }) + +export const ArrowBottom_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 21a1 1 0 0 1-.707-.293l-6-6a1 1 0 1 1 1.414-1.414L11 17.586V4a1 1 0 1 1 2 0v13.586l4.293-4.293a1 1 0 0 1 1.414 1.414l-6 6A1 1 0 0 1 12 21Z', +}) |