about summary refs log tree commit diff
path: root/src/screens/Search/modules/ExploreTrendingTopics.tsx
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2025-04-03 03:21:15 +0300
committerGitHub <noreply@github.com>2025-04-02 17:21:15 -0700
commit87da619aaa92e0ec762e68c13b24e58a25da10a8 (patch)
tree4da902d3ca43a226f6da8e5c090ab33c2df3297a /src/screens/Search/modules/ExploreTrendingTopics.tsx
parent8d1f97b5ffac5d86762f1d4e9384ff3097acbc52 (diff)
downloadvoidsky-87da619aaa92e0ec762e68c13b24e58a25da10a8.tar.zst
[Explore] Base (#8053)
* migrate to #/screens

* rm unneeded import

* block drawer gesture on recent profiles

* rm recommendations (#8056)

* [Explore] Disable Trending videos (#8054)

* remove giant header

* disable

* [Explore] Dynamic module ordering (#8066)

* Dynamic module ordering

* [Explore] New headers, metrics (#8067)

* new sticky headers

* improve spacing between modules

* view metric on modules

* update metrics names

* [Explore] Suggested accounts module (#8072)

* use modern profile card, update load more

* add tab bar

* tabbed suggested accounts

* [Explore] Discover feeds module (#8073)

* cap number of feeds to 3

* change feed pin button

* Apply suggestions from code review

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* restore statsig to log events

* filter out followed profiles, make suer enough are loaded (#8090)

* [Explore] Trending topics (#8055)

* redesigned trending topics

* rm borders on web

* get post count / age / ranking from api

* spacing tweaks

* fetch more topics then slice

* use api data for avis/category

* rm top border

* Integrate new SDK, part out components

* Clean up

* Use status field

* Bump SDK

* Send up interests and langs

---------

Co-authored-by: Eric Bailey <git@esb.lol>

* Clean up module spacing and borders

(cherry picked from commit 63d19b6c2d67e226e0e14709b1047a1f88b3ce1c)
(cherry picked from commit 62d7d394ab1dc31b40b9c2cf59075adbf94737a1)

* Switch back border ordering

(cherry picked from commit 34e3789f8b410132c1390df3c2bb8257630ebdd9)

* [Explore] Starter Packs (#8095)

* Temp WIP

(cherry picked from commit 43b5d7b1e64b3adb1ed162262d0310e0bf026c18)

* New SP card

* Load state

* Revert change

* Cleanup

* Interests and caching

* Count total

* Format

* Caching

* [Explore] Feed previews module (#8075)

* wip new hook

* get fetching working, maybe

* get feed previews rendering!

* fix header height

* working pin button

* extract out FeedLink

* add loader

* only make preview:header sticky

* Fix headers

* Header tweaks

* Fix moderation filter

* Fix threading

---------

Co-authored-by: Eric Bailey <git@esb.lol>

* Space it out

* Fix query key

* Mock new endpoint, filter saved feeds

* Make sure we're pinning, lower cache time

* add news category

* Remove log

* Improve suggested accounts load state

* Integrate new app view endpoint

* fragment

* Update src/screens/Search/modules/ExploreTrendingTopics.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update src/screens/Search/modules/ExploreTrendingTopics.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* lint

* maybe fix this

---------

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: Hailey <me@haileyok.com>
Diffstat (limited to 'src/screens/Search/modules/ExploreTrendingTopics.tsx')
-rw-r--r--src/screens/Search/modules/ExploreTrendingTopics.tsx278
1 files changed, 278 insertions, 0 deletions
diff --git a/src/screens/Search/modules/ExploreTrendingTopics.tsx b/src/screens/Search/modules/ExploreTrendingTopics.tsx
new file mode 100644
index 000000000..88d16b393
--- /dev/null
+++ b/src/screens/Search/modules/ExploreTrendingTopics.tsx
@@ -0,0 +1,278 @@
+import {Pressable, View} from 'react-native'
+import {type AppBskyUnspeccedDefs} from '@atproto/api'
+import {msg, plural, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {logger} from '#/logger'
+import {useTrendingSettings} from '#/state/preferences/trending'
+import {useGetTrendsQuery} from '#/state/queries/trending/useGetTrendsQuery'
+import {useTrendingConfig} from '#/state/trending-config'
+import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
+import {formatCount} from '#/view/com/util/numeric/format'
+import {atoms as a, useGutters, useTheme, type ViewStyleProp, web} from '#/alf'
+import {AvatarStack} from '#/components/AvatarStack'
+import {type Props as SVGIconProps} from '#/components/icons/common'
+import {Flame_Stroke2_Corner1_Rounded as FlameIcon} from '#/components/icons/Flame'
+import {Trending3_Stroke2_Corner1_Rounded as TrendingIcon} from '#/components/icons/Trending'
+import {Link} from '#/components/Link'
+import {Text} from '#/components/Typography'
+
+const TOPIC_COUNT = 5
+
+export function ExploreTrendingTopics() {
+  const {enabled} = useTrendingConfig()
+  const {trendingDisabled} = useTrendingSettings()
+  return enabled && !trendingDisabled ? <Inner /> : null
+}
+
+function Inner() {
+  const {data: trending, error, isLoading} = useGetTrendsQuery()
+  const noTopics = !isLoading && !error && !trending?.trends?.length
+
+  return isLoading ? (
+    Array.from({length: TOPIC_COUNT}).map((__, i) => (
+      <TrendingTopicRowSkeleton key={i} withPosts={i === 0} />
+    ))
+  ) : error || !trending?.trends || noTopics ? null : (
+    <>
+      {trending.trends.map((trend, index) => (
+        <TrendRow
+          key={trend.link}
+          trend={trend}
+          rank={index + 1}
+          onPress={() => {
+            logger.metric('trendingTopic:click', {context: 'explore'})
+          }}
+        />
+      ))}
+    </>
+  )
+}
+
+export function TrendRow({
+  trend,
+  rank,
+  children,
+  onPress,
+}: ViewStyleProp & {
+  trend: AppBskyUnspeccedDefs.TrendView
+  rank: number
+  children?: React.ReactNode
+  onPress?: () => void
+}) {
+  const t = useTheme()
+  const {_, i18n} = useLingui()
+  const gutters = useGutters([0, 'base'])
+
+  const category = useCategoryDisplayName(trend?.category || 'other')
+  const age = Math.floor(
+    (Date.now() - new Date(trend.startedAt || Date.now()).getTime()) /
+      (1000 * 60 * 60),
+  )
+  const badgeType = trend.status === 'hot' ? 'hot' : age < 2 ? 'new' : age
+  const postCount = trend.postCount
+    ? _(
+        plural(trend.postCount, {
+          other: `${formatCount(i18n, trend.postCount)} posts`,
+        }),
+      )
+    : null
+
+  return (
+    <Link
+      testID={trend.link}
+      label={_(msg`Browse topic ${trend.displayName}`)}
+      to={trend.link}
+      onPress={onPress}
+      style={[a.border_b, t.atoms.border_contrast_low]}
+      PressableComponent={Pressable}>
+      {({hovered, pressed}) => (
+        <>
+          <View
+            style={[
+              gutters,
+              a.w_full,
+              a.py_lg,
+              a.flex_row,
+              a.gap_2xs,
+              (hovered || pressed) && t.atoms.bg_contrast_25,
+            ]}>
+            <View style={[a.flex_1, a.gap_xs]}>
+              <View style={[a.flex_row]}>
+                <Text
+                  style={[a.text_md, a.font_bold, a.leading_snug, {width: 20}]}>
+                  <Trans comment='The trending topic rank, i.e. "1. March Madness", "2. The Bachelor"'>
+                    {rank}.
+                  </Trans>
+                </Text>
+                <Text
+                  style={[a.text_md, a.font_bold, a.leading_snug]}
+                  numberOfLines={1}>
+                  {trend.displayName}
+                </Text>
+              </View>
+              <View
+                style={[
+                  a.flex_row,
+                  a.gap_sm,
+                  a.align_center,
+                  {paddingLeft: 20},
+                ]}>
+                {trend.actors.length > 0 && (
+                  <AvatarStack size={20} profiles={trend.actors} />
+                )}
+                <Text
+                  style={[
+                    a.text_sm,
+                    t.atoms.text_contrast_medium,
+                    web(a.leading_snug),
+                  ]}
+                  numberOfLines={1}>
+                  {postCount}
+                  {postCount && category && <> &middot; </>}
+                  {category}
+                </Text>
+              </View>
+            </View>
+            <View style={[a.flex_shrink_0]}>
+              <TrendingIndicator type={badgeType} />
+            </View>
+          </View>
+
+          {children}
+        </>
+      )}
+    </Link>
+  )
+}
+
+type TrendingIndicatorType = 'hot' | 'new' | number
+
+function TrendingIndicator({type}: {type: TrendingIndicatorType | 'skeleton'}) {
+  const t = useTheme()
+  const {_} = useLingui()
+  const pillStyles = [
+    a.flex_row,
+    a.align_center,
+    a.gap_xs,
+    a.rounded_full,
+    a.px_sm,
+    {
+      height: 28,
+    },
+  ]
+
+  let Icon: React.ComponentType<SVGIconProps> | null = null
+  let text: string | null = null
+  let color: string | null = null
+  let backgroundColor: string | null = null
+
+  switch (type) {
+    case 'skeleton': {
+      return (
+        <View
+          style={[
+            pillStyles,
+            {backgroundColor: t.palette.contrast_25, width: 65, height: 28},
+          ]}
+        />
+      )
+    }
+    case 'hot': {
+      Icon = FlameIcon
+      color =
+        t.scheme === 'light' ? t.palette.negative_500 : t.palette.negative_950
+      backgroundColor =
+        t.scheme === 'light' ? t.palette.negative_50 : t.palette.negative_200
+      text = _(msg`Hot`)
+      break
+    }
+    case 'new': {
+      Icon = TrendingIcon
+      text = _(msg`New`)
+      color = t.palette.positive_700
+      backgroundColor = t.palette.positive_50
+      break
+    }
+    default: {
+      text = _(
+        msg({
+          message: `${type}h ago`,
+          comment:
+            'trending topic time spent trending. should be as short as possible to fit in a pill',
+        }),
+      )
+      color = t.atoms.text_contrast_medium.color
+      backgroundColor = t.atoms.bg_contrast_25.backgroundColor
+      break
+    }
+  }
+
+  return (
+    <View style={[pillStyles, {backgroundColor}]}>
+      {Icon && <Icon size="sm" style={{color}} />}
+      <Text style={[a.text_sm, {color}]}>{text}</Text>
+    </View>
+  )
+}
+
+function useCategoryDisplayName(
+  category: AppBskyUnspeccedDefs.TrendView['category'],
+) {
+  const {_} = useLingui()
+
+  switch (category) {
+    case 'sports':
+      return _(msg`Sports`)
+    case 'politics':
+      return _(msg`Politics`)
+    case 'video-games':
+      return _(msg`Video Games`)
+    case 'pop-culture':
+      return _(msg`Entertainment`)
+    case 'news':
+      return _(msg`News`)
+    case 'other':
+    default:
+      return null
+  }
+}
+
+export function TrendingTopicRowSkeleton({}: {withPosts: boolean}) {
+  const t = useTheme()
+  const gutters = useGutters([0, 'base'])
+
+  return (
+    <View
+      style={[
+        gutters,
+        a.w_full,
+        a.py_lg,
+        a.flex_row,
+        a.gap_2xs,
+        a.border_b,
+        t.atoms.border_contrast_low,
+      ]}>
+      <View style={[a.flex_1, a.gap_sm]}>
+        <View style={[a.flex_row, a.align_center]}>
+          <View style={[{width: 20}]}>
+            <LoadingPlaceholder
+              width={12}
+              height={12}
+              style={[a.rounded_full]}
+            />
+          </View>
+          <LoadingPlaceholder width={90} height={18} />
+        </View>
+        <View style={[a.flex_row, a.gap_sm, a.align_center, {paddingLeft: 20}]}>
+          <LoadingPlaceholder width={70} height={18} />
+          <LoadingPlaceholder width={40} height={18} />
+          <LoadingPlaceholder width={60} height={18} />
+        </View>
+      </View>
+      <View style={[a.flex_shrink_0]}>
+        <TrendingIndicator type="skeleton" />
+      </View>
+    </View>
+  )
+}