about summary refs log tree commit diff
path: root/src/components
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2024-12-17 21:45:39 -0600
committerGitHub <noreply@github.com>2024-12-17 19:45:39 -0800
commita2019aceec001e276272832b97ea5e2ec864c8a5 (patch)
treeeaddab8a7a009650d93bb3b49c750619d98bb44d /src/components
parenta07949ec8e63bae178a829f65c33fcd9622b28ec (diff)
downloadvoidsky-a2019aceec001e276272832b97ea5e2ec864c8a5.tar.zst
Trending (Beta) (#7144)
* Add WIP UIs for trending topics and suggested starterpacks

* Disable SPs for now

* Improve explore treatment a bit, add some polish to cards

* Add tiny option in RightNav

* Add persisted option to hide trending from sidebar

* Add to settings, abstract state, not updating in tab

* Fix up hide/show toggle state, WITH broadcast hacK

* Clean up persisted code, add new setting

* Add new interstitial to Discover

* Exploration

* First hack at mute words

* Wire up interstitial and Explore page

* Align components

* Some skeleton UI

* Handle service config, enablement, load states, update lex contract

* Centralize mute word handling

* Stale time to 30m

* Cache enabled value for reloads, use real data for service config

* Remove broadcast hack

* Remove titleChild

* Gate settings too

* Update package, rm langs

* Add feature gate

* Only english during beta period

* Hook up real data

* Tweak config

* Straight passthrough links

* Hook up prod agent

* Fix no-show logic

* Up config query to 5 min

* Remove old file

* Remove comment

* Remove stray flex_1

* Make trending setting global

* Quick placeholder state

* Limit # in sidebar, tweak spacing

* Tweak gaps

* Handle hide/show of sidebar

* Simplify messages

* Remove interstitial

* Revert "Remove interstitial"

This reverts commit 1358ad47fdf7e633749340c410933b508af46c10.

* Only show interstitial on mobile

* Fix gap

* Add explore page recommendations

* [topics] add topic screen (#7149)

* add topic screen

* decode

* fix search query

* decode

* add server route

* Fix potential bad destructure (undefined)

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
Co-authored-by: Hailey <me@haileyok.com>
Diffstat (limited to 'src/components')
-rw-r--r--src/components/GradientFill.tsx8
-rw-r--r--src/components/TrendingTopics.tsx223
-rw-r--r--src/components/interstitials/Trending.tsx111
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>
+  )
+}