diff options
Diffstat (limited to 'src/components')
-rw-r--r-- | src/components/GradientFill.tsx | 8 | ||||
-rw-r--r-- | src/components/TrendingTopics.tsx | 223 | ||||
-rw-r--r-- | src/components/interstitials/Trending.tsx | 111 |
3 files changed, 339 insertions, 3 deletions
diff --git a/src/components/GradientFill.tsx b/src/components/GradientFill.tsx index 3dff404d7..9ad6ed7dc 100644 --- a/src/components/GradientFill.tsx +++ b/src/components/GradientFill.tsx @@ -1,11 +1,13 @@ import {LinearGradient} from 'expo-linear-gradient' -import {atoms as a, tokens} from '#/alf' +import {atoms as a, tokens, ViewStyleProp} from '#/alf' export function GradientFill({ gradient, -}: { + style, +}: ViewStyleProp & { gradient: + | typeof tokens.gradients.primary | typeof tokens.gradients.sky | typeof tokens.gradients.midnight | typeof tokens.gradients.sunrise @@ -26,7 +28,7 @@ export function GradientFill({ } start={{x: 0, y: 0}} end={{x: 1, y: 1}} - style={[a.absolute, a.inset_0]} + style={[a.absolute, a.inset_0, style]} /> ) } diff --git a/src/components/TrendingTopics.tsx b/src/components/TrendingTopics.tsx new file mode 100644 index 000000000..6881f24bd --- /dev/null +++ b/src/components/TrendingTopics.tsx @@ -0,0 +1,223 @@ +import React from 'react' +import {View} from 'react-native' +import {AtUri} from '@atproto/api' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +// import {makeProfileLink} from '#/lib/routes/links' +// import {feedUriToHref} from '#/lib/strings/url-helpers' +// import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag' +// import {CloseQuote_Filled_Stroke2_Corner0_Rounded as Quote} from '#/components/icons/Quote' +// import {UserAvatar} from '#/view/com/util/UserAvatar' +import type {TrendingTopic} from '#/state/queries/trending/useTrendingTopics' +import {atoms as a, useTheme, ViewStyleProp} from '#/alf' +import {Link as InternalLink, LinkProps} from '#/components/Link' +import {Text} from '#/components/Typography' + +export function TrendingTopic({ + topic: raw, + size, + style, +}: {topic: TrendingTopic; size?: 'large' | 'small'} & ViewStyleProp) { + const t = useTheme() + const topic = useTopic(raw) + + const isSmall = size === 'small' + // const hasAvi = topic.type === 'feed' || topic.type === 'profile' + // const aviSize = isSmall ? 16 : 20 + // const iconSize = isSmall ? 16 : 20 + + return ( + <View + style={[ + a.flex_row, + a.align_center, + a.rounded_full, + a.border, + t.atoms.border_contrast_medium, + t.atoms.bg, + isSmall + ? [ + { + paddingVertical: 5, + paddingHorizontal: 10, + }, + ] + : [a.py_sm, a.px_md], + style, + /* + { + padding: 6, + gap: hasAvi ? 4 : 2, + }, + a.pr_md, + */ + ]}> + {/* + <View + style={[ + a.align_center, + a.justify_center, + a.rounded_full, + a.overflow_hidden, + { + width: aviSize, + height: aviSize, + }, + ]}> + {topic.type === 'tag' ? ( + <Hashtag width={iconSize} /> + ) : topic.type === 'topic' ? ( + <Quote width={iconSize - 2} /> + ) : topic.type === 'feed' ? ( + <UserAvatar + type="user" + size={aviSize} + avatar="" + /> + ) : ( + <UserAvatar + type="user" + size={aviSize} + avatar="" + /> + )} + </View> + */} + + <Text + style={[ + a.font_bold, + a.leading_tight, + isSmall ? [a.text_sm] : [a.text_md, {paddingBottom: 1}], + ]} + numberOfLines={1}> + {topic.displayName} + </Text> + </View> + ) +} + +export function TrendingTopicSkeleton({ + size = 'large', + index = 0, +}: { + size?: 'large' | 'small' + index?: number +}) { + const t = useTheme() + const isSmall = size === 'small' + return ( + <View + style={[ + a.rounded_full, + a.border, + t.atoms.border_contrast_medium, + t.atoms.bg_contrast_25, + isSmall + ? { + width: index % 2 === 0 ? 75 : 90, + height: 27, + } + : { + width: index % 2 === 0 ? 90 : 110, + height: 36, + }, + ]} + /> + ) +} + +export function TrendingTopicLink({ + topic: raw, + children, + ...rest +}: { + topic: TrendingTopic +} & Omit<LinkProps, 'to' | 'label'>) { + const topic = useTopic(raw) + + return ( + <InternalLink label={topic.label} to={topic.url} {...rest}> + {children} + </InternalLink> + ) +} + +type ParsedTrendingTopic = + | { + type: 'topic' | 'tag' | 'unknown' + label: string + displayName: string + url: string + uri: undefined + } + | { + type: 'profile' | 'feed' + label: string + displayName: string + url: string + uri: AtUri + } + +export function useTopic(raw: TrendingTopic): ParsedTrendingTopic { + const {_} = useLingui() + return React.useMemo(() => { + const {topic: displayName, link} = raw + + if (link.startsWith('/search')) { + return { + type: 'topic', + label: _(msg`Browse posts about ${displayName}`), + displayName, + uri: undefined, + url: link, + } + } else if (link.startsWith('/hashtag')) { + return { + type: 'tag', + label: _(msg`Browse posts tagged with ${displayName}`), + displayName, + // displayName: displayName.replace(/^#/, ''), + uri: undefined, + url: link, + } + } + + /* + if (!link.startsWith('at://')) { + // above logic + } else { + const urip = new AtUri(link) + switch (urip.collection) { + case 'app.bsky.actor.profile': { + return { + type: 'profile', + label: _(msg`View ${displayName}'s profile`), + displayName, + uri: urip, + url: makeProfileLink({did: urip.host, handle: urip.host}), + } + } + case 'app.bsky.feed.generator': { + return { + type: 'feed', + label: _(msg`Browse the ${displayName} feed`), + displayName, + uri: urip, + url: feedUriToHref(link), + } + } + } + } + */ + + return { + type: 'unknown', + label: _(msg`Browse topic ${displayName}`), + displayName, + uri: undefined, + url: link, + } + }, [_, raw]) +} diff --git a/src/components/interstitials/Trending.tsx b/src/components/interstitials/Trending.tsx new file mode 100644 index 000000000..3944d92f0 --- /dev/null +++ b/src/components/interstitials/Trending.tsx @@ -0,0 +1,111 @@ +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import { + useTrendingSettings, + useTrendingSettingsApi, +} from '#/state/preferences/trending' +import { + DEFAULT_LIMIT as TRENDING_TOPICS_COUNT, + useTrendingTopics, +} from '#/state/queries/trending/useTrendingTopics' +import {useTrendingConfig} from '#/state/trending-config' +import {atoms as a, tokens, useGutters, useTheme} from '#/alf' +import {Button, ButtonIcon} from '#/components/Button' +import {GradientFill} from '#/components/GradientFill' +import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' +import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2' +import * as Prompt from '#/components/Prompt' +import { + TrendingTopic, + TrendingTopicLink, + TrendingTopicSkeleton, +} from '#/components/TrendingTopics' +import {Text} from '#/components/Typography' + +export function TrendingInterstitial() { + const {enabled} = useTrendingConfig() + const {trendingDisabled} = useTrendingSettings() + return enabled && !trendingDisabled ? <Inner /> : null +} + +export function Inner() { + const t = useTheme() + const {_} = useLingui() + const gutters = useGutters(['wide', 'base']) + const trendingPrompt = Prompt.usePromptControl() + const {setTrendingDisabled} = useTrendingSettingsApi() + const {data: trending, error, isLoading} = useTrendingTopics() + const noTopics = !isLoading && !error && !trending?.topics?.length + + return error || noTopics ? null : ( + <View + style={[ + gutters, + a.gap_lg, + a.border_t, + t.atoms.border_contrast_low, + t.atoms.bg_contrast_25, + ]}> + <View style={[a.flex_row, a.align_center, a.gap_sm]}> + <View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> + <Graph size="lg" /> + <Text style={[a.text_lg, a.font_heavy]}> + <Trans>Trending</Trans> + </Text> + <View style={[a.py_xs, a.px_sm, a.rounded_sm, a.overflow_hidden]}> + <GradientFill gradient={tokens.gradients.primary} /> + <Text style={[a.text_sm, a.font_heavy, {color: 'white'}]}> + <Trans>BETA</Trans> + </Text> + </View> + </View> + + <Button + label={_(msg`Hide trending topics`)} + size="tiny" + variant="outline" + color="secondary" + shape="round" + onPress={() => trendingPrompt.open()}> + <ButtonIcon icon={X} /> + </Button> + </View> + + <View style={[a.flex_row, a.flex_wrap, {rowGap: 8, columnGap: 6}]}> + {isLoading ? ( + Array(TRENDING_TOPICS_COUNT) + .fill(0) + .map((_n, i) => <TrendingTopicSkeleton key={i} index={i} />) + ) : !trending?.topics ? null : ( + <> + {trending.topics.map(topic => ( + <TrendingTopicLink key={topic.link} topic={topic}> + {({hovered}) => ( + <TrendingTopic + topic={topic} + style={[ + hovered && [ + t.atoms.border_contrast_high, + t.atoms.bg_contrast_25, + ], + ]} + /> + )} + </TrendingTopicLink> + ))} + </> + )} + </View> + + <Prompt.Basic + control={trendingPrompt} + title={_(msg`Hide trending topics?`)} + description={_(msg`You can update this later from your settings.`)} + confirmButtonCta={_(msg`Hide`)} + onConfirm={() => setTrendingDisabled(true)} + /> + </View> + ) +} |