From de15f8e2d3264b9ffb18c3a089612ef604eb87b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Thu, 12 Dec 2024 01:31:29 +0900 Subject: [PATCH 01/58] feat(embed): Add support for dark mode (#6912) * feat(embed): Support dark mode (wip) * finishing up the implementation * fix tailwind color selector * tweak design * refactor: unify types * fix * fix english grammar * refactor: unify types * tweak design * remove the customization part --- bskyembed/snippet/embed.ts | 4 ++- bskyembed/src/color-mode.ts | 17 +++++++++++++ bskyembed/src/components/container.tsx | 2 +- bskyembed/src/components/embed.tsx | 34 +++++++++++++++----------- bskyembed/src/components/post.tsx | 18 ++++++++------ bskyembed/src/index.css | 4 +++ bskyembed/src/screens/landing.tsx | 31 +++++++++++++---------- bskyembed/src/screens/post.tsx | 9 ++++--- bskyembed/tailwind.config.cjs | 8 ++++++ bskyembed/tsconfig.json | 3 +-- 10 files changed, 88 insertions(+), 42 deletions(-) create mode 100644 bskyembed/src/color-mode.ts diff --git a/bskyembed/snippet/embed.ts b/bskyembed/snippet/embed.ts index 380cda5fb9..3c1b14b955 100644 --- a/bskyembed/snippet/embed.ts +++ b/bskyembed/snippet/embed.ts @@ -20,6 +20,7 @@ window.addEventListener('message', event => { return } + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const id = (event.data as {id: string}).id if (!id) { return @@ -33,6 +34,7 @@ window.addEventListener('message', event => { return } + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const height = (event.data as {height: number}).height if (height) { embed.style.height = `${height}px` @@ -47,7 +49,7 @@ window.addEventListener('message', event => { * @returns */ function scan(node = document) { - const embeds = node.querySelectorAll('[data-bluesky-uri]') + const embeds = node.querySelectorAll('[data-bluesky-uri]') for (let i = 0; i < embeds.length; i++) { const id = String(Math.random()).slice(2) diff --git a/bskyembed/src/color-mode.ts b/bskyembed/src/color-mode.ts new file mode 100644 index 0000000000..2b392c6178 --- /dev/null +++ b/bskyembed/src/color-mode.ts @@ -0,0 +1,17 @@ +export function applyTheme(theme: 'light' | 'dark') { + document.documentElement.classList.remove('light', 'dark') + document.documentElement.classList.add(theme) +} + +export function initColorMode() { + applyTheme( + window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light', + ) + window + .matchMedia('(prefers-color-scheme: dark)') + .addEventListener('change', mql => { + applyTheme(mql.matches ? 'dark' : 'light') + }) +} diff --git a/bskyembed/src/components/container.tsx b/bskyembed/src/components/container.tsx index 5b1b2b7fb4..8e142a25be 100644 --- a/bskyembed/src/components/container.tsx +++ b/bskyembed/src/components/container.tsx @@ -37,7 +37,7 @@ export function Container({ return (
{ if (ref.current && href) { // forwardRef requires preact/compat - let's keep it simple diff --git a/bskyembed/src/components/embed.tsx b/bskyembed/src/components/embed.tsx index 74eacf16d4..20ffcb2b29 100644 --- a/bskyembed/src/components/embed.tsx +++ b/bskyembed/src/components/embed.tsx @@ -78,9 +78,9 @@ export function Embed({ return ( + className="transition-colors hover:bg-neutral-100 dark:hover:bg-slate-700 border dark:border-slate-600 rounded-lg p-2 gap-1.5 w-full flex flex-col">
-
+

{record.author.displayName} - + @{record.author.handle}

@@ -209,7 +209,7 @@ function Info({children}: {children: ComponentChildren}) { return (
-

{children}

+

{children}

) } @@ -308,7 +308,7 @@ function ExternalEmbed({ return ( {content.external.thumb && ( )}
-

+

{toNiceDomain(content.external.uri)}

{content.external.title}

-

+

{content.external.description}

@@ -345,23 +345,29 @@ function GenericWithImageEmbed({ return ( + className="w-full rounded-lg border dark:border-slate-600 py-2 px-3 flex flex-col gap-2">
{image ? ( {title} ) : (
)}

{title}

-

{subtitle}

+

+ {subtitle} +

- {description &&

{description}

} + {description && ( +

+ {description} +

+ )} ) } @@ -406,7 +412,7 @@ function StarterPackEmbed({ return ( + className="w-full rounded-lg overflow-hidden border dark:border-slate-600 flex flex-col items-stretch">
@@ -415,7 +421,7 @@ function StarterPackEmbed({

{content.record.name}

-

+

Starter pack by{' '} {content.creator.displayName || `@${content.creator.handle}`}

@@ -425,7 +431,7 @@ function StarterPackEmbed({

{content.record.description}

)} {!!content.joinedAllTimeCount && content.joinedAllTimeCount > 50 && ( -

+

{content.joinedAllTimeCount} users have joined!

)} diff --git a/bskyembed/src/components/post.tsx b/bskyembed/src/components/post.tsx index 4db5eeb45e..26945eb69d 100644 --- a/bskyembed/src/components/post.tsx +++ b/bskyembed/src/components/post.tsx @@ -38,7 +38,7 @@ export function Post({thread}: Props) {
-
+
+ className="text-[15px] text-textLight dark:text-textDimmed hover:underline line-clamp-1">

@{post.author.handle}

@@ -69,15 +69,15 @@ export function Post({thread}: Props) { -
+
{!!post.likeCount && (
-

+

{prettyNumber(post.likeCount)}

@@ -85,17 +85,19 @@ export function Post({thread}: Props) { {!!post.repostCount && (
-

+

{prettyNumber(post.repostCount)}

)}
-

Reply

+

+ Reply +

-

+

{post.replyCount ? `Read ${prettyNumber(post.replyCount)} ${ post.replyCount > 1 ? 'replies' : 'reply' diff --git a/bskyembed/src/index.css b/bskyembed/src/index.css index 22b2b8be5c..289e34cf00 100644 --- a/bskyembed/src/index.css +++ b/bskyembed/src/index.css @@ -5,3 +5,7 @@ .break-word { word-break: break-word; } + +:root { + color-scheme: light dark; +} diff --git a/bskyembed/src/screens/landing.tsx b/bskyembed/src/screens/landing.tsx index a9e08cd3f2..a3448e90ac 100644 --- a/bskyembed/src/screens/landing.tsx +++ b/bskyembed/src/screens/landing.tsx @@ -6,6 +6,7 @@ import {useEffect, useMemo, useRef, useState} from 'preact/hooks' import arrowBottom from '../../assets/arrowBottom_stroke2_corner0_rounded.svg' import logo from '../../assets/logo.svg' +import {initColorMode} from '../color-mode' import {Container} from '../components/container' import {Link} from '../components/link' import {Post} from '../components/post' @@ -21,6 +22,8 @@ export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js` const root = document.getElementById('app') if (!root) throw new Error('No root element') +initColorMode() + const agent = new BskyAgent({ service: 'https://public.api.bsky.app', }) @@ -108,7 +111,7 @@ function LandingPage() { }, [uri]) return ( -

+
@@ -121,20 +124,22 @@ function LandingPage() { type="text" value={uri} onInput={e => setUri(e.currentTarget.value)} - className="border rounded-lg py-3 w-full max-w-[600px] px-4" + className="border rounded-lg py-3 w-full max-w-[600px] px-4 dark:bg-dimmedBg dark:border-slate-500" placeholder={DEFAULT_POST} /> - + {loading ? ( - +
+ +
) : (
{!error && thread && uri && } {!error && thread && } {error && ( -
+

{error}

)} @@ -149,15 +154,15 @@ function Skeleton() {
-
+
-
-
+
+
-
-
-
+
+
+
) @@ -220,7 +225,7 @@ function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) { ref={ref} type="text" value={snippet} - className="border rounded-lg py-3 w-full px-4" + className="border rounded-lg py-3 w-full px-4 dark:bg-dimmedBg dark:border-slate-500" readOnly autoFocus onFocus={() => { @@ -228,7 +233,7 @@ function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) { }} /> diff --git a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx index ca0cb1e626..1725c4aaca 100644 --- a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx +++ b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx @@ -291,10 +291,12 @@ let ProfileHeaderLabeler = ({ }, }} size="tiny" - label={plural(likeCount, { - one: 'Liked by # user', - other: 'Liked by # users', - })}> + label={_( + msg`Liked by ${plural(likeCount, { + one: '# user', + other: '# users', + })}`, + )}> {({hovered, focused, pressed}) => ( - + + Liked by{' '} + + )} diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx index 707aad7fb0..a591488891 100644 --- a/src/view/com/feeds/FeedSourceCard.tsx +++ b/src/view/com/feeds/FeedSourceCard.tsx @@ -300,11 +300,14 @@ export function FeedSourceCardLoaded({ {showLikes && feed.type === 'feed' ? ( - + + Liked by{' '} + + ) : null} diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index deb4b51d82..39caaf0987 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -258,10 +258,12 @@ let PostCtrls = ({ } }} accessibilityRole="button" - accessibilityLabel={plural(post.replyCount || 0, { - one: 'Reply (# reply)', - other: 'Reply (# replies)', - })} + accessibilityLabel={_( + msg`Reply (${plural(post.replyCount || 0, { + one: '# reply', + other: '# replies', + })})`, + )} accessibilityHint="" hitSlop={POST_CTRL_HITSLOP}> diff --git a/src/view/com/util/post-ctrls/RepostButton.tsx b/src/view/com/util/post-ctrls/RepostButton.tsx index 06b1fcaf6b..ca1647a991 100644 --- a/src/view/com/util/post-ctrls/RepostButton.tsx +++ b/src/view/com/util/post-ctrls/RepostButton.tsx @@ -62,11 +62,21 @@ let RepostButton = ({ {padding: 5}, ]} hoverStyle={t.atoms.bg_contrast_25} - label={`${ + label={ isReposted - ? _(msg`Undo repost`) - : _(msg({message: 'Repost', context: 'action'})) - } (${plural(repostCount || 0, {one: '# repost', other: '# reposts'})})`} + ? _( + msg`Undo repost (${plural(repostCount || 0, { + one: '# repost', + other: '# reposts', + })})`, + ) + : _( + msg`Repost (${plural(repostCount || 0, { + one: '# repost', + other: '# reposts', + })})`, + ) + } shape="round" variant="ghost" color="secondary" diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx index dd2f8f12b5..c3f98c067f 100644 --- a/src/view/screens/ProfileFeed.tsx +++ b/src/view/screens/ProfileFeed.tsx @@ -587,11 +587,9 @@ function AboutSection({ label={_(msg`View users who like this feed`)} to={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')} style={[t.atoms.text_contrast_medium, a.font_bold]}> - + + Liked by + )} From c8745a9ab0a99ac3cf484e00760e72d1e9f28dfc Mon Sep 17 00:00:00 2001 From: dan Date: Thu, 12 Dec 2024 04:20:22 +0000 Subject: [PATCH 10/58] Cache Reanimated worklets (#7068) * Undo perf hackfix * Bump Reanimated to include https://github.com/software-mansion/react-native-reanimated/pull/6758 * Bump to 3.17.0-nightly-20241211-17e89ca24 --- package.json | 2 +- src/view/com/util/MainScrollProvider.tsx | 25 ++++++------------------ yarn.lock | 8 ++++---- 3 files changed, 11 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 4c7c179693..79735a1936 100644 --- a/package.json +++ b/package.json @@ -181,7 +181,7 @@ "react-native-picker-select": "^9.3.1", "react-native-progress": "bluesky-social/react-native-progress", "react-native-qrcode-styled": "^0.3.3", - "react-native-reanimated": "^3.16.3", + "react-native-reanimated": "3.17.0-nightly-20241211-17e89ca24", "react-native-root-siblings": "^4.1.1", "react-native-safe-area-context": "4.14.0", "react-native-screens": "~4.3.0", diff --git a/src/view/com/util/MainScrollProvider.tsx b/src/view/com/util/MainScrollProvider.tsx index c1293a83c0..e2538eaab6 100644 --- a/src/view/com/util/MainScrollProvider.tsx +++ b/src/view/com/util/MainScrollProvider.tsx @@ -1,11 +1,6 @@ import React, {useCallback, useEffect} from 'react' import {NativeScrollEvent} from 'react-native' -import { - interpolate, - makeMutable, - useSharedValue, - withSpring, -} from 'react-native-reanimated' +import {interpolate, useSharedValue, withSpring} from 'react-native-reanimated' import EventEmitter from 'eventemitter3' import {ScrollProvider} from '#/lib/ScrollContext' @@ -20,18 +15,6 @@ function clamp(num: number, min: number, max: number) { return Math.min(Math.max(num, min), max) } -const V0 = makeMutable( - withSpring(0, { - overshootClamping: true, - }), -) - -const V1 = makeMutable( - withSpring(1, { - overshootClamping: true, - }), -) - export function MainScrollProvider({children}: {children: React.ReactNode}) { const {headerHeight} = useShellLayout() const {headerMode} = useMinimalShellMode() @@ -42,7 +25,11 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) { const setMode = React.useCallback( (v: boolean) => { 'worklet' - headerMode.set(v ? V1.get() : V0.get()) + headerMode.set(() => + withSpring(v ? 1 : 0, { + overshootClamping: true, + }), + ) }, [headerMode], ) diff --git a/yarn.lock b/yarn.lock index 21fdb48c60..03ffcfcfd6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16047,10 +16047,10 @@ react-native-qrcode-styled@^0.3.3: qrcode "^1.5.4" react-fast-compare "^3.2.2" -react-native-reanimated@^3.16.3: - version "3.16.3" - resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.16.3.tgz#3b559dca49e9e40abcf5de834dc27fc05f856b66" - integrity sha512-OWlA6e1oHhytTpc7WiSZ7Tmb8OYwLKYZz29Sz6d6WAg60Hm5GuAiKIWUG7Ako7FLcYhFkA0pEQ2xPMEYUo9vlw== +react-native-reanimated@3.17.0-nightly-20241211-17e89ca24: + version "3.17.0-nightly-20241211-17e89ca24" + resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.17.0-nightly-20241211-17e89ca24.tgz#af0c36e278646eb2f79e28ad0047cfd80d0e29f5" + integrity sha512-5p7jr0DrnID1puOzMel3VZVRw5Hl/UdMUvPCI1sEG9IA2mUaWrgeoojS2wVwW1U0Pj6HXjPNEimDSXZneZKNuQ== dependencies: "@babel/plugin-transform-arrow-functions" "^7.0.0-0" "@babel/plugin-transform-class-properties" "^7.0.0-0" From 20ac42a5a324ad553324d15ad868a1fea1868641 Mon Sep 17 00:00:00 2001 From: Snow Pettersen Date: Wed, 11 Dec 2024 20:28:15 -0800 Subject: [PATCH 11/58] configure the bitdrift Capture SDK (#6979) * configure bitdrift Capture SDK * remove console log * use the new url * add patch to fix devtool interaction * update patches * fix patch * add BITDRIFT_API_KEY to .env.example --- .env.example | 1 + app.config.js | 1 + docs/build.md | 6 ++++++ package.json | 1 + patches/expo-modules-core+2.1.1.md | 3 +++ patches/expo-modules-core+2.1.1.patch | 15 +++++++++++++++ src/App.native.tsx | 1 + src/lib/bitdrift.ts | 7 +++++++ src/logger/index.ts | 24 ++++++++++++++++++++++++ yarn.lock | 5 +++++ 10 files changed, 64 insertions(+) create mode 100644 patches/expo-modules-core+2.1.1.md create mode 100644 patches/expo-modules-core+2.1.1.patch create mode 100644 src/lib/bitdrift.ts diff --git a/.env.example b/.env.example index 6ab02256e4..979589f58b 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ # Copy this to `.env` and `.env.test` files +BITDRIFT_API_KEY= SENTRY_AUTH_TOKEN= EXPO_PUBLIC_ENV=development EXPO_PUBLIC_LOG_LEVEL=debug diff --git a/app.config.js b/app.config.js index 8b288e1a73..bc283152bd 100644 --- a/app.config.js +++ b/app.config.js @@ -222,6 +222,7 @@ module.exports = function (config) { }, ], 'react-native-compressor', + '@bitdrift/react-native', './plugins/starterPackAppClipExtension/withStarterPackAppClip.js', './plugins/withAndroidManifestPlugin.js', './plugins/withAndroidManifestFCMIconPlugin.js', diff --git a/docs/build.md b/docs/build.md index 6661d8fb21..7a29ab3553 100644 --- a/docs/build.md +++ b/docs/build.md @@ -87,6 +87,12 @@ However, if you're a part of the Bluesky team and want to enable Sentry, fill in If you change `SENTRY_AUTH_TOKEN`, you need to do `yarn prebuild` before running `yarn ios` or `yarn android` again. +### Adding bitdrift + +Adding bitdirft is NOT required. You can keep `BITDRIFT_API_KEY=` in `.env` which will avoid initializing bitdrift during startup. + +However, if you're a part of the Bluesky team and want to enable bitdrift, fill in `BITDRIFT_API_KEY` in your `.env` to enable bitdrift. + ### Adding and Updating Locales - `yarn intl:build` -> you will also need to run this anytime `./src/locale/{locale}/messages.po` change diff --git a/package.json b/package.json index 79735a1936..bbe46aa7c3 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ }, "dependencies": { "@atproto/api": "^0.13.18", + "@bitdrift/react-native": "0.4.0", "@braintree/sanitize-url": "^6.0.2", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", "@emoji-mart/react": "^1.1.1", diff --git a/patches/expo-modules-core+2.1.1.md b/patches/expo-modules-core+2.1.1.md new file mode 100644 index 0000000000..a71324c19e --- /dev/null +++ b/patches/expo-modules-core+2.1.1.md @@ -0,0 +1,3 @@ +## expo-modules-core Patch + +This patch fixes an issue where bitdrift's API stream gets blocked by the Expo interceptor used to power the devtools diff --git a/patches/expo-modules-core+2.1.1.patch b/patches/expo-modules-core+2.1.1.patch new file mode 100644 index 0000000000..f3d9bfd149 --- /dev/null +++ b/patches/expo-modules-core+2.1.1.patch @@ -0,0 +1,15 @@ +diff --git a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt +index 47c4d15..afe138d 100644 +--- a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt ++++ b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt +@@ -125,6 +125,10 @@ internal fun peekResponseBody( + } + + internal fun shouldParseBody(response: Response): Boolean { ++ if (response.request.url.encodedPath == "/bitdrift_public.protobuf.client.v1.ApiService/Mux") { ++ return false ++ } ++ + // Check for Content-Type + val skipContentTypes = listOf( + "text/event-stream", // Server Sent Events diff --git a/src/App.native.tsx b/src/App.native.tsx index 0d8d7a92e6..bc38eec799 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -1,5 +1,6 @@ import 'react-native-url-polyfill/auto' import '#/lib/sentry' // must be near top +import '#/lib/bitdrift' // must be near top import '#/view/icons' import React, {useEffect, useState} from 'react' diff --git a/src/lib/bitdrift.ts b/src/lib/bitdrift.ts new file mode 100644 index 0000000000..2b22155e78 --- /dev/null +++ b/src/lib/bitdrift.ts @@ -0,0 +1,7 @@ +import {init} from '@bitdrift/react-native' + +const BITDRIFT_API_KEY = process.env.BITDRIFT_API_KEY + +if (BITDRIFT_API_KEY) { + init(BITDRIFT_API_KEY, {url: 'https://api-bsky.bitdrift.io'}) +} diff --git a/src/logger/index.ts b/src/logger/index.ts index 7bd812af00..e4adc48e28 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -1,3 +1,9 @@ +import { + debug as bdDebug, + error as bdError, + info as bdInfo, + warn as bdWarn, +} from '@bitdrift/react-native' import format from 'date-fns/format' import {nanoid} from 'nanoid/non-secure' @@ -131,6 +137,20 @@ export const consoleTransport: Transport = ( } } +export const bitdriftTransport: Transport = (level, message) => { + const log = ( + { + [LogLevel.Debug]: bdDebug, + [LogLevel.Info]: bdInfo, + [LogLevel.Log]: bdInfo, + [LogLevel.Warn]: bdWarn, + [LogLevel.Error]: bdError, + } as const + )[level] + + log(message.toString()) +} + export const sentryTransport: Transport = ( level, message, @@ -328,6 +348,10 @@ export class Logger { */ export const logger = new Logger() +if (!env.IS_TEST) { + logger.addTransport(bitdriftTransport) +} + if (env.IS_DEV && !env.IS_TEST) { logger.addTransport(consoleTransport) diff --git a/yarn.lock b/yarn.lock index 03ffcfcfd6..16fe9579a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3377,6 +3377,11 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@bitdrift/react-native@0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@bitdrift/react-native/-/react-native-0.4.0.tgz#e6484343ef04824aa924df2a757bd9620b2106c1" + integrity sha512-KuYzWEkoGwjjP0ZurjHwV+zfRZjQXxbXa3zhijWv0iqzMI/7kbrBd9lm+wNQo8OrkqFVDlebCb8AGPc0jMZw7A== + "@braintree/sanitize-url@^6.0.2": version "6.0.4" resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz#923ca57e173c6b232bbbb07347b1be982f03e783" From 704e36c2801c4c06a3763eaef90c6a3e532a326d Mon Sep 17 00:00:00 2001 From: dan Date: Thu, 12 Dec 2024 05:00:09 +0000 Subject: [PATCH 12/58] Fix web build (#7069) * Fix web build * Fix circular deps * Hoist log functions --- src/logger/bitdriftTransport.ts | 23 +++++++ src/logger/bitdriftTransport.web.ts | 7 +++ src/logger/index.ts | 94 ++--------------------------- src/logger/types.ts | 69 +++++++++++++++++++++ 4 files changed, 105 insertions(+), 88 deletions(-) create mode 100644 src/logger/bitdriftTransport.ts create mode 100644 src/logger/bitdriftTransport.web.ts create mode 100644 src/logger/types.ts diff --git a/src/logger/bitdriftTransport.ts b/src/logger/bitdriftTransport.ts new file mode 100644 index 0000000000..c2235e0d4b --- /dev/null +++ b/src/logger/bitdriftTransport.ts @@ -0,0 +1,23 @@ +import { + debug as bdDebug, + error as bdError, + info as bdInfo, + warn as bdWarn, +} from '@bitdrift/react-native' + +import {LogLevel, Transport} from './types' + +export function createBitdriftTransport(): Transport { + const logFunctions = { + [LogLevel.Debug]: bdDebug, + [LogLevel.Info]: bdInfo, + [LogLevel.Log]: bdInfo, + [LogLevel.Warn]: bdWarn, + [LogLevel.Error]: bdError, + } as const + + return (level, message) => { + const log = logFunctions[level] + log(message.toString()) + } +} diff --git a/src/logger/bitdriftTransport.web.ts b/src/logger/bitdriftTransport.web.ts new file mode 100644 index 0000000000..ecea3f6f3b --- /dev/null +++ b/src/logger/bitdriftTransport.web.ts @@ -0,0 +1,7 @@ +import {Transport} from './index' + +export function createBitdriftTransport(): Transport { + return (_level, _message) => { + // noop + } +} diff --git a/src/logger/index.ts b/src/logger/index.ts index e4adc48e28..02e5d5f257 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -1,9 +1,3 @@ -import { - debug as bdDebug, - error as bdError, - info as bdInfo, - warn as bdWarn, -} from '@bitdrift/react-native' import format from 'date-fns/format' import {nanoid} from 'nanoid/non-secure' @@ -12,74 +6,12 @@ import {DebugContext} from '#/logger/debugContext' import {add} from '#/logger/logDump' import {Sentry} from '#/logger/sentry' import * as env from '#/env' +import {createBitdriftTransport} from './bitdriftTransport' +import {Metadata} from './types' +import {ConsoleTransportEntry, LogLevel, Transport} from './types' -export enum LogLevel { - Debug = 'debug', - Info = 'info', - Log = 'log', - Warn = 'warn', - Error = 'error', -} - -type Transport = ( - level: LogLevel, - message: string | Error, - metadata: Metadata, - timestamp: number, -) => void - -/** - * A union of some of Sentry's breadcrumb properties as well as Sentry's - * `captureException` parameter, `CaptureContext`. - */ -type Metadata = { - /** - * Applied as Sentry breadcrumb types. Defaults to `default`. - * - * @see https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/#breadcrumb-types - */ - type?: - | 'default' - | 'debug' - | 'error' - | 'navigation' - | 'http' - | 'info' - | 'query' - | 'transaction' - | 'ui' - | 'user' - - /** - * Passed through to `Sentry.captureException` - * - * @see https://github.com/getsentry/sentry-javascript/blob/903addf9a1a1534a6cb2ba3143654b918a86f6dd/packages/types/src/misc.ts#L65 - */ - tags?: { - [key: string]: - | number - | string - | boolean - | bigint - | symbol - | null - | undefined - } - - /** - * Any additional data, passed through to Sentry as `extra` param on - * exceptions, or the `data` param on breadcrumbs. - */ - [key: string]: unknown -} & Parameters[1] - -export type ConsoleTransportEntry = { - id: string - timestamp: number - level: LogLevel - message: string | Error - metadata: Metadata -} +export {LogLevel} +export type {ConsoleTransportEntry, Transport} const enabledLogLevels: { [key in LogLevel]: LogLevel[] @@ -137,20 +69,6 @@ export const consoleTransport: Transport = ( } } -export const bitdriftTransport: Transport = (level, message) => { - const log = ( - { - [LogLevel.Debug]: bdDebug, - [LogLevel.Info]: bdInfo, - [LogLevel.Log]: bdInfo, - [LogLevel.Warn]: bdWarn, - [LogLevel.Error]: bdError, - } as const - )[level] - - log(message.toString()) -} - export const sentryTransport: Transport = ( level, message, @@ -349,7 +267,7 @@ export class Logger { export const logger = new Logger() if (!env.IS_TEST) { - logger.addTransport(bitdriftTransport) + logger.addTransport(createBitdriftTransport()) } if (env.IS_DEV && !env.IS_TEST) { diff --git a/src/logger/types.ts b/src/logger/types.ts new file mode 100644 index 0000000000..252e7373be --- /dev/null +++ b/src/logger/types.ts @@ -0,0 +1,69 @@ +import type {Sentry} from '#/logger/sentry' + +export enum LogLevel { + Debug = 'debug', + Info = 'info', + Log = 'log', + Warn = 'warn', + Error = 'error', +} + +export type Transport = ( + level: LogLevel, + message: string | Error, + metadata: Metadata, + timestamp: number, +) => void + +/** + * A union of some of Sentry's breadcrumb properties as well as Sentry's + * `captureException` parameter, `CaptureContext`. + */ +export type Metadata = { + /** + * Applied as Sentry breadcrumb types. Defaults to `default`. + * + * @see https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/#breadcrumb-types + */ + type?: + | 'default' + | 'debug' + | 'error' + | 'navigation' + | 'http' + | 'info' + | 'query' + | 'transaction' + | 'ui' + | 'user' + + /** + * Passed through to `Sentry.captureException` + * + * @see https://github.com/getsentry/sentry-javascript/blob/903addf9a1a1534a6cb2ba3143654b918a86f6dd/packages/types/src/misc.ts#L65 + */ + tags?: { + [key: string]: + | number + | string + | boolean + | bigint + | symbol + | null + | undefined + } + + /** + * Any additional data, passed through to Sentry as `extra` param on + * exceptions, or the `data` param on breadcrumbs. + */ + [key: string]: unknown +} & Parameters[1] + +export type ConsoleTransportEntry = { + id: string + timestamp: number + level: LogLevel + message: string | Error + metadata: Metadata +} From 88166926fa64c7dd7924a3a8100056175c5f989b Mon Sep 17 00:00:00 2001 From: dan Date: Thu, 12 Dec 2024 15:35:56 +0000 Subject: [PATCH 13/58] Make the pager take full width (#7066) * Wide tabs for web * Wide tabs on mobile * Tweak min for profile * Driveby border fix * Fix single tab indicator --- src/screens/Hashtag.tsx | 2 +- src/view/com/pager/TabBar.tsx | 58 +++++++++++++++++++++++++------ src/view/com/pager/TabBar.web.tsx | 21 ++++++++++- 3 files changed, 69 insertions(+), 12 deletions(-) diff --git a/src/screens/Hashtag.tsx b/src/screens/Hashtag.tsx index a87487150c..3e98f364bf 100644 --- a/src/screens/Hashtag.tsx +++ b/src/screens/Hashtag.tsx @@ -107,7 +107,7 @@ export default function HashtagScreen({ return ( - + {headerTitle} diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx index b08b364c52..b257559bcb 100644 --- a/src/view/com/pager/TabBar.tsx +++ b/src/view/com/pager/TabBar.tsx @@ -51,6 +51,7 @@ export function TabBar({ const containerSize = useSharedValue(0) const scrollX = useSharedValue(0) const layouts = useSharedValue<{x: number; width: number}[]>([]) + const textLayouts = useSharedValue<{width: number}[]>([]) const itemsLength = items.length const scrollToOffsetJS = useCallback( @@ -211,21 +212,40 @@ export function TabBar({ [layouts], ) + const onTextLayout = useCallback( + (i: number, layout: {width: number}) => { + 'worklet' + textLayouts.modify(ls => { + ls[i] = layout + return ls + }) + }, + [textLayouts], + ) + const indicatorStyle = useAnimatedStyle(() => { if (!_WORKLET) { return {opacity: 0} } const layoutsValue = layouts.get() + const textLayoutsValue = textLayouts.get() if ( layoutsValue.length !== itemsLength || - layoutsValue.some(l => l === undefined) + textLayoutsValue.length !== itemsLength ) { return { opacity: 0, } } - if (layoutsValue.length === 1) { - return {opacity: 1} + if (textLayoutsValue.length === 1) { + return { + opacity: 1, + transform: [ + { + scaleX: textLayoutsValue[0].width / contentSize.get(), + }, + ], + } } return { opacity: 1, @@ -240,10 +260,8 @@ export function TabBar({ { scaleX: interpolate( dragProgress.get(), - layoutsValue.map((l, i) => i), - layoutsValue.map( - l => (l.width - ITEM_PADDING * 2) / contentSize.get(), - ), + textLayoutsValue.map((l, i) => i), + textLayoutsValue.map(l => l.width / contentSize.get()), ), }, ], @@ -287,7 +305,7 @@ export function TabBar({ onLayout={e => { contentSize.set(e.nativeEvent.layout.width) }} - style={{flexDirection: 'row'}}> + style={{flexDirection: 'row', flexGrow: 1}}> {items.map((item, i) => { return ( ) })} @@ -328,6 +347,7 @@ function TabBarItem({ item, onPressItem, onItemLayout, + onTextLayout, }: { index: number testID: string | undefined @@ -335,6 +355,7 @@ function TabBarItem({ item: string onPressItem: (index: number) => void onItemLayout: (index: number, layout: {x: number; width: number}) => void + onTextLayout: (index: number, layout: {width: number}) => void }) { const t = useTheme() const style = useAnimatedStyle(() => { @@ -358,8 +379,15 @@ function TabBarItem({ [index, onItemLayout], ) + const handleTextLayout = useCallback( + (e: LayoutChangeEvent) => { + runOnUI(onTextLayout)(index, e.nativeEvent.layout) + }, + [index, onTextLayout], + ) + return ( - + + style={[styles.itemText, t.atoms.text, a.text_md, a.font_bold]} + onLayout={handleTextLayout}> {item} @@ -381,19 +410,28 @@ function TabBarItem({ const styles = StyleSheet.create({ contentContainer: { + flexGrow: 1, backgroundColor: 'transparent', paddingHorizontal: CONTENT_PADDING, }, item: { + flexGrow: 1, paddingTop: 10, paddingHorizontal: ITEM_PADDING, justifyContent: 'center', }, itemInner: { + alignItems: 'center', + flexGrow: 1, paddingBottom: 10, borderBottomWidth: 3, borderBottomColor: 'transparent', }, + itemText: { + lineHeight: 20, + minWidth: 45, + textAlign: 'center', + }, outerBottomBorder: { position: 'absolute', left: 0, diff --git a/src/view/com/pager/TabBar.web.tsx b/src/view/com/pager/TabBar.web.tsx index 789f88e753..f44e03368e 100644 --- a/src/view/com/pager/TabBar.web.tsx +++ b/src/view/com/pager/TabBar.web.tsx @@ -115,12 +115,14 @@ export function TabBar({ hoverStyle={t.atoms.bg_contrast_25} onPress={() => onPressItem(i)} accessibilityRole="tab"> - + Date: Thu, 12 Dec 2024 17:21:28 +0000 Subject: [PATCH 14/58] followersCount -> followsCount (#7080) --- src/screens/Profile/ProfileFollows.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/screens/Profile/ProfileFollows.tsx b/src/screens/Profile/ProfileFollows.tsx index 97a05d5cfd..85ebccf30b 100644 --- a/src/screens/Profile/ProfileFollows.tsx +++ b/src/screens/Profile/ProfileFollows.tsx @@ -38,7 +38,7 @@ export const ProfileFollowsScreen = ({route}: Props) => { From f8cdd6b9ae46a9a4efe0be87e55ee1debded4f91 Mon Sep 17 00:00:00 2001 From: dan Date: Thu, 12 Dec 2024 17:37:07 +0000 Subject: [PATCH 15/58] [Notifications] Add a Mentions tab (#7044) * Split out NotificationsTab * Remove unused route parameter * Refine the split between components * Hoist some logic out of NotificationFeed * Remove unused option * Add all|conversations to query, hardcode "all" * Add a Conversations tab * Rename to Mentions * Bump packages * Rename fields * Fix oopsie * Simplify header * Track active tab * Fix types * Separate logic for tabs * Better border for first unread * Highlight unread for all only * Fix spinner races * Fix fetchPage races * Fix bottom bar border being obscured by glimmer * Remember last tab within the session * One tab at a time * Fix TS * Handle all RQKEY usages * Nit --- package.json | 4 +- src/lib/hooks/useNotificationHandler.ts | 15 +- src/lib/routes/types.ts | 6 +- src/screens/Settings/NotificationSettings.tsx | 8 +- src/state/queries/notifications/feed.ts | 34 ++- src/state/queries/notifications/settings.ts | 9 +- src/state/queries/notifications/unread.tsx | 16 +- src/state/queries/notifications/util.ts | 5 +- src/state/queries/util.ts | 4 +- .../com/notifications/NotificationFeed.tsx | 33 +-- .../notifications/NotificationFeedItem.tsx | 13 +- src/view/com/pager/Pager.tsx | 14 +- src/view/screens/DebugMod.tsx | 8 +- src/view/screens/Notifications.tsx | 256 ++++++++++++------ yarn.lock | 251 ++++++++--------- 15 files changed, 409 insertions(+), 267 deletions(-) diff --git a/package.json b/package.json index bbe46aa7c3..23e2bb324a 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "icons:optimize": "svgo -f ./assets/icons" }, "dependencies": { - "@atproto/api": "^0.13.18", + "@atproto/api": "^0.13.20", "@bitdrift/react-native": "0.4.0", "@braintree/sanitize-url": "^6.0.2", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", @@ -206,7 +206,7 @@ "zod": "^3.20.2" }, "devDependencies": { - "@atproto/dev-env": "^0.3.64", + "@atproto/dev-env": "^0.3.67", "@babel/core": "^7.26.0", "@babel/preset-env": "^7.26.0", "@babel/runtime": "^7.26.0", diff --git a/src/lib/hooks/useNotificationHandler.ts b/src/lib/hooks/useNotificationHandler.ts index 69ae536d02..2ec3fcb799 100644 --- a/src/lib/hooks/useNotificationHandler.ts +++ b/src/lib/hooks/useNotificationHandler.ts @@ -239,14 +239,21 @@ export function useNotificationsHandler() { ) logEvent('notifications:openApp', {}) invalidateCachedUnreadPage() - truncateAndInvalidate(queryClient, RQKEY_NOTIFS()) + const payload = e.notification.request.trigger + .payload as NotificationPayload + truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all')) + if ( + payload.reason === 'mention' || + payload.reason === 'quote' || + payload.reason === 'reply' + ) { + truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions')) + } logger.debug('Notifications: handleNotification', { content: e.notification.request.content, payload: e.notification.request.trigger.payload, }) - handleNotification( - e.notification.request.trigger.payload as NotificationPayload, - ) + handleNotification(payload) Notifications.dismissAllNotificationsAsync() } }) diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 9e3407261d..238e4be4c3 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -75,7 +75,7 @@ export type SearchTabNavigatorParams = CommonNavigatorParams & { } export type NotificationsTabNavigatorParams = CommonNavigatorParams & { - Notifications: {show?: 'all'} + Notifications: undefined } export type MyProfileTabNavigatorParams = CommonNavigatorParams & { @@ -90,7 +90,7 @@ export type FlatNavigatorParams = CommonNavigatorParams & { Home: undefined Search: {q?: string} Feeds: undefined - Notifications: {show?: 'all'} + Notifications: undefined Hashtag: {tag: string; author?: string} Messages: {pushToConversation?: string; animation?: 'push' | 'pop'} } @@ -102,7 +102,7 @@ export type AllNavigatorParams = CommonNavigatorParams & { Search: {q?: string} Feeds: undefined NotificationsTab: undefined - Notifications: {show?: 'all'} + Notifications: undefined MyProfileTab: undefined Hashtag: {tag: string; author?: string} MessagesTab: undefined diff --git a/src/screens/Settings/NotificationSettings.tsx b/src/screens/Settings/NotificationSettings.tsx index 1c77b31489..ebb230c2ca 100644 --- a/src/screens/Settings/NotificationSettings.tsx +++ b/src/screens/Settings/NotificationSettings.tsx @@ -18,7 +18,13 @@ type Props = NativeStackScreenProps export function NotificationSettingsScreen({}: Props) { const {_} = useLingui() - const {data, isError: isQueryError, refetch} = useNotificationFeedQuery() + const { + data, + isError: isQueryError, + refetch, + } = useNotificationFeedQuery({ + filter: 'all', + }) const serverPriority = data?.pages.at(0)?.priority const { diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts index 19a92fc3c0..72100a6245 100644 --- a/src/state/queries/notifications/feed.ts +++ b/src/state/queries/notifications/feed.ts @@ -52,25 +52,22 @@ const PAGE_SIZE = 30 type RQPageParam = string | undefined const RQKEY_ROOT = 'notification-feed' -export function RQKEY(priority?: false) { - return [RQKEY_ROOT, priority] +export function RQKEY(filter: 'all' | 'mentions') { + return [RQKEY_ROOT, filter] } -export function useNotificationFeedQuery(opts?: { +export function useNotificationFeedQuery(opts: { enabled?: boolean - overridePriorityNotifications?: boolean + filter: 'all' | 'mentions' }) { const agent = useAgent() const queryClient = useQueryClient() const moderationOpts = useModerationOpts() const unreads = useUnreadNotificationsApi() - const enabled = opts?.enabled !== false + const enabled = opts.enabled !== false + const filter = opts.filter const {uris: hiddenReplyUris} = useThreadgateHiddenReplyUris() - // false: force showing all notifications - // undefined: let the server decide - const priority = opts?.overridePriorityNotifications ? false : undefined - const selectArgs = useMemo(() => { return { moderationOpts, @@ -91,14 +88,23 @@ export function useNotificationFeedQuery(opts?: { RQPageParam >({ staleTime: STALE.INFINITY, - queryKey: RQKEY(priority), + queryKey: RQKEY(filter), async queryFn({pageParam}: {pageParam: RQPageParam}) { let page - if (!pageParam) { + if (filter === 'all' && !pageParam) { // for the first page, we check the cached page held by the unread-checker first page = unreads.getCachedUnreadPage() } if (!page) { + let reasons: string[] = [] + if (filter === 'mentions') { + reasons = [ + // Anything that's a post + 'mention', + 'reply', + 'quote', + ] + } const {page: fetchedPage} = await fetchPage({ agent, limit: PAGE_SIZE, @@ -106,13 +112,13 @@ export function useNotificationFeedQuery(opts?: { queryClient, moderationOpts, fetchAdditionalData: true, - priority, + reasons, }) page = fetchedPage } - // if the first page has an unread, mark all read - if (!pageParam) { + if (filter === 'all' && !pageParam) { + // if the first page has an unread, mark all read unreads.markAllRead() } diff --git a/src/state/queries/notifications/settings.ts b/src/state/queries/notifications/settings.ts index a17fce8326..e552b65202 100644 --- a/src/state/queries/notifications/settings.ts +++ b/src/state/queries/notifications/settings.ts @@ -45,7 +45,8 @@ export function useNotificationSettingsMutation() { }, onSettled: () => { invalidateCachedUnreadPage() - queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS()}) + queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS('all')}) + queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS('mentions')}) }, }) } @@ -54,7 +55,7 @@ function eagerlySetCachedPriority( queryClient: ReturnType, enabled: boolean, ) { - queryClient.setQueryData(RQKEY_NOTIFS(), (old: any) => { + function updateData(old: any) { if (!old) return old return { ...old, @@ -65,5 +66,7 @@ function eagerlySetCachedPriority( } }), } - }) + } + queryClient.setQueryData(RQKEY_NOTIFS('all'), updateData) + queryClient.setQueryData(RQKEY_NOTIFS('mentions'), updateData) } diff --git a/src/state/queries/notifications/unread.tsx b/src/state/queries/notifications/unread.tsx index 2ade04246f..ba2377a784 100644 --- a/src/state/queries/notifications/unread.tsx +++ b/src/state/queries/notifications/unread.tsx @@ -2,7 +2,7 @@ * A kind of companion API to ./feed.ts. See that file for more info. */ -import React from 'react' +import React, {useRef} from 'react' import {AppState} from 'react-native' import {useQueryClient} from '@tanstack/react-query' import EventEmitter from 'eventemitter3' @@ -105,6 +105,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) { } }, [setNumUnread]) + const isFetchingRef = useRef(false) + // create API const api = React.useMemo(() => { return { @@ -138,6 +140,12 @@ export function Provider({children}: React.PropsWithChildren<{}>) { } } + if (isFetchingRef.current) { + return + } + // Do not move this without ensuring it gets a symmetrical reset in the finally block. + isFetchingRef.current = true + // count const {page, indexedAt: lastIndexed} = await fetchPage({ agent, @@ -145,6 +153,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { limit: 40, queryClient, moderationOpts, + reasons: [], // only fetch subjects when the page is going to be used // in the notifications query, otherwise skip it @@ -174,11 +183,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) { // update & broadcast setNumUnread(unreadCountStr) if (invalidate) { - truncateAndInvalidate(queryClient, RQKEY_NOTIFS()) + truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all')) + truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions')) } broadcast.postMessage({event: unreadCountStr}) } catch (e) { logger.warn('Failed to check unread notifications', {error: e}) + } finally { + isFetchingRef.current = false } }, diff --git a/src/state/queries/notifications/util.ts b/src/state/queries/notifications/util.ts index a251d170ec..0d72e9e929 100644 --- a/src/state/queries/notifications/util.ts +++ b/src/state/queries/notifications/util.ts @@ -31,6 +31,7 @@ export async function fetchPage({ queryClient, moderationOpts, fetchAdditionalData, + reasons, }: { agent: BskyAgent cursor: string | undefined @@ -38,7 +39,7 @@ export async function fetchPage({ queryClient: QueryClient moderationOpts: ModerationOpts | undefined fetchAdditionalData: boolean - priority?: boolean + reasons: string[] }): Promise<{ page: FeedPage indexedAt: string | undefined @@ -46,7 +47,7 @@ export async function fetchPage({ const res = await agent.listNotifications({ limit, cursor, - // priority, + reasons, }) const indexedAt = res.data.notifications[0]?.indexedAt diff --git a/src/state/queries/util.ts b/src/state/queries/util.ts index 0d6a8e99ac..887c1df0ac 100644 --- a/src/state/queries/util.ts +++ b/src/state/queries/util.ts @@ -8,7 +8,7 @@ import { } from '@atproto/api' import {InfiniteData, QueryClient, QueryKey} from '@tanstack/react-query' -export function truncateAndInvalidate( +export async function truncateAndInvalidate( queryClient: QueryClient, queryKey: QueryKey, ) { @@ -21,7 +21,7 @@ export function truncateAndInvalidate( } return data }) - queryClient.invalidateQueries({queryKey}) + return queryClient.invalidateQueries({queryKey}) } // Given an AtUri, this function will check if the AtUri matches a diff --git a/src/view/com/notifications/NotificationFeed.tsx b/src/view/com/notifications/NotificationFeed.tsx index 5168933aeb..0b814e68dc 100644 --- a/src/view/com/notifications/NotificationFeed.tsx +++ b/src/view/com/notifications/NotificationFeed.tsx @@ -9,13 +9,11 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' -import {usePalette} from '#/lib/hooks/usePalette' import {cleanError} from '#/lib/strings/errors' import {s} from '#/lib/styles' import {logger} from '#/logger' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useNotificationFeedQuery} from '#/state/queries/notifications/feed' -import {useUnreadNotificationsApi} from '#/state/queries/notifications/unread' import {EmptyState} from '#/view/com/util/EmptyState' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {List, ListRef} from '#/view/com/util/List' @@ -28,26 +26,26 @@ const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'} const LOADING_ITEM = {_reactKey: '__loading__'} export function NotificationFeed({ + filter, + enabled, scrollElRef, onPressTryAgain, onScrolledDownChange, ListHeaderComponent, - overridePriorityNotifications, + refreshNotifications, }: { + filter: 'all' | 'mentions' + enabled: boolean scrollElRef?: ListRef onPressTryAgain?: () => void onScrolledDownChange: (isScrolledDown: boolean) => void ListHeaderComponent?: () => JSX.Element - overridePriorityNotifications?: boolean + refreshNotifications: () => Promise }) { const initialNumToRender = useInitialNumToRender() - const [isPTRing, setIsPTRing] = React.useState(false) - const pal = usePalette('default') - const {_} = useLingui() const moderationOpts = useModerationOpts() - const {checkUnread} = useUnreadNotificationsApi() const { data, isFetching, @@ -58,8 +56,8 @@ export function NotificationFeed({ isFetchingNextPage, fetchNextPage, } = useNotificationFeedQuery({ - enabled: !!moderationOpts, - overridePriorityNotifications, + enabled: enabled && !!moderationOpts, + filter, }) const isEmpty = !isFetching && !data?.pages[0]?.items.length @@ -85,7 +83,7 @@ export function NotificationFeed({ const onRefresh = React.useCallback(async () => { try { setIsPTRing(true) - await checkUnread({invalidate: true}) + await refreshNotifications() } catch (err) { logger.error('Failed to refresh notifications feed', { message: err, @@ -93,7 +91,7 @@ export function NotificationFeed({ } finally { setIsPTRing(false) } - }, [checkUnread, setIsPTRing]) + }, [refreshNotifications, setIsPTRing]) const onEndReached = React.useCallback(async () => { if (isFetching || !hasNextPage || isError) return @@ -129,21 +127,18 @@ export function NotificationFeed({ /> ) } else if (item === LOADING_ITEM) { - return ( - - - - ) + return } return ( ) }, - [moderationOpts, _, onPressRetryLoadMore, pal.border], + [moderationOpts, _, onPressRetryLoadMore, filter], ) const FeedFooter = React.useCallback( diff --git a/src/view/com/notifications/NotificationFeedItem.tsx b/src/view/com/notifications/NotificationFeedItem.tsx index 4902e66bc7..1267ce0894 100644 --- a/src/view/com/notifications/NotificationFeedItem.tsx +++ b/src/view/com/notifications/NotificationFeedItem.tsx @@ -79,10 +79,12 @@ interface Author { let NotificationFeedItem = ({ item, moderationOpts, + highlightUnread, hideTopBorder, }: { item: FeedNotification moderationOpts: ModerationOpts + highlightUnread: boolean hideTopBorder?: boolean }): React.ReactNode => { const queryClient = useQueryClient() @@ -151,6 +153,7 @@ let NotificationFeedItem = ({ if (!item.subject) { return null } + const isHighlighted = highlightUnread && !item.notification.isRead return ( diff --git a/src/view/com/pager/Pager.tsx b/src/view/com/pager/Pager.tsx index 2c0bbee520..b3f936ddc4 100644 --- a/src/view/com/pager/Pager.tsx +++ b/src/view/com/pager/Pager.tsx @@ -136,12 +136,14 @@ export const Pager = forwardRef>( return ( - {renderTabBar({ - selectedPage, - onSelect: onTabBarSelect, - dragProgress, - dragState, - })} + + {renderTabBar({ + selectedPage, + onSelect: onTabBarSelect, + dragProgress, + dragState, + })} + ) } - return + return ( + + ) } function MockAccountCard({ diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx index 70ab32db0d..82c68dde64 100644 --- a/src/view/screens/Notifications.tsx +++ b/src/view/screens/Notifications.tsx @@ -13,7 +13,7 @@ import { } from '#/lib/routes/types' import {s} from '#/lib/styles' import {logger} from '#/logger' -import {isNative, isWeb} from '#/platform/detection' +import {isNative} from '#/platform/detection' import {emitSoftReset, listenSoftReset} from '#/state/events' import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed' import { @@ -24,35 +24,173 @@ import {truncateAndInvalidate} from '#/state/queries/util' import {useSetMinimalShellMode} from '#/state/shell' import {useComposerControls} from '#/state/shell/composer' import {NotificationFeed} from '#/view/com/notifications/NotificationFeed' +import {Pager} from '#/view/com/pager/Pager' +import {TabBar} from '#/view/com/pager/TabBar' import {FAB} from '#/view/com/util/fab/FAB' import {ListMethods} from '#/view/com/util/List' import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn' import {MainScrollProvider} from '#/view/com/util/MainScrollProvider' -import {atoms as a, useBreakpoints, useTheme} from '#/alf' -import {Button, ButtonIcon} from '#/components/Button' +import {atoms as a} from '#/alf' +import {web} from '#/alf' +import {ButtonIcon} from '#/components/Button' import {SettingsGear2_Stroke2_Corner0_Rounded as SettingsIcon} from '#/components/icons/SettingsGear2' import * as Layout from '#/components/Layout' import {Link} from '#/components/Link' import {Loader} from '#/components/Loader' +// We don't currently persist this across reloads since +// you gotta visit All to clear the badge anyway. +// But let's at least persist it during the sesssion. +let lastActiveTab = 0 + type Props = NativeStackScreenProps< NotificationsTabNavigatorParams, 'Notifications' > -export function NotificationsScreen({route: {params}}: Props) { - const t = useTheme() - const {gtTablet} = useBreakpoints() +export function NotificationsScreen({}: Props) { + const {_} = useLingui() + const {openComposer} = useComposerControls() + const unreadNotifs = useUnreadNotifications() + const hasNew = !!unreadNotifs + const {checkUnread: checkUnreadAll} = useUnreadNotificationsApi() + const [isLoadingAll, setIsLoadingAll] = React.useState(false) + const [isLoadingMentions, setIsLoadingMentions] = React.useState(false) + const initialActiveTab = lastActiveTab + const [activeTab, setActiveTab] = React.useState(initialActiveTab) + const isLoading = activeTab === 0 ? isLoadingAll : isLoadingMentions + + const onPageSelected = React.useCallback( + (index: number) => { + setActiveTab(index) + lastActiveTab = index + }, + [setActiveTab], + ) + + const queryClient = useQueryClient() + const checkUnreadMentions = React.useCallback( + async ({invalidate}: {invalidate: boolean}) => { + if (invalidate) { + return truncateAndInvalidate(queryClient, NOTIFS_RQKEY('mentions')) + } else { + // Background polling is not implemented for the mentions tab. + // Just ignore it. + } + }, + [queryClient], + ) + + const sections = React.useMemo(() => { + return [ + { + title: _(msg`All`), + component: ( + + ), + }, + { + title: _(msg`Mentions`), + component: ( + + ), + }, + ] + }, [ + _, + hasNew, + checkUnreadAll, + checkUnreadMentions, + activeTab, + isLoadingAll, + isLoadingMentions, + ]) + + return ( + + + + + + Notifications + + + + + + + + + ( + + section.title)} + onPressSelected={() => emitSoftReset()} + /> + + )} + initialPage={initialActiveTab}> + {sections.map((section, i) => ( + {section.component} + ))} + + openComposer({})} + icon={} + accessibilityRole="button" + accessibilityLabel={_(msg`New post`)} + accessibilityHint="" + /> + + ) +} + +function NotificationsTab({ + filter, + isActive, + isLoading, + hasNew, + checkUnread, + setIsLoadingLatest, +}: { + filter: 'all' | 'mentions' + isActive: boolean + isLoading: boolean + hasNew: boolean + checkUnread: ({invalidate}: {invalidate: boolean}) => Promise + setIsLoadingLatest: (v: boolean) => void +}) { const {_} = useLingui() const setMinimalShellMode = useSetMinimalShellMode() const [isScrolledDown, setIsScrolledDown] = React.useState(false) - const [isLoadingLatest, setIsLoadingLatest] = React.useState(false) const scrollElRef = React.useRef(null) const queryClient = useQueryClient() - const unreadNotifs = useUnreadNotifications() - const unreadApi = useUnreadNotificationsApi() - const hasNew = !!unreadNotifs const isScreenFocused = useIsFocused() - const {openComposer} = useComposerControls() + const isFocusedAndActive = isScreenFocused && isActive // event handlers // = @@ -65,16 +203,23 @@ export function NotificationsScreen({route: {params}}: Props) { scrollToTop() if (hasNew) { // render what we have now - truncateAndInvalidate(queryClient, NOTIFS_RQKEY()) - } else { + truncateAndInvalidate(queryClient, NOTIFS_RQKEY(filter)) + } else if (!isLoading) { // check with the server setIsLoadingLatest(true) - unreadApi - .checkUnread({invalidate: true}) + checkUnread({invalidate: true}) .catch(() => undefined) .then(() => setIsLoadingLatest(false)) } - }, [scrollToTop, queryClient, unreadApi, hasNew, setIsLoadingLatest]) + }, [ + scrollToTop, + queryClient, + checkUnread, + hasNew, + isLoading, + setIsLoadingLatest, + filter, + ]) const onFocusCheckLatest = useNonReactiveCallback(() => { // on focus, check for latest, but only invalidate if the user @@ -87,79 +232,36 @@ export function NotificationsScreen({route: {params}}: Props) { // we're just going to look it up synchronously. currentIsScrolledDown = window.scrollY > 200 } - unreadApi.checkUnread({invalidate: !currentIsScrolledDown}) + checkUnread({invalidate: !currentIsScrolledDown}) }) // on-visible setup // = useFocusEffect( React.useCallback(() => { - setMinimalShellMode(false) - logger.debug('NotificationsScreen: Focus') - onFocusCheckLatest() - }, [setMinimalShellMode, onFocusCheckLatest]), + if (isFocusedAndActive) { + setMinimalShellMode(false) + logger.debug('NotificationsScreen: Focus') + onFocusCheckLatest() + } + }, [setMinimalShellMode, onFocusCheckLatest, isFocusedAndActive]), ) React.useEffect(() => { - if (!isScreenFocused) { + if (!isFocusedAndActive) { return } return listenSoftReset(onPressLoadLatest) - }, [onPressLoadLatest, isScreenFocused]) + }, [onPressLoadLatest, isFocusedAndActive]) return ( - - - - - - - - - - - - - + <> checkUnread({invalidate: true})} onScrolledDownChange={setIsScrolledDown} scrollElRef={scrollElRef} - overridePriorityNotifications={params?.show === 'all'} /> {(isScrolledDown || hasNew) && ( @@ -169,14 +271,6 @@ export function NotificationsScreen({route: {params}}: Props) { showIndicator={hasNew} /> )} - openComposer({})} - icon={} - accessibilityRole="button" - accessibilityLabel={_(msg`New post`)} - accessibilityHint="" - /> - + ) } diff --git a/yarn.lock b/yarn.lock index 16fe9579a4..9e2259e0d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20,21 +20,21 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@atproto-labs/fetch-node@0.1.3": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@atproto-labs/fetch-node/-/fetch-node-0.1.3.tgz#2581bf4710a4f957c74c75d959961de3304b3595" - integrity sha512-KX3ogPJt6dXNppWImQ9omfhrc8t73WrJaxHMphRAqQL8jXxKW5NBCTjSuwroBkJ1pj1aValBrc5NpdYu+H/9Qg== +"@atproto-labs/fetch-node@0.1.4": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@atproto-labs/fetch-node/-/fetch-node-0.1.4.tgz#03859a39556eab936e2b3bec2d087585c6408cb3" + integrity sha512-hwYx0XpgIl2zydRy13DtWvywruuHk1EX+yCjqjgUIezUm8fi35ZN4QvR6INEm0MpN2MD/kQsImPbd8ZftzZ3zw== dependencies: - "@atproto-labs/fetch" "0.1.1" + "@atproto-labs/fetch" "0.1.2" "@atproto-labs/pipe" "0.1.0" ipaddr.js "^2.1.0" psl "^1.9.0" undici "^6.14.1" -"@atproto-labs/fetch@0.1.1": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@atproto-labs/fetch/-/fetch-0.1.1.tgz#10e7f8c06cf01a63f58e130b95d9ee0d4171902c" - integrity sha512-X1zO1MDoJzEurbWXMAe1H8EZ995Xam/aXdxhGVrXmOMyPDuvBa1oxwh/kQNZRCKcMQUbiwkk+Jfq6ZkTuvGbww== +"@atproto-labs/fetch@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@atproto-labs/fetch/-/fetch-0.1.2.tgz#e1b9354205fb76f106ae3e1c6b56e7865a39600f" + integrity sha512-7mQQIRtVenqtdBQKCqoLjyAhPS2aA56EGEjyz5zB3sramM3qkrvzyusr55GAzGDS0tvB6cy9cDEtSLmfK7LUnA== dependencies: "@atproto-labs/pipe" "0.1.0" optionalDependencies: @@ -58,28 +58,28 @@ resolved "https://registry.yarnpkg.com/@atproto-labs/simple-store/-/simple-store-0.1.1.tgz#e743a2722b5d8732166f0a72aca8bd10e9bff106" integrity sha512-WKILW2b3QbAYKh+w5U2x6p5FqqLl0nAeLwGeDY+KjX01K4Dq3vQTR9b/qNp0jZm48CabPQVrqCv0PPU9LgRRRg== -"@atproto/api@^0.13.18": - version "0.13.18" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.18.tgz#cc537cc3b4c8d03f258a373f4d893fea11a77cdd" - integrity sha512-rrl5HhzGYWZ7fiC965TPBUOVItq9M4dxMb6qz8IvAVQliSkrJrKc7UD0QWL89QiiXaOBuX8w+4i5r4wrfBGddg== +"@atproto/api@^0.13.20": + version "0.13.20" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.20.tgz#5140db303c3b0981958dfe6a5fa6d7d1cd7bb3cc" + integrity sha512-z/+CvG6BEttRHf856tKSe1AeUQNfrobRJldaHAthGmFk7O3wLZQyfcI9DUmBJQ9+4wAt0dZwvKWVGLZOV9eLHA== dependencies: "@atproto/common-web" "^0.3.1" - "@atproto/lexicon" "^0.4.3" + "@atproto/lexicon" "^0.4.4" "@atproto/syntax" "^0.3.1" - "@atproto/xrpc" "^0.6.4" + "@atproto/xrpc" "^0.6.5" await-lock "^2.2.2" multiformats "^9.9.0" tlds "^1.234.0" zod "^3.23.8" -"@atproto/aws@^0.2.9": - version "0.2.9" - resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.9.tgz#3539b281b725914b769451ee4afc62315dff1afc" - integrity sha512-sc9aXUePcqItkJSOJJnGNVthVfAKjhn3zMDG+RRLzKUBye6Yutrlhpt1yxNZLHQiqIK5fy2Cuc4EX3p3jeWUYw== +"@atproto/aws@^0.2.10": + version "0.2.10" + resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.10.tgz#e0b888fd50308cc24b7086cf3ec209587c13bbe4" + integrity sha512-zQElKk6wGTQo5aKdXtmx/dINjkVgbJU9+C/xOVTs+M88I8IrrBxPvo1dASLJcMtRb9VjXh5snLJeAjgyx6qC6Q== dependencies: - "@atproto/common" "^0.4.4" + "@atproto/common" "^0.4.5" "@atproto/crypto" "^0.4.2" - "@atproto/repo" "^0.5.5" + "@atproto/repo" "^0.6.0" "@aws-sdk/client-cloudfront" "^3.261.0" "@aws-sdk/client-kms" "^3.196.0" "@aws-sdk/client-s3" "^3.224.0" @@ -89,20 +89,20 @@ multiformats "^9.9.0" uint8arrays "3.0.0" -"@atproto/bsky@^0.0.96": - version "0.0.96" - resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.96.tgz#b89abf2828f57738357beb4efd05539667dd14b3" - integrity sha512-Tk0ppiPMKdcnPU3x+uBAVRn92vroznhr2OlqinNSy/PZ39qWViRlKAhG3CLJsU2gjSHxsNfaIwulj7tPvKCmSw== +"@atproto/bsky@^0.0.98": + version "0.0.98" + resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.98.tgz#4c4746e588568df1878647ae80cf4b963bc95924" + integrity sha512-Y+un2pD1W1H0s0IWdY6S4vLy8rgR8cpqThz9onn4wDppmGWvOBNXeD8AjNzIWC0iFlYcfR4rwCKSoccUXYzxNg== dependencies: - "@atproto/api" "^0.13.18" - "@atproto/common" "^0.4.4" + "@atproto/api" "^0.13.20" + "@atproto/common" "^0.4.5" "@atproto/crypto" "^0.4.2" "@atproto/identity" "^0.4.3" - "@atproto/lexicon" "^0.4.3" - "@atproto/repo" "^0.5.5" - "@atproto/sync" "^0.1.6" + "@atproto/lexicon" "^0.4.4" + "@atproto/repo" "^0.6.0" + "@atproto/sync" "^0.1.7" "@atproto/syntax" "^0.3.1" - "@atproto/xrpc-server" "^0.7.3" + "@atproto/xrpc-server" "^0.7.4" "@bufbuild/protobuf" "^1.5.0" "@connectrpc/connect" "^1.1.4" "@connectrpc/connect-express" "^1.1.4" @@ -129,12 +129,12 @@ typed-emitter "^2.1.0" uint8arrays "3.0.0" -"@atproto/bsync@^0.0.9": - version "0.0.9" - resolved "https://registry.yarnpkg.com/@atproto/bsync/-/bsync-0.0.9.tgz#7a6d58ef776404893d3c1139bdfe606fef483612" - integrity sha512-N0+TnYOoJz4hTo6/h1jJKh6QzdbwkFuOQ1bdwugzST7ZkwMtjs5FX8o/uqgiD4gSHSqfQSRrew7+qYEHUT61Aw== +"@atproto/bsync@^0.0.10": + version "0.0.10" + resolved "https://registry.yarnpkg.com/@atproto/bsync/-/bsync-0.0.10.tgz#fa16acfaf67112449b703778a20c785226c94189" + integrity sha512-qviPMyYade/sqhX/9X9eTT4KaQ+FLvOyz+140LCDk/0vbZUCZPuYSEXZDCQkL5nlEXzScsQ3iyVeoYCGvV5kYw== dependencies: - "@atproto/common" "^0.4.4" + "@atproto/common" "^0.4.5" "@atproto/syntax" "^0.3.1" "@bufbuild/protobuf" "^1.5.0" "@connectrpc/connect" "^1.1.4" @@ -175,10 +175,10 @@ pino "^8.6.1" zod "^3.14.2" -"@atproto/common@^0.4.4": - version "0.4.4" - resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.4.4.tgz#79096aef920f5ad7cda5c682d7ed7416d0581e1a" - integrity sha512-58tMbn6A1Zu296s/l3uIj8z9d7IRHpZvLOfsFRikaQaYrzhJpL2aPY4uFQ8GJcxnsxeUnxBCrQz9we5jVVJI5Q== +"@atproto/common@^0.4.5": + version "0.4.5" + resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.4.5.tgz#28fd176a9b5527c723828e725586bc0be9fa9516" + integrity sha512-LFAGqHcxCI5+b31Xgk+VQQtZU258iGPpHJzNeHVcdh6teIKZi4C2l6YV+m+3CEz+yYcfP7jjUmgqesx7l9Arsg== dependencies: "@atproto/common-web" "^0.3.1" "@ipld/dag-cbor" "^7.0.3" @@ -207,23 +207,23 @@ "@noble/hashes" "^1.3.1" uint8arrays "3.0.0" -"@atproto/dev-env@^0.3.64": - version "0.3.64" - resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.64.tgz#148537785b6a86b0a56d0988e63a1ff8ea7c84e9" - integrity sha512-s7mdppgp2BS0uy5ASZwqJ3J8dez14pDGI9uqTGbsOYF/qTCbBGZKw/Vkqjci5bY1UaW+o6n787q63ECDtljM8A== +"@atproto/dev-env@^0.3.67": + version "0.3.67" + resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.67.tgz#4f6a20f0aafa8125ed9ec715abceedd11580882e" + integrity sha512-7Ize4Y5vdjQjyrxTwjBPbkxKXQdE02KpE7AJLJt6Xpvowd2vbn8l8rDXfha+LtVi6t/613U4s+Slo5c1YD3x9A== dependencies: - "@atproto/api" "^0.13.18" - "@atproto/bsky" "^0.0.96" - "@atproto/bsync" "^0.0.9" + "@atproto/api" "^0.13.20" + "@atproto/bsky" "^0.0.98" + "@atproto/bsync" "^0.0.10" "@atproto/common-web" "^0.3.1" "@atproto/crypto" "^0.4.2" "@atproto/identity" "^0.4.3" - "@atproto/lexicon" "^0.4.3" - "@atproto/ozone" "^0.1.57" - "@atproto/pds" "^0.4.73" - "@atproto/sync" "^0.1.6" + "@atproto/lexicon" "^0.4.4" + "@atproto/ozone" "^0.1.59" + "@atproto/pds" "^0.4.76" + "@atproto/sync" "^0.1.7" "@atproto/syntax" "^0.3.1" - "@atproto/xrpc-server" "^0.7.3" + "@atproto/xrpc-server" "^0.7.4" "@did-plc/lib" "^0.0.1" "@did-plc/server" "^0.0.1" axios "^0.27.2" @@ -258,10 +258,10 @@ multiformats "^9.9.0" zod "^3.23.8" -"@atproto/lexicon@^0.4.3": - version "0.4.3" - resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.3.tgz#d69f6bb363a6326df7766c48132bfa30e22622d9" - integrity sha512-lFVZXe1S1pJP0dcxvJuHP3r/a+EAIBwwU7jUK+r8iLhIja+ml6NmYv8KeFHmIJATh03spEQ9s02duDmFVdCoXg== +"@atproto/lexicon@^0.4.4": + version "0.4.4" + resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.4.tgz#0d97314bb57b693b76f2495fa5e02872469dd93a" + integrity sha512-QFEmr3rpj/RoAmfX9ALU/asBG/rsVtQZnw+9nOB1/AuIwoxXd+ZyndR6lVUc2+DL4GEjl6W2yvBru5xbQIZWyA== dependencies: "@atproto/common-web" "^0.3.1" "@atproto/syntax" "^0.3.1" @@ -269,20 +269,20 @@ multiformats "^9.9.0" zod "^3.23.8" -"@atproto/oauth-provider@^0.2.7": - version "0.2.7" - resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.2.7.tgz#38a211c197ee1ce4e92a5b59a92f2e15fcacee0b" - integrity sha512-T/cEr7TGs36SqTW8JzLAt9EchumYY48zuI4rqoAepYT29eGpP37SxK+5X0+fQHOKJPKWUGlYocR9fDm4CdzAPQ== +"@atproto/oauth-provider@^0.2.10": + version "0.2.10" + resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.2.10.tgz#f9820d7f82c33d3b74e81a75873f50e1e654b901" + integrity sha512-cF42lo0+Mj+Zq2RXwS2NxmobmtL7YL1vXlYcN6iKflZ8pQ5WvpR/cZKsKEZOT9cEBBTw5MARKTYxbr8CPDKlHg== dependencies: - "@atproto-labs/fetch" "0.1.1" - "@atproto-labs/fetch-node" "0.1.3" + "@atproto-labs/fetch" "0.1.2" + "@atproto-labs/fetch-node" "0.1.4" "@atproto-labs/pipe" "0.1.0" "@atproto-labs/simple-store" "0.1.1" "@atproto-labs/simple-store-memory" "0.1.1" - "@atproto/common" "^0.4.4" + "@atproto/common" "^0.4.5" "@atproto/jwk" "0.1.1" "@atproto/jwk-jose" "0.1.2" - "@atproto/oauth-types" "0.2.0" + "@atproto/oauth-types" "0.2.1" "@hapi/accept" "^6.0.3" "@hapi/bourne" "^3.0.0" "@hapi/content" "^6.0.0" @@ -294,27 +294,27 @@ psl "^1.9.0" zod "^3.23.8" -"@atproto/oauth-types@0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@atproto/oauth-types/-/oauth-types-0.2.0.tgz#28bc861b56cba093e6c52603cec1d3d38cd2a1e7" - integrity sha512-v/4ht6eRh0yOu2iuuWujZdnJBamPKimdy8k0Xan8cVZ+a2i83UkhIIU+S/XUbbvJ4a64wLPZrS9IDd0K5XYYTQ== +"@atproto/oauth-types@0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@atproto/oauth-types/-/oauth-types-0.2.1.tgz#a7ace557cc91817fcde6195f023e4e1838e4aef6" + integrity sha512-hDisUXzcq5KU1HMuCYZ8Kcz7BePl7V11bFjjgZvND3mdSphiyBpJ8MCNn3QzAa6cXpFo0w9PDcYMAlCCRZHdVw== dependencies: "@atproto/jwk" "0.1.1" zod "^3.23.8" -"@atproto/ozone@^0.1.57": - version "0.1.57" - resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.57.tgz#141d66b213710575c7859d691586fd44c731f7ca" - integrity sha512-P2YKeRFPbxKc2e2yftUoMTTcWYuFV0qU1/Nkd4GxuHnBnDJcbtMPglXd7kyLf0p8plCCFau/wZ8QdY8KSDLM9Q== +"@atproto/ozone@^0.1.59": + version "0.1.59" + resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.59.tgz#219984a46617b0ac039f2f02767290eaa0b4cfc3" + integrity sha512-AD03Ocb3fZW+grxO/VwMld5iNdCLgbahFzku6xh1qEw0tLOBKp3GXSfepVd9XWu5fb1yPhGPd2JgjApV5hbJvw== dependencies: - "@atproto/api" "^0.13.18" - "@atproto/common" "^0.4.4" + "@atproto/api" "^0.13.20" + "@atproto/common" "^0.4.5" "@atproto/crypto" "^0.4.2" "@atproto/identity" "^0.4.3" - "@atproto/lexicon" "^0.4.3" + "@atproto/lexicon" "^0.4.4" "@atproto/syntax" "^0.3.1" - "@atproto/xrpc" "^0.6.4" - "@atproto/xrpc-server" "^0.7.3" + "@atproto/xrpc" "^0.6.5" + "@atproto/xrpc-server" "^0.7.4" "@did-plc/lib" "^0.0.1" axios "^1.6.7" compression "^1.7.4" @@ -331,29 +331,30 @@ typed-emitter "^2.1.0" uint8arrays "3.0.0" -"@atproto/pds@^0.4.73": - version "0.4.73" - resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.73.tgz#49b7625d9b40a5a24be1cdd7cdb56faab9e25707" - integrity sha512-fzrKlgKVF5JvTTmhfvofXT9Ok1KFTfAjCzTrLJivbOcqQSqBagNTuz5CiQxAAAo/JTlSxmnyr3e7OrlJdrph1w== +"@atproto/pds@^0.4.76": + version "0.4.76" + resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.76.tgz#cd7b3f13359a7c31dc9362a5e4309419512c4102" + integrity sha512-+cFVpqlgpCS0BuGac5fCQPZUugpS1r7ghnSQLVdjnTnvQJCqLRA++BlJWYbGgRP6FJrumCY2jtuwG8t59Rjt8Q== dependencies: - "@atproto-labs/fetch-node" "0.1.3" - "@atproto/api" "^0.13.18" - "@atproto/aws" "^0.2.9" - "@atproto/common" "^0.4.4" + "@atproto-labs/fetch-node" "0.1.4" + "@atproto/api" "^0.13.20" + "@atproto/aws" "^0.2.10" + "@atproto/common" "^0.4.5" "@atproto/crypto" "^0.4.2" "@atproto/identity" "^0.4.3" - "@atproto/lexicon" "^0.4.3" - "@atproto/oauth-provider" "^0.2.7" - "@atproto/repo" "^0.5.5" + "@atproto/lexicon" "^0.4.4" + "@atproto/oauth-provider" "^0.2.10" + "@atproto/repo" "^0.6.0" "@atproto/syntax" "^0.3.1" - "@atproto/xrpc" "^0.6.4" - "@atproto/xrpc-server" "^0.7.3" + "@atproto/xrpc" "^0.6.5" + "@atproto/xrpc-server" "^0.7.4" "@did-plc/lib" "^0.0.4" + "@hapi/address" "^5.1.1" better-sqlite3 "^10.0.0" bytes "^3.1.2" compression "^1.7.4" cors "^2.8.5" - disposable-email "^0.2.3" + disposable-email-domains-js "^1.5.0" express "^4.17.2" express-async-errors "^3.1.1" file-type "^16.5.4" @@ -376,49 +377,50 @@ undici "^6.19.8" zod "^3.23.8" -"@atproto/repo@^0.5.5": - version "0.5.5" - resolved "https://registry.yarnpkg.com/@atproto/repo/-/repo-0.5.5.tgz#73eaf1a0b35cfc4fc1c837f4e3ddeb6768d29c20" - integrity sha512-Zu1tw42KBVyFzIh1XYSIvm8V+V9oEKWJR7NnHBgeSMwCc9QwM32jO7uqgvEjZYEXgdYKanGhv/YHLyxtZa5Ckg== +"@atproto/repo@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@atproto/repo/-/repo-0.6.0.tgz#29e698731e6df63636b0f7c91ce106a9de50ad19" + integrity sha512-6YGVhjiHKmqCW5Ce4oY49E3NCEfbvAGowJ5ETXX2sx2l4D2bOL7a2hn5zWqsPHYpWSLjrPfnj7PVpApK0kmL7A== dependencies: - "@atproto/common" "^0.4.4" + "@atproto/common" "^0.4.5" "@atproto/common-web" "^0.3.1" "@atproto/crypto" "^0.4.2" - "@atproto/lexicon" "^0.4.3" + "@atproto/lexicon" "^0.4.4" "@ipld/car" "^3.2.3" "@ipld/dag-cbor" "^7.0.0" multiformats "^9.9.0" uint8arrays "3.0.0" zod "^3.23.8" -"@atproto/sync@^0.1.6": - version "0.1.6" - resolved "https://registry.yarnpkg.com/@atproto/sync/-/sync-0.1.6.tgz#fb3e61147c05caf2c3d1cd597ff94fef68abbc02" - integrity sha512-9lqe6E6fIns28TJyQufLCVefMxmK3bvEfQBhmXJBGZMHuKlH8+F5P9DfnHv6vs6ygfmHIUIjYDWqJu/rpt8pzw== +"@atproto/sync@^0.1.7": + version "0.1.7" + resolved "https://registry.yarnpkg.com/@atproto/sync/-/sync-0.1.7.tgz#c7f78d99bb40eacf93ca13fdd04134a0985bf421" + integrity sha512-liJH2EsD4AbWA8G0oRDURgbHW6Uq4NnM2rNfbrTlqgtj0kyGRY3FcVEyqeRcaQYfCuscChIg5DQKHqY421/7Mw== dependencies: - "@atproto/common" "^0.4.4" + "@atproto/common" "^0.4.5" "@atproto/identity" "^0.4.3" - "@atproto/lexicon" "^0.4.3" - "@atproto/repo" "^0.5.5" + "@atproto/lexicon" "^0.4.4" + "@atproto/repo" "^0.6.0" "@atproto/syntax" "^0.3.1" - "@atproto/xrpc-server" "^0.7.3" + "@atproto/xrpc-server" "^0.7.4" multiformats "^9.9.0" p-queue "^6.6.2" + ws "^8.12.0" "@atproto/syntax@^0.3.1": version "0.3.1" resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.3.1.tgz#4346418728f9643d783d2ffcf7c77e132e1f53d4" integrity sha512-fzW0Mg1QUOVCWUD3RgEsDt6d1OZ6DdFmbKcDdbzUfh0t4rhtRAC05KbZYmxuMPWDAiJ4BbbQ5dkAc/mNypMXkw== -"@atproto/xrpc-server@^0.7.3": - version "0.7.3" - resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.7.3.tgz#d09b36d00edb7aacca48675d1ebb7fa796fa11bd" - integrity sha512-x0qegkN6snrbXJO3v9h2kuh9e90g6ZZkDXv3COiraGS3yRTzIm6i4bMvDSfCI50+0xCNtPKOkpn8taRoRgkyiw== +"@atproto/xrpc-server@^0.7.4": + version "0.7.4" + resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.7.4.tgz#dfac8f7276c1c971a35eaba627eb6372088441c3" + integrity sha512-MrAwxfJBQm/kCol3D8qc+vpQzBMzLqvtUbauSSfVVJ10PlGtxg4LlXqcjkAuhrjyrqp3dQH9LHuhDpgVQK+G3w== dependencies: - "@atproto/common" "^0.4.4" + "@atproto/common" "^0.4.5" "@atproto/crypto" "^0.4.2" - "@atproto/lexicon" "^0.4.3" - "@atproto/xrpc" "^0.6.4" + "@atproto/lexicon" "^0.4.4" + "@atproto/xrpc" "^0.6.5" cbor-x "^1.5.1" express "^4.17.2" http-errors "^2.0.0" @@ -428,12 +430,12 @@ ws "^8.12.0" zod "^3.23.8" -"@atproto/xrpc@^0.6.4": - version "0.6.4" - resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.4.tgz#4cf59774f7c72e5bc821bc5f1d57f0a6ae2014db" - integrity sha512-9ZAJ8nsXTqC4XFyS0E1Wlg7bAvonhXQNQ3Ocs1L1LIwFLXvsw/4fNpIHXxvXvqTCVeyHLbImOnE9UiO1c/qIYA== +"@atproto/xrpc@^0.6.5": + version "0.6.5" + resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.5.tgz#8b180fc5f6b8374fd00c41b9e4cd7b24ead48e6b" + integrity sha512-t6u8iPEVbWge5RhzKZDahSzNDYIAxUtop6Q/X/apAZY1rgreVU0/1sSvvRoRFH19d3UIKjYdLuwFqMi9w8nY3Q== dependencies: - "@atproto/lexicon" "^0.4.3" + "@atproto/lexicon" "^0.4.4" zod "^3.23.8" "@aws-crypto/crc32@3.0.0": @@ -4378,6 +4380,13 @@ "@hapi/boom" "^10.0.1" "@hapi/hoek" "^11.0.2" +"@hapi/address@^5.1.1": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@hapi/address/-/address-5.1.1.tgz#e9925fc1b65f5cc3fbea821f2b980e4652e84cb6" + integrity sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA== + dependencies: + "@hapi/hoek" "^11.0.2" + "@hapi/boom@^10.0.0", "@hapi/boom@^10.0.1": version "10.0.1" resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-10.0.1.tgz#ebb14688275ae150aa6af788dbe482e6a6062685" @@ -9593,10 +9602,10 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -disposable-email@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/disposable-email/-/disposable-email-0.2.3.tgz#a21a49717f6034a8ff777dc8eae3b4d994a7b988" - integrity sha512-gkBQQ5Res431ZXqLlAafrXHizG7/1FWmi8U2RTtriD78Vc10HhBUvdJun3R4eSF0KRIQQJs+wHlxjkED/Hr1EQ== +disposable-email-domains-js@^1.5.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/disposable-email-domains-js/-/disposable-email-domains-js-1.7.0.tgz#2bf859bccf7a2eb697025577e18f0434409713ec" + integrity sha512-qcIJcnXjDvH8EEt0tyAesk1sZVGU5ZFtW6Wys2wKCAcbUf5nJYfwZfT7Z0PVA/LBMlqd/Xgk9dXN2Q3fx7NFAg== dns-equal@^1.0.0: version "1.0.0" From 4b32b0a71be4669fa0741efc46d646093c3114f5 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 12 Dec 2024 17:42:36 +0000 Subject: [PATCH 16/58] Increase scrubber height if touch device (#7081) * further increase target area of scrubber * make scrubber bigger if touch device --------- Co-authored-by: Antonio Sarcevic --- .../post-embeds/VideoEmbedInner/web-controls/Scrubber.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/Scrubber.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/Scrubber.tsx index 74aad64e11..96960bad47 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/Scrubber.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/Scrubber.tsx @@ -3,7 +3,7 @@ import {View} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {isFirefox} from '#/lib/browser' +import {isFirefox, isTouchDevice} from '#/lib/browser' import {clamp} from '#/lib/numbers' import {atoms as a, useTheme, web} from '#/alf' import {useInteractionState} from '#/components/hooks/useInteractionState' @@ -148,7 +148,11 @@ export function Scrubber({ return (
Date: Thu, 12 Dec 2024 17:46:19 +0000 Subject: [PATCH 17/58] [Layout] Bleed profile banner into safe area (#6967) * bleed profile banner into safe area (cherry picked from commit 50b3a4d0c6fd94b583ffe4efa65de35c81ae7f4e) * pointer events none when hidden (cherry picked from commit bae2c7b2dd6d7f858a98812196628308c0877755) * fix web (cherry picked from commit e3f9597170375f2903b6e567b963f008ec95aed1) * add status bar shadow * rm log * rm mini header * speed up animation * pass bool rather than int in light status bar --- src/App.native.tsx | 5 +- src/App.web.tsx | 5 +- src/components/Layout/Header/index.tsx | 3 +- src/screens/Profile/Header/GrowableBanner.tsx | 15 ++- src/screens/Profile/Header/Shell.tsx | 31 +++-- .../Profile/Header/StatusBarShadow.tsx | 56 +++++++++ .../Profile/Header/StatusBarShadow.web.tsx | 3 + src/screens/Profile/Header/index.tsx | 119 ++++++++++++++++-- src/state/shell/light-status-bar.tsx | 45 +++++++ src/view/com/pager/PagerHeaderContext.tsx | 30 +++-- src/view/com/pager/PagerWithHeader.tsx | 25 +++- src/view/com/pager/PagerWithHeader.web.tsx | 16 ++- src/view/screens/Profile.tsx | 10 +- src/view/shell/index.tsx | 6 +- 14 files changed, 322 insertions(+), 47 deletions(-) create mode 100644 src/screens/Profile/Header/StatusBarShadow.tsx create mode 100644 src/screens/Profile/Header/StatusBarShadow.web.tsx create mode 100644 src/state/shell/light-status-bar.tsx diff --git a/src/App.native.tsx b/src/App.native.tsx index bc38eec799..c22a66e824 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -54,6 +54,7 @@ import { import {readLastActiveAccount} from '#/state/session/util' import {Provider as ShellStateProvider} from '#/state/shell' import {Provider as ComposerProvider} from '#/state/shell/composer' +import {Provider as LightStatusBarProvider} from '#/state/shell/light-status-bar' import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out' import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' @@ -209,7 +210,9 @@ function App() { - + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index 808b0fc278..b7c5a56334 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -40,6 +40,7 @@ import { import {readLastActiveAccount} from '#/state/session/util' import {Provider as ShellStateProvider} from '#/state/shell' import {Provider as ComposerProvider} from '#/state/shell/composer' +import {Provider as LightStatusBarProvider} from '#/state/shell/light-status-bar' import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out' import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' @@ -181,7 +182,9 @@ function App() { - + + + diff --git a/src/components/Layout/Header/index.tsx b/src/components/Layout/Header/index.tsx index f05350dca9..16b484cea9 100644 --- a/src/components/Layout/Header/index.tsx +++ b/src/components/Layout/Header/index.tsx @@ -175,7 +175,8 @@ export function TitleText({ gtMobile && a.text_xl, style, ]} - numberOfLines={2}> + numberOfLines={2} + emoji> {children} ) diff --git a/src/screens/Profile/Header/GrowableBanner.tsx b/src/screens/Profile/Header/GrowableBanner.tsx index 7f5a3cd6e4..3d2830439a 100644 --- a/src/screens/Profile/Header/GrowableBanner.tsx +++ b/src/screens/Profile/Header/GrowableBanner.tsx @@ -10,6 +10,7 @@ import Animated, { useAnimatedReaction, useAnimatedStyle, } from 'react-native-reanimated' +import {useSafeAreaInsets} from 'react-native-safe-area-context' import {BlurView} from 'expo-blur' import {useIsFetching} from '@tanstack/react-query' @@ -32,7 +33,7 @@ export function GrowableBanner({ }) { const pagerContext = usePagerHeaderContext() - // pagerContext should only be present on iOS, but better safe than sorry + // plain non-growable mode for Android/Web if (!pagerContext || !isIOS) { return ( @@ -60,6 +61,7 @@ function GrowableBannerInner({ backButton?: React.ReactNode children: React.ReactNode }) { + const {top: topInset} = useSafeAreaInsets() const isFetching = useIsProfileFetching() const animateSpinner = useShouldAnimateSpinner({isFetching, scrollY}) @@ -104,7 +106,7 @@ function GrowableBannerInner({ const animatedBackButtonStyle = useAnimatedStyle(() => ({ transform: [ { - translateY: interpolate(scrollY.get(), [-150, 60], [-150, 60], { + translateY: interpolate(scrollY.get(), [-150, 10], [-150, 10], { extrapolateRight: Extrapolation.CLAMP, }), }, @@ -128,7 +130,14 @@ function GrowableBannerInner({ animatedProps={animatedBlurViewProps} /> - + @@ -43,7 +44,8 @@ let ProfileHeaderShell = ({ const {_} = useLingui() const {openLightbox} = useLightboxControls() const navigation = useNavigation() - const {isDesktop} = useWebMediaQueries() + const {top: topInset} = useSafeAreaInsets() + const aviRef = useHandleRef() const onPressBack = React.useCallback(() => { @@ -100,10 +102,11 @@ let ProfileHeaderShell = ({ + - {!isDesktop && !hideBackButton && ( + {!hideBackButton && ( - - + + )} @@ -186,7 +194,6 @@ export {ProfileHeaderShell} const styles = StyleSheet.create({ backBtnWrapper: { position: 'absolute', - top: 10, left: 10, width: 30, height: 30, diff --git a/src/screens/Profile/Header/StatusBarShadow.tsx b/src/screens/Profile/Header/StatusBarShadow.tsx new file mode 100644 index 0000000000..587b410510 --- /dev/null +++ b/src/screens/Profile/Header/StatusBarShadow.tsx @@ -0,0 +1,56 @@ +import Animated, {SharedValue, useAnimatedStyle} from 'react-native-reanimated' +import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {LinearGradient} from 'expo-linear-gradient' + +import {isIOS} from '#/platform/detection' +import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext' +import {atoms as a} from '#/alf' + +const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient) + +export function StatusBarShadow() { + const {top: topInset} = useSafeAreaInsets() + const pagerContext = usePagerHeaderContext() + + if (isIOS && pagerContext) { + const {scrollY} = pagerContext + return + } + + return ( + + ) +} + +function StatusBarShadowInnner({scrollY}: {scrollY: SharedValue}) { + const {top: topInset} = useSafeAreaInsets() + + const animatedStyle = useAnimatedStyle(() => { + return { + transform: [ + { + translateY: Math.min(0, scrollY.get()), + }, + ], + } + }) + + return ( + + ) +} diff --git a/src/screens/Profile/Header/StatusBarShadow.web.tsx b/src/screens/Profile/Header/StatusBarShadow.web.tsx new file mode 100644 index 0000000000..cd79871ea7 --- /dev/null +++ b/src/screens/Profile/Header/StatusBarShadow.web.tsx @@ -0,0 +1,3 @@ +export function StatusBarShadow() { + return null +} diff --git a/src/screens/Profile/Header/index.tsx b/src/screens/Profile/Header/index.tsx index deb8063d99..7e4b9bb313 100644 --- a/src/screens/Profile/Header/index.tsx +++ b/src/screens/Profile/Header/index.tsx @@ -1,14 +1,25 @@ -import React, {memo} from 'react' -import {StyleSheet, View} from 'react-native' +import React, {memo, useState} from 'react' +import {LayoutChangeEvent, StyleSheet, View} from 'react-native' +import Animated, { + runOnJS, + useAnimatedReaction, + useAnimatedStyle, + withTiming, +} from 'react-native-reanimated' +import {useSafeAreaInsets} from 'react-native-safe-area-context' import { AppBskyActorDefs, AppBskyLabelerDefs, ModerationOpts, RichText as RichTextAPI, } from '@atproto/api' +import {useIsFocused} from '@react-navigation/native' +import {isNative} from '#/platform/detection' +import {useSetLightStatusBar} from '#/state/shell/light-status-bar' +import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext' import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' -import {useTheme} from '#/alf' +import {atoms as a, useTheme} from '#/alf' import {ProfileHeaderLabeler} from './ProfileHeaderLabeler' import {ProfileHeaderStandard} from './ProfileHeaderStandard' @@ -43,20 +54,114 @@ interface Props { moderationOpts: ModerationOpts hideBackButton?: boolean isPlaceholderProfile?: boolean + setMinimumHeight: (height: number) => void } -let ProfileHeader = (props: Props): React.ReactNode => { +let ProfileHeader = ({setMinimumHeight, ...props}: Props): React.ReactNode => { + let content if (props.profile.associated?.labeler) { if (!props.labeler) { - return + content = + } else { + content = } - return + } else { + content = } - return + + return ( + <> + {isNative && ( + setMinimumHeight(evt.nativeEvent.layout.height)} + profile={props.profile} + hideBackButton={props.hideBackButton} + /> + )} + {content} + + ) } ProfileHeader = memo(ProfileHeader) export {ProfileHeader} +const MinimalHeader = React.memo(function MinimalHeader({ + onLayout, +}: { + onLayout: (e: LayoutChangeEvent) => void + profile: AppBskyActorDefs.ProfileViewDetailed + hideBackButton?: boolean +}) { + const t = useTheme() + const insets = useSafeAreaInsets() + const ctx = usePagerHeaderContext() + const [visible, setVisible] = useState(false) + const [minimalHeaderHeight, setMinimalHeaderHeight] = React.useState(0) + const isScreenFocused = useIsFocused() + if (!ctx) throw new Error('MinimalHeader cannot be used on web') + const {scrollY, headerHeight} = ctx + + const animatedStyle = useAnimatedStyle(() => { + // if we don't yet have the min header height in JS, hide + if (!_WORKLET || minimalHeaderHeight === 0) { + return { + opacity: 0, + } + } + const pastThreshold = scrollY.get() > 100 + return { + opacity: pastThreshold + ? withTiming(1, {duration: 75}) + : withTiming(0, {duration: 75}), + transform: [ + { + translateY: Math.min( + scrollY.get(), + headerHeight - minimalHeaderHeight, + ), + }, + ], + } + }) + + useAnimatedReaction( + () => scrollY.get() > 100, + (value, prev) => { + if (prev !== value) { + runOnJS(setVisible)(value) + } + }, + ) + + useSetLightStatusBar(isScreenFocused && !visible) + + return ( + { + setMinimalHeaderHeight(evt.nativeEvent.layout.height) + onLayout(evt) + }} + style={[ + a.absolute, + a.z_50, + t.atoms.bg, + { + top: 0, + left: 0, + right: 0, + paddingTop: insets.top, + }, + animatedStyle, + ]} + /> + ) +}) +MinimalHeader.displayName = 'MinimalHeader' + const styles = StyleSheet.create({ avi: { position: 'absolute', diff --git a/src/state/shell/light-status-bar.tsx b/src/state/shell/light-status-bar.tsx new file mode 100644 index 0000000000..eb213adb93 --- /dev/null +++ b/src/state/shell/light-status-bar.tsx @@ -0,0 +1,45 @@ +import {createContext, useContext, useEffect, useState} from 'react' + +import {isWeb} from '#/platform/detection' +import {IS_DEV} from '#/env' + +const LightStatusBarRefCountContext = createContext(false) +const SetLightStatusBarRefCountContext = createContext +> | null>(null) + +export function useLightStatusBar() { + return useContext(LightStatusBarRefCountContext) +} + +export function useSetLightStatusBar(enabled: boolean) { + const setRefCount = useContext(SetLightStatusBarRefCountContext) + useEffect(() => { + // noop on web -sfn + if (isWeb) return + + if (!setRefCount) { + if (IS_DEV) + console.error( + 'useLightStatusBar was used without a SetLightStatusBarRefCountContext provider', + ) + return + } + if (enabled) { + setRefCount(prev => prev + 1) + return () => setRefCount(prev => prev - 1) + } + }, [enabled, setRefCount]) +} + +export function Provider({children}: React.PropsWithChildren<{}>) { + const [refCount, setRefCount] = useState(0) + + return ( + + 0}> + {children} + + + ) +} diff --git a/src/view/com/pager/PagerHeaderContext.tsx b/src/view/com/pager/PagerHeaderContext.tsx index fd4cc74632..c979f7a6dd 100644 --- a/src/view/com/pager/PagerHeaderContext.tsx +++ b/src/view/com/pager/PagerHeaderContext.tsx @@ -1,40 +1,48 @@ import React, {useContext} from 'react' import {SharedValue} from 'react-native-reanimated' -import {isIOS} from '#/platform/detection' +import {isNative} from '#/platform/detection' -export const PagerHeaderContext = - React.createContext | null>(null) +export const PagerHeaderContext = React.createContext<{ + scrollY: SharedValue + headerHeight: number +} | null>(null) /** - * Passes the scrollY value to the pager header's banner, so it can grow on - * overscroll on iOS. Not necessary to use this context provider on other platforms. + * Passes information about the scroll position and header height down via + * context for the pager header to consume. * - * @platform ios + * @platform ios, android */ export function PagerHeaderProvider({ scrollY, + headerHeight, children, }: { scrollY: SharedValue + headerHeight: number children: React.ReactNode }) { + const value = React.useMemo( + () => ({scrollY, headerHeight}), + [scrollY, headerHeight], + ) return ( - + {children} ) } export function usePagerHeaderContext() { - const scrollY = useContext(PagerHeaderContext) - if (isIOS) { - if (!scrollY) { + const ctx = useContext(PagerHeaderContext) + if (isNative) { + if (!ctx) { throw new Error( 'usePagerHeaderContext must be used within a HeaderProvider', ) } - return {scrollY} + return ctx } else { return null } diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx index 6174459647..dcf141f84d 100644 --- a/src/view/com/pager/PagerWithHeader.tsx +++ b/src/view/com/pager/PagerWithHeader.tsx @@ -38,7 +38,11 @@ export interface PagerWithHeaderProps { | ((props: PagerWithHeaderChildParams) => JSX.Element) items: string[] isHeaderReady: boolean - renderHeader?: () => JSX.Element + renderHeader?: ({ + setMinimumHeight, + }: { + setMinimumHeight: (height: number) => void + }) => JSX.Element initialPage?: number onPageSelected?: (index: number) => void onCurrentPageSelected?: (index: number) => void @@ -83,7 +87,9 @@ export const PagerWithHeader = React.forwardRef( const renderTabBar = React.useCallback( (props: RenderTabBarFnProps) => { return ( - + - renderHeader?: () => JSX.Element + renderHeader?: ({ + setMinimumHeight, + }: { + setMinimumHeight: (height: number) => void + }) => JSX.Element onHeaderOnlyLayout: (height: number) => void onTabBarLayout: (e: LayoutChangeEvent) => void onCurrentPageSelected?: (index: number) => void @@ -246,8 +256,13 @@ let PagerTabBar = ({ dragProgress: SharedValue dragState: SharedValue<'idle' | 'dragging' | 'settling'> }): React.ReactNode => { + const [minimumHeaderHeight, setMinimumHeaderHeight] = React.useState(0) const headerTransform = useAnimatedStyle(() => { - const translateY = Math.min(scrollY.get(), headerOnlyHeight) * -1 + const translateY = + Math.min( + scrollY.get(), + Math.max(headerOnlyHeight - minimumHeaderHeight, 0), + ) * -1 return { transform: [ { @@ -267,7 +282,7 @@ let PagerTabBar = ({ ref={headerRef} pointerEvents={isIOS ? 'auto' : 'box-none'} collapsable={false}> - {renderHeader?.()} + {renderHeader?.({setMinimumHeight: setMinimumHeaderHeight})} { // It wouldn't be enough to place `onLayout` on the parent node because // this would risk measuring before `isHeaderReady` has turned `true`. diff --git a/src/view/com/pager/PagerWithHeader.web.tsx b/src/view/com/pager/PagerWithHeader.web.tsx index 3335532b3d..98b32b3476 100644 --- a/src/view/com/pager/PagerWithHeader.web.tsx +++ b/src/view/com/pager/PagerWithHeader.web.tsx @@ -21,7 +21,11 @@ export interface PagerWithHeaderProps { | ((props: PagerWithHeaderChildParams) => JSX.Element) items: string[] isHeaderReady: boolean - renderHeader?: () => JSX.Element + renderHeader?: ({ + setMinimumHeight, + }: { + setMinimumHeight: () => void + }) => JSX.Element initialPage?: number onPageSelected?: (index: number) => void onCurrentPageSelected?: (index: number) => void @@ -115,7 +119,11 @@ let PagerTabBar = ({ currentPage: number items: string[] testID?: string - renderHeader?: () => JSX.Element + renderHeader?: ({ + setMinimumHeight, + }: { + setMinimumHeight: () => void + }) => JSX.Element isHeaderReady: boolean onCurrentPageSelected?: (index: number) => void onSelect?: (index: number) => void @@ -123,7 +131,7 @@ let PagerTabBar = ({ }): React.ReactNode => { return ( <> - {renderHeader?.()} + {renderHeader?.({setMinimumHeight: noop})} {tabBarAnchor} (v: T | T[]): T[] { } return [v] } + +function noop() {} diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 782e9b9c84..ebf1d955d8 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -43,6 +43,7 @@ import {ListRef} from '#/view/com/util/List' import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header' import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed' import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels' +import {atoms as a} from '#/alf' import * as Layout from '#/components/Layout' import {ScreenHider} from '#/components/moderation/ScreenHider' import {ProfileStarterPacks} from '#/components/StarterPack/ProfileStarterPacks' @@ -56,7 +57,7 @@ interface SectionRef { type Props = NativeStackScreenProps export function ProfileScreen(props: Props) { return ( - + ) @@ -329,7 +330,11 @@ function ProfileScreenLoaded({ // rendering // = - const renderHeader = () => { + const renderHeader = ({ + setMinimumHeight, + }: { + setMinimumHeight: (height: number) => void + }) => { return ( ) diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index 179e8858e0..a5e97610d0 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -18,6 +18,7 @@ import { useIsDrawerSwipeDisabled, useSetDrawerOpen, } from '#/state/shell' +import {useLightStatusBar} from '#/state/shell/light-status-bar' import {useCloseAnyActiveElement} from '#/state/util' import {Lightbox} from '#/view/com/lightbox/Lightbox' import {ModalsContainer} from '#/view/com/modals/Modal' @@ -154,6 +155,7 @@ function ShellInner() { export const Shell: React.FC = function ShellImpl() { const {fullyExpandedCount} = useDialogStateControlContext() + const lightStatusBar = useLightStatusBar() const t = useTheme() useIntentHandler() @@ -165,7 +167,9 @@ export const Shell: React.FC = function ShellImpl() { 0) + t.name !== 'light' || + (isIOS && fullyExpandedCount > 0) || + lightStatusBar ? 'light' : 'dark' } From 2808f8b73da4791e0f3cb85b03403738e5bf6b63 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 12 Dec 2024 11:48:26 -0600 Subject: [PATCH 18/58] New profile feed header (#7056) * Init hacking * Lil baby button checkpoint * Playing around * Revert "Playing around" This reverts commit f58a7fafa12269035d440cfa2d8cb1dbd562305f. * Mostly there * Cleanups * Cleanup * Fix report dialog nesting * Remove transform on native * Rename header * Fix layout, overflowing FAB buttons * Remove hack * Couple of fixes * Keep Pin primary CTA (#7061) * Update src/screens/Profile/components/ProfileFeedHeader.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Simplify, use old string * Wrap Trans better --------- Co-authored-by: dan Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> --- .../pin_filled_stroke2_corner0_rounded.svg | 1 + src/Navigation.tsx | 2 +- src/alf/themes.ts | 4 + src/alf/types.ts | 1 + src/components/Layout/index.tsx | 4 +- src/components/icons/Pin.tsx | 4 + src/screens/Profile/ProfileFeed/index.tsx | 227 +++++++ .../Profile/components/ProfileFeedHeader.tsx | 534 +++++++++++++++ src/view/screens/ProfileFeed.tsx | 621 ------------------ 9 files changed, 775 insertions(+), 623 deletions(-) create mode 100644 assets/icons/pin_filled_stroke2_corner0_rounded.svg create mode 100644 src/screens/Profile/ProfileFeed/index.tsx create mode 100644 src/screens/Profile/components/ProfileFeedHeader.tsx delete mode 100644 src/view/screens/ProfileFeed.tsx diff --git a/assets/icons/pin_filled_stroke2_corner0_rounded.svg b/assets/icons/pin_filled_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..a2e71b967c --- /dev/null +++ b/assets/icons/pin_filled_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/src/Navigation.tsx b/src/Navigation.tsx index cf00215264..7443128d2c 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -55,7 +55,6 @@ import {NotificationsScreen} from '#/view/screens/Notifications' import {PostThreadScreen} from '#/view/screens/PostThread' import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy' import {ProfileScreen} from '#/view/screens/Profile' -import {ProfileFeedScreen} from '#/view/screens/ProfileFeed' import {ProfileFeedLikedByScreen} from '#/view/screens/ProfileFeedLikedBy' import {ProfileListScreen} from '#/view/screens/ProfileList' import {SavedFeeds} from '#/view/screens/SavedFeeds' @@ -75,6 +74,7 @@ import {PostLikedByScreen} from '#/screens/Post/PostLikedBy' import {PostQuotesScreen} from '#/screens/Post/PostQuotes' import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy' import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers' +import {ProfileFeedScreen} from '#/screens/Profile/ProfileFeed' import {ProfileFollowersScreen} from '#/screens/Profile/ProfileFollowers' import {ProfileFollowsScreen} from '#/screens/Profile/ProfileFollows' import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy' diff --git a/src/alf/themes.ts b/src/alf/themes.ts index 0cfe09aadc..cb97a7065b 100644 --- a/src/alf/themes.ts +++ b/src/alf/themes.ts @@ -60,6 +60,7 @@ export function createThemes({ dim: Theme } { const color = { + like: '#ec4899', trueBlack: '#000000', gray_0: `hsl(${hues.primary}, 20%, ${defaultScale[14]}%)`, @@ -124,6 +125,7 @@ export function createThemes({ const lightPalette = { white: color.gray_0, black: color.gray_1000, + like: color.like, contrast_25: color.gray_25, contrast_50: color.gray_50, @@ -185,6 +187,7 @@ export function createThemes({ const darkPalette: Palette = { white: color.gray_25, black: color.trueBlack, + like: color.like, contrast_25: color.gray_975, contrast_50: color.gray_950, @@ -246,6 +249,7 @@ export function createThemes({ const dimPalette: Palette = { ...darkPalette, black: `hsl(${hues.primary}, 28%, ${dimScale[0]}%)`, + like: color.like, contrast_25: `hsl(${hues.primary}, 28%, ${dimScale[1]}%)`, contrast_50: `hsl(${hues.primary}, 28%, ${dimScale[2]}%)`, diff --git a/src/alf/types.ts b/src/alf/types.ts index 08ec593927..5bac690e2b 100644 --- a/src/alf/types.ts +++ b/src/alf/types.ts @@ -12,6 +12,7 @@ export type ThemeName = 'light' | 'dim' | 'dark' export type Palette = { white: string black: string + like: string contrast_25: string contrast_50: string diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index d08505fbfd..8532cbbb49 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -21,6 +21,7 @@ export * as Header from '#/components/Layout/Header' export type ScreenProps = React.ComponentProps & { style?: StyleProp + noInsetTop?: boolean } /** @@ -28,6 +29,7 @@ export type ScreenProps = React.ComponentProps & { */ export const Screen = React.memo(function Screen({ style, + noInsetTop, ...props }: ScreenProps) { const {top} = useSafeAreaInsets() @@ -35,7 +37,7 @@ export const Screen = React.memo(function Screen({ <> {isWeb && } diff --git a/src/components/icons/Pin.tsx b/src/components/icons/Pin.tsx index 03dbbac90e..d1c37f39a3 100644 --- a/src/components/icons/Pin.tsx +++ b/src/components/icons/Pin.tsx @@ -3,3 +3,7 @@ import {createSinglePathSVG} from './TEMPLATE' export const Pin_Stroke2_Corner0_Rounded = createSinglePathSVG({ path: 'M6.5 3a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v3.997a6.25 6.25 0 0 0 1.83 4.42l.377.376A1 1 0 0 1 20 12.5V15a1 1 0 0 1-1 1h-6v5a1 1 0 1 1-2 0v-5H5a1 1 0 0 1-1-1v-2.5a1 1 0 0 1 .293-.707l.376-.377A6.25 6.25 0 0 0 6.5 6.996V3.001Zm2 1v2.997a8.25 8.25 0 0 1-2.416 5.834L6 12.914V14h12v-1.086l-.084-.083A8.25 8.25 0 0 1 15.5 6.997V4h-7Z', }) + +export const Pin_Filled_Corner0_Rounded = createSinglePathSVG({ + path: 'M7.5 2a1 1 0 0 0-1 1v3.997a6.25 6.25 0 0 1-1.83 4.42l-.377.376A1 1 0 0 0 4 12.5V15a1 1 0 0 0 1 1h6v5a1 1 0 1 0 2 0v-5h6a1 1 0 0 0 1-1v-2.5a1 1 0 0 0-.293-.707l-.376-.377a6.25 6.25 0 0 1-1.831-4.42V3.001a1 1 0 0 0-1-1h-9Z', +}) diff --git a/src/screens/Profile/ProfileFeed/index.tsx b/src/screens/Profile/ProfileFeed/index.tsx new file mode 100644 index 0000000000..7d48b5ac15 --- /dev/null +++ b/src/screens/Profile/ProfileFeed/index.tsx @@ -0,0 +1,227 @@ +import React, {useCallback, useMemo} from 'react' +import {StyleSheet, View} from 'react-native' +import {useAnimatedRef} from 'react-native-reanimated' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useIsFocused, useNavigation} from '@react-navigation/native' +import {NativeStackScreenProps} from '@react-navigation/native-stack' +import {useQueryClient} from '@tanstack/react-query' + +import {usePalette} from '#/lib/hooks/usePalette' +import {useSetTitle} from '#/lib/hooks/useSetTitle' +import {ComposeIcon2} from '#/lib/icons' +import {CommonNavigatorParams} from '#/lib/routes/types' +import {NavigationProp} from '#/lib/routes/types' +import {makeRecordUri} from '#/lib/strings/url-helpers' +import {s} from '#/lib/styles' +import {isNative} from '#/platform/detection' +import {listenSoftReset} from '#/state/events' +import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback' +import {FeedSourceFeedInfo, useFeedSourceInfoQuery} from '#/state/queries/feed' +import {FeedDescriptor} from '#/state/queries/post-feed' +import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' +import { + usePreferencesQuery, + UsePreferencesQueryResponse, +} from '#/state/queries/preferences' +import {useResolveUriQuery} from '#/state/queries/resolve-uri' +import {truncateAndInvalidate} from '#/state/queries/util' +import {useSession} from '#/state/session' +import {useComposerControls} from '#/state/shell/composer' +import {PostFeed} from '#/view/com/posts/PostFeed' +import {EmptyState} from '#/view/com/util/EmptyState' +import {FAB} from '#/view/com/util/fab/FAB' +import {Button} from '#/view/com/util/forms/Button' +import {ListRef} from '#/view/com/util/List' +import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn' +import {LoadingScreen} from '#/view/com/util/LoadingScreen' +import {Text} from '#/view/com/util/text/Text' +import {ProfileFeedHeader} from '#/screens/Profile/components/ProfileFeedHeader' +import * as Layout from '#/components/Layout' + +type Props = NativeStackScreenProps +export function ProfileFeedScreen(props: Props) { + const {rkey, name: handleOrDid} = props.route.params + + const pal = usePalette('default') + const {_} = useLingui() + const navigation = useNavigation() + + const uri = useMemo( + () => makeRecordUri(handleOrDid, 'app.bsky.feed.generator', rkey), + [rkey, handleOrDid], + ) + const {error, data: resolvedUri} = useResolveUriQuery(uri) + + const onPressBack = React.useCallback(() => { + if (navigation.canGoBack()) { + navigation.goBack() + } else { + navigation.navigate('Home') + } + }, [navigation]) + + if (error) { + return ( + + + + + Could not load feed + + + {error.toString()} + + + + + + + + + ) + } + + return resolvedUri ? ( + + + + ) : ( + + + + ) +} + +function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) { + const {data: preferences} = usePreferencesQuery() + const {data: info} = useFeedSourceInfoQuery({uri: feedUri}) + + if (!preferences || !info) { + return + } + + return ( + + ) +} + +export function ProfileFeedScreenInner({ + feedInfo, +}: { + preferences: UsePreferencesQueryResponse + feedInfo: FeedSourceFeedInfo +}) { + const {_} = useLingui() + const {hasSession} = useSession() + const {openComposer} = useComposerControls() + const isScreenFocused = useIsFocused() + + useSetTitle(feedInfo?.displayName) + + const feed = `feedgen|${feedInfo.uri}` as FeedDescriptor + + const [hasNew, setHasNew] = React.useState(false) + const [isScrolledDown, setIsScrolledDown] = React.useState(false) + const queryClient = useQueryClient() + const feedFeedback = useFeedFeedback(feed, hasSession) + const scrollElRef = useAnimatedRef() as ListRef + + const onScrollToTop = useCallback(() => { + scrollElRef.current?.scrollToOffset({ + animated: isNative, + offset: 0, // -headerHeight, + }) + truncateAndInvalidate(queryClient, FEED_RQKEY(feed)) + setHasNew(false) + }, [scrollElRef, queryClient, feed, setHasNew]) + + React.useEffect(() => { + if (!isScreenFocused) { + return + } + return listenSoftReset(onScrollToTop) + }, [onScrollToTop, isScreenFocused]) + + const renderPostsEmpty = useCallback(() => { + return + }, [_]) + + return ( + <> + + + + + + + {(isScrolledDown || hasNew) && ( + + )} + + {hasSession && ( + openComposer({})} + icon={ + + } + accessibilityRole="button" + accessibilityLabel={_(msg`New post`)} + accessibilityHint="" + /> + )} + + ) +} + +const styles = StyleSheet.create({ + btn: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + paddingVertical: 7, + paddingHorizontal: 14, + borderRadius: 50, + marginLeft: 6, + }, + notFoundContainer: { + margin: 10, + paddingHorizontal: 18, + paddingVertical: 14, + borderRadius: 6, + }, + aboutSectionContainer: { + paddingVertical: 4, + paddingHorizontal: 16, + gap: 12, + }, +}) diff --git a/src/screens/Profile/components/ProfileFeedHeader.tsx b/src/screens/Profile/components/ProfileFeedHeader.tsx new file mode 100644 index 0000000000..0154d535c2 --- /dev/null +++ b/src/screens/Profile/components/ProfileFeedHeader.tsx @@ -0,0 +1,534 @@ +import React from 'react' +import {View} from 'react-native' +import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {AtUri} from '@atproto/api' +import {msg, Plural, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useHaptics} from '#/lib/haptics' +import {makeProfileLink} from '#/lib/routes/links' +import {makeCustomFeedLink} from '#/lib/routes/links' +import {shareUrl} from '#/lib/sharing' +import {sanitizeHandle} from '#/lib/strings/handles' +import {toShareUrl} from '#/lib/strings/url-helpers' +import {logger} from '#/logger' +import {FeedSourceFeedInfo} from '#/state/queries/feed' +import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like' +import { + useAddSavedFeedsMutation, + usePreferencesQuery, + useRemoveFeedMutation, + useUpdateSavedFeedsMutation, +} from '#/state/queries/preferences' +import {useSession} from '#/state/session' +import {formatCount} from '#/view/com/util/numeric/format' +import * as Toast from '#/view/com/util/Toast' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {Divider} from '#/components/Divider' +import {useRichText} from '#/components/hooks/useRichText' +import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' +import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron' +import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' +import { + Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled, + Heart2_Stroke2_Corner0_Rounded as Heart, +} from '#/components/icons/Heart2' +import { + Pin_Filled_Corner0_Rounded as PinFilled, + Pin_Stroke2_Corner0_Rounded as Pin, +} from '#/components/icons/Pin' +import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' +import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' +import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' +import * as Layout from '#/components/Layout' +import {InlineLinkText} from '#/components/Link' +import {Loader} from '#/components/Loader' +import * as Menu from '#/components/Menu' +import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' +import {RichText} from '#/components/RichText' +import {Text} from '#/components/Typography' + +export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { + const t = useTheme() + const {_, i18n} = useLingui() + const {hasSession} = useSession() + const {gtPhone, gtMobile} = useBreakpoints() + const {top} = useSafeAreaInsets() + const infoControl = Dialog.useDialogControl() + const playHaptic = useHaptics() + + const {data: preferences} = usePreferencesQuery() + + const [likeUri, setLikeUri] = React.useState(info.likeUri || '') + const isLiked = !!likeUri + const likeCount = + isLiked && likeUri ? (info.likeCount || 0) + 1 : info.likeCount || 0 + + const {mutateAsync: addSavedFeeds, isPending: isAddSavedFeedPending} = + useAddSavedFeedsMutation() + const {mutateAsync: removeFeed, isPending: isRemovePending} = + useRemoveFeedMutation() + const {mutateAsync: updateSavedFeeds, isPending: isUpdateFeedPending} = + useUpdateSavedFeedsMutation() + + const isFeedStateChangePending = + isAddSavedFeedPending || isRemovePending || isUpdateFeedPending + const savedFeedConfig = preferences?.savedFeeds?.find( + f => f.value === info.uri, + ) + const isSaved = Boolean(savedFeedConfig) + const isPinned = Boolean(savedFeedConfig?.pinned) + + const onToggleSaved = React.useCallback(async () => { + try { + playHaptic() + + if (savedFeedConfig) { + await removeFeed(savedFeedConfig) + Toast.show(_(msg`Removed from your feeds`)) + } else { + await addSavedFeeds([ + { + type: 'feed', + value: info.uri, + pinned: false, + }, + ]) + Toast.show(_(msg`Saved to your feeds`)) + } + } catch (err) { + Toast.show( + _( + msg`There was an issue updating your feeds, please check your internet connection and try again.`, + ), + 'xmark', + ) + logger.error('Failed to update feeds', {message: err}) + } + }, [_, playHaptic, info, removeFeed, addSavedFeeds, savedFeedConfig]) + + const onTogglePinned = React.useCallback(async () => { + try { + playHaptic() + + if (savedFeedConfig) { + const pinned = !savedFeedConfig.pinned + await updateSavedFeeds([ + { + ...savedFeedConfig, + pinned, + }, + ]) + + if (pinned) { + Toast.show(_(msg`Pinned ${info.displayName} to Home`)) + } else { + Toast.show(_(msg`Unpinned ${info.displayName} from Home`)) + } + } else { + await addSavedFeeds([ + { + type: 'feed', + value: info.uri, + pinned: true, + }, + ]) + Toast.show(_(msg`Pinned ${info.displayName} to Home`)) + } + } catch (e) { + Toast.show(_(msg`There was an issue contacting the server`), 'xmark') + logger.error('Failed to toggle pinned feed', {message: e}) + } + }, [playHaptic, info, _, savedFeedConfig, updateSavedFeeds, addSavedFeeds]) + + return ( + <> + + + + + + + + {hasSession && ( + + {isPinned ? ( + + + {({props}) => { + return ( + + ) + }} + + + + + {_(msg`Unpin from home`)} + + + + + {isSaved + ? _(msg`Remove from my feeds`) + : _(msg`Save to my feeds`)} + + + + + + ) : ( + + )} + + )} + + + + + + + + + + + ) +} + +function DialogInner({ + info, + likeUri, + setLikeUri, + likeCount, + isPinned, + onTogglePinned, + isFeedStateChangePending, +}: { + info: FeedSourceFeedInfo + likeUri: string + setLikeUri: (uri: string) => void + likeCount: number + isPinned: boolean + onTogglePinned: () => void + isFeedStateChangePending: boolean +}) { + const t = useTheme() + const {_} = useLingui() + const {hasSession} = useSession() + const playHaptic = useHaptics() + const control = Dialog.useDialogContext() + const reportDialogControl = useReportDialogControl() + const [rt, loading] = useRichText(info.description.text) + const {mutateAsync: likeFeed, isPending: isLikePending} = useLikeMutation() + const {mutateAsync: unlikeFeed, isPending: isUnlikePending} = + useUnlikeMutation() + + const isLiked = !!likeUri + const feedRkey = React.useMemo(() => new AtUri(info.uri).rkey, [info.uri]) + + const onToggleLiked = React.useCallback(async () => { + try { + playHaptic() + + if (isLiked && likeUri) { + await unlikeFeed({uri: likeUri}) + setLikeUri('') + } else { + const res = await likeFeed({uri: info.uri, cid: info.cid}) + setLikeUri(res.uri) + } + } catch (err) { + Toast.show( + _( + msg`There was an issue contacting the server, please check your internet connection and try again.`, + ), + 'xmark', + ) + logger.error('Failed to toggle like', {message: err}) + } + }, [playHaptic, isLiked, likeUri, unlikeFeed, setLikeUri, likeFeed, info, _]) + + const onPressShare = React.useCallback(() => { + playHaptic() + const url = toShareUrl(info.route.href) + shareUrl(url) + }, [info, playHaptic]) + + const onPressReport = React.useCallback(() => { + reportDialogControl.open() + }, [reportDialogControl]) + + return loading ? ( + + ) : ( + + + + + + + {info.displayName} + + + + By{' '} + control.close()}> + {sanitizeHandle(info.creatorHandle, '@')} + + + + + + + + + + + + {typeof likeCount === 'number' && ( + control.close()}> + + Liked by + + + )} + + + {hasSession && ( + <> + + + + + + + + + + + Something wrong? Let us know. + + + + + + + + + )} + + ) +} diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx deleted file mode 100644 index c3f98c067f..0000000000 --- a/src/view/screens/ProfileFeed.tsx +++ /dev/null @@ -1,621 +0,0 @@ -import React, {useCallback, useMemo} from 'react' -import {Pressable, StyleSheet, View} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {msg, Plural, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useIsFocused, useNavigation} from '@react-navigation/native' -import {NativeStackScreenProps} from '@react-navigation/native-stack' -import {useQueryClient} from '@tanstack/react-query' - -import {HITSLOP_20} from '#/lib/constants' -import {useHaptics} from '#/lib/haptics' -import {usePalette} from '#/lib/hooks/usePalette' -import {useSetTitle} from '#/lib/hooks/useSetTitle' -import {ComposeIcon2} from '#/lib/icons' -import {makeCustomFeedLink} from '#/lib/routes/links' -import {CommonNavigatorParams} from '#/lib/routes/types' -import {NavigationProp} from '#/lib/routes/types' -import {shareUrl} from '#/lib/sharing' -import {makeRecordUri} from '#/lib/strings/url-helpers' -import {toShareUrl} from '#/lib/strings/url-helpers' -import {s} from '#/lib/styles' -import {logger} from '#/logger' -import {isNative} from '#/platform/detection' -import {listenSoftReset} from '#/state/events' -import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback' -import {FeedSourceFeedInfo, useFeedSourceInfoQuery} from '#/state/queries/feed' -import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like' -import {FeedDescriptor} from '#/state/queries/post-feed' -import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' -import { - useAddSavedFeedsMutation, - usePreferencesQuery, - UsePreferencesQueryResponse, - useRemoveFeedMutation, - useUpdateSavedFeedsMutation, -} from '#/state/queries/preferences' -import {useResolveUriQuery} from '#/state/queries/resolve-uri' -import {truncateAndInvalidate} from '#/state/queries/util' -import {useSession} from '#/state/session' -import {useComposerControls} from '#/state/shell/composer' -import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader' -import {PostFeed} from '#/view/com/posts/PostFeed' -import {ProfileSubpageHeader} from '#/view/com/profile/ProfileSubpageHeader' -import {EmptyState} from '#/view/com/util/EmptyState' -import {FAB} from '#/view/com/util/fab/FAB' -import {Button} from '#/view/com/util/forms/Button' -import {ListRef} from '#/view/com/util/List' -import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn' -import {LoadingScreen} from '#/view/com/util/LoadingScreen' -import {Text} from '#/view/com/util/text/Text' -import * as Toast from '#/view/com/util/Toast' -import {atoms as a, useTheme} from '#/alf' -import {Button as NewButton, ButtonText} from '#/components/Button' -import {useRichText} from '#/components/hooks/useRichText' -import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' -import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' -import { - Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled, - Heart2_Stroke2_Corner0_Rounded as HeartOutline, -} from '#/components/icons/Heart2' -import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' -import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' -import * as Layout from '#/components/Layout' -import {InlineLinkText} from '#/components/Link' -import * as Menu from '#/components/Menu' -import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' -import {RichText} from '#/components/RichText' - -const SECTION_TITLES = ['Posts'] - -interface SectionRef { - scrollToTop: () => void -} - -type Props = NativeStackScreenProps -export function ProfileFeedScreen(props: Props) { - const {rkey, name: handleOrDid} = props.route.params - - const pal = usePalette('default') - const {_} = useLingui() - const navigation = useNavigation() - - const uri = useMemo( - () => makeRecordUri(handleOrDid, 'app.bsky.feed.generator', rkey), - [rkey, handleOrDid], - ) - const {error, data: resolvedUri} = useResolveUriQuery(uri) - - const onPressBack = React.useCallback(() => { - if (navigation.canGoBack()) { - navigation.goBack() - } else { - navigation.navigate('Home') - } - }, [navigation]) - - if (error) { - return ( - - - - - Could not load feed - - - {error.toString()} - - - - - - - - - ) - } - - return resolvedUri ? ( - - - - ) : ( - - - - ) -} - -function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) { - const {data: preferences} = usePreferencesQuery() - const {data: info} = useFeedSourceInfoQuery({uri: feedUri}) - - if (!preferences || !info) { - return - } - - return ( - - ) -} - -export function ProfileFeedScreenInner({ - preferences, - feedInfo, -}: { - preferences: UsePreferencesQueryResponse - feedInfo: FeedSourceFeedInfo -}) { - const {_} = useLingui() - const t = useTheme() - const {hasSession, currentAccount} = useSession() - const reportDialogControl = useReportDialogControl() - const {openComposer} = useComposerControls() - const playHaptic = useHaptics() - const feedSectionRef = React.useRef(null) - const isScreenFocused = useIsFocused() - - const {mutateAsync: addSavedFeeds, isPending: isAddSavedFeedPending} = - useAddSavedFeedsMutation() - const {mutateAsync: removeFeed, isPending: isRemovePending} = - useRemoveFeedMutation() - const {mutateAsync: updateSavedFeeds, isPending: isUpdateFeedPending} = - useUpdateSavedFeedsMutation() - - const isPending = - isAddSavedFeedPending || isRemovePending || isUpdateFeedPending - const savedFeedConfig = preferences.savedFeeds.find( - f => f.value === feedInfo.uri, - ) - const isSaved = Boolean(savedFeedConfig) - const isPinned = Boolean(savedFeedConfig?.pinned) - - useSetTitle(feedInfo?.displayName) - - // event handlers - // - - const onToggleSaved = React.useCallback(async () => { - try { - playHaptic() - - if (savedFeedConfig) { - await removeFeed(savedFeedConfig) - Toast.show(_(msg`Removed from your feeds`)) - } else { - await addSavedFeeds([ - { - type: 'feed', - value: feedInfo.uri, - pinned: false, - }, - ]) - Toast.show(_(msg`Saved to your feeds`)) - } - } catch (err) { - Toast.show( - _( - msg`There was an issue updating your feeds, please check your internet connection and try again.`, - ), - 'xmark', - ) - logger.error('Failed to update feeds', {message: err}) - } - }, [_, playHaptic, feedInfo, removeFeed, addSavedFeeds, savedFeedConfig]) - - const onTogglePinned = React.useCallback(async () => { - try { - playHaptic() - - if (savedFeedConfig) { - await updateSavedFeeds([ - { - ...savedFeedConfig, - pinned: !savedFeedConfig.pinned, - }, - ]) - } else { - await addSavedFeeds([ - { - type: 'feed', - value: feedInfo.uri, - pinned: true, - }, - ]) - } - } catch (e) { - Toast.show(_(msg`There was an issue contacting the server`), 'xmark') - logger.error('Failed to toggle pinned feed', {message: e}) - } - }, [ - playHaptic, - feedInfo, - _, - savedFeedConfig, - updateSavedFeeds, - addSavedFeeds, - ]) - - const onPressShare = React.useCallback(() => { - const url = toShareUrl(feedInfo.route.href) - shareUrl(url) - }, [feedInfo]) - - const onPressReport = React.useCallback(() => { - reportDialogControl.open() - }, [reportDialogControl]) - - const onCurrentPageSelected = React.useCallback( - (index: number) => { - if (index === 0) { - feedSectionRef.current?.scrollToTop() - } - }, - [feedSectionRef], - ) - - const renderHeader = useCallback(() => { - return ( - <> - - - {feedInfo && hasSession && ( - - - {isPinned ? _(msg`Unpin`) : _(msg`Pin to Home`)} - - - )} - - - {({props, state}) => { - return ( - - - - ) - }} - - - - - {hasSession && ( - <> - - - {isSaved - ? _(msg`Remove from my feeds`) - : _(msg`Save to my feeds`)} - - - - - - {_(msg`Report feed`)} - - - - )} - - - {_(msg`Share feed`)} - - - - - - - - - - ) - }, [ - _, - hasSession, - feedInfo, - isPinned, - onTogglePinned, - onToggleSaved, - currentAccount?.did, - isSaved, - onPressReport, - onPressShare, - t, - isPending, - ]) - - return ( - <> - - - {({headerHeight, scrollElRef, isFocused}) => ( - - )} - - {hasSession && ( - openComposer({})} - icon={ - - } - accessibilityRole="button" - accessibilityLabel={_(msg`New post`)} - accessibilityHint="" - /> - )} - - ) -} - -interface FeedSectionProps { - feed: FeedDescriptor - headerHeight: number - scrollElRef: ListRef - isFocused: boolean -} -const FeedSection = React.forwardRef( - function FeedSectionImpl({feed, headerHeight, scrollElRef, isFocused}, ref) { - const {_} = useLingui() - const [hasNew, setHasNew] = React.useState(false) - const [isScrolledDown, setIsScrolledDown] = React.useState(false) - const queryClient = useQueryClient() - const isScreenFocused = useIsFocused() - const {hasSession} = useSession() - const feedFeedback = useFeedFeedback(feed, hasSession) - - const onScrollToTop = useCallback(() => { - scrollElRef.current?.scrollToOffset({ - animated: isNative, - offset: -headerHeight, - }) - truncateAndInvalidate(queryClient, FEED_RQKEY(feed)) - setHasNew(false) - }, [scrollElRef, headerHeight, queryClient, feed, setHasNew]) - - React.useImperativeHandle(ref, () => ({ - scrollToTop: onScrollToTop, - })) - - React.useEffect(() => { - if (!isScreenFocused) { - return - } - return listenSoftReset(onScrollToTop) - }, [onScrollToTop, isScreenFocused]) - - const renderPostsEmpty = useCallback(() => { - return - }, [_]) - - return ( - - - - - {(isScrolledDown || hasNew) && ( - - )} - - ) - }, -) - -function AboutSection({ - feedOwnerDid, - feedRkey, - feedInfo, -}: { - feedOwnerDid: string - feedRkey: string - feedInfo: FeedSourceFeedInfo -}) { - const t = useTheme() - const pal = usePalette('default') - const {_} = useLingui() - const [likeUri, setLikeUri] = React.useState(feedInfo.likeUri) - const {hasSession} = useSession() - const playHaptic = useHaptics() - const {mutateAsync: likeFeed, isPending: isLikePending} = useLikeMutation() - const {mutateAsync: unlikeFeed, isPending: isUnlikePending} = - useUnlikeMutation() - const [resolvedRT] = useRichText(feedInfo.description.text || '') - - const isLiked = !!likeUri - const likeCount = - isLiked && likeUri ? (feedInfo.likeCount || 0) + 1 : feedInfo.likeCount - - const onToggleLiked = React.useCallback(async () => { - try { - playHaptic() - - if (isLiked && likeUri) { - await unlikeFeed({uri: likeUri}) - setLikeUri('') - } else { - const res = await likeFeed({uri: feedInfo.uri, cid: feedInfo.cid}) - setLikeUri(res.uri) - } - } catch (err) { - Toast.show( - _( - msg`There was an issue contacting the server, please check your internet connection and try again.`, - ), - 'xmark', - ) - logger.error('Failed to toggle like', {message: err}) - } - }, [playHaptic, isLiked, likeUri, unlikeFeed, likeFeed, feedInfo, _]) - - return ( - - - {feedInfo.description ? ( - - ) : ( - - No description - - )} - - - - - {isLiked ? ( - - ) : ( - - )} - - {typeof likeCount === 'number' && ( - - - Liked by - - - )} - - - ) -} - -const styles = StyleSheet.create({ - btn: { - flexDirection: 'row', - alignItems: 'center', - gap: 6, - paddingVertical: 7, - paddingHorizontal: 14, - borderRadius: 50, - marginLeft: 6, - }, - notFoundContainer: { - margin: 10, - paddingHorizontal: 18, - paddingVertical: 14, - borderRadius: 6, - }, - aboutSectionContainer: { - paddingVertical: 4, - paddingHorizontal: 16, - gap: 12, - }, -}) From 2276cb0e75356779d175e40550469c8c04a7e69a Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 12 Dec 2024 11:50:29 -0800 Subject: [PATCH 19/58] Fix share button not working for some users (#5849) * fix share button * Revert "fix share button" This reverts commit 3521c241729dc9bbe3dd7b62fc6e3e61e011cdf9. * tweak * Clean up context --------- Co-authored-by: Eric Bailey --- src/components/Menu/context.tsx | 20 +++++++++++++++++ src/components/Menu/index.tsx | 36 +++++++++++++++---------------- src/components/Menu/index.web.tsx | 19 ++++++++-------- 3 files changed, 48 insertions(+), 27 deletions(-) diff --git a/src/components/Menu/context.tsx b/src/components/Menu/context.tsx index 1ddcd583fc..908ad352ea 100644 --- a/src/components/Menu/context.tsx +++ b/src/components/Menu/context.tsx @@ -10,3 +10,23 @@ export const Context = React.createContext({ export const ItemContext = React.createContext({ disabled: false, }) + +export function useMenuContext() { + const context = React.useContext(Context) + + if (!context) { + throw new Error('useMenuContext must be used within a Context.Provider') + } + + return context +} + +export function useMenuItemContext() { + const context = React.useContext(ItemContext) + + if (!context) { + throw new Error('useMenuItemContext must be used within a Context.Provider') + } + + return context +} diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx index a171e97723..73eb9da526 100644 --- a/src/components/Menu/index.tsx +++ b/src/components/Menu/index.tsx @@ -9,7 +9,12 @@ import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {useInteractionState} from '#/components/hooks/useInteractionState' -import {Context, ItemContext} from '#/components/Menu/context' +import { + Context, + ItemContext, + useMenuContext, + useMenuItemContext, +} from '#/components/Menu/context' import { ContextType, GroupProps, @@ -25,10 +30,6 @@ export { useDialogControl as useMenuControl, } from '#/components/Dialog' -export function useMemoControlContext() { - return React.useContext(Context) -} - export function Root({ children, control, @@ -47,7 +48,7 @@ export function Root({ } export function Trigger({children, label, role = 'button'}: TriggerProps) { - const {control} = React.useContext(Context) + const context = useMenuContext() const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() const { state: pressed, @@ -57,14 +58,14 @@ export function Trigger({children, label, role = 'button'}: TriggerProps) { return children({ isNative: true, - control, + control: context.control, state: { hovered: false, focused, pressed, }, props: { - onPress: control.open, + onPress: context.control.open, onFocus, onBlur, onPressIn, @@ -82,7 +83,7 @@ export function Outer({ showCancel?: boolean style?: StyleProp }>) { - const context = React.useContext(Context) + const context = useMenuContext() const {_} = useLingui() return ( @@ -105,7 +106,7 @@ export function Outer({ export function Item({children, label, style, onPress, ...rest}: ItemProps) { const t = useTheme() - const {control} = React.useContext(Context) + const context = useMenuContext() const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() const { state: pressed, @@ -121,10 +122,9 @@ export function Item({children, label, style, onPress, ...rest}: ItemProps) { onFocus={onFocus} onBlur={onBlur} onPress={async e => { - await onPress(e) - if (!e.defaultPrevented) { - control?.close() - } + context.control.close(() => { + onPress?.(e) + }) }} onPressIn={e => { onPressIn() @@ -156,7 +156,7 @@ export function Item({children, label, style, onPress, ...rest}: ItemProps) { export function ItemText({children, style}: ItemTextProps) { const t = useTheme() - const {disabled} = React.useContext(ItemContext) + const {disabled} = useMenuItemContext() return ( control.close()}> + onPress={() => context.control.close()}> Cancel diff --git a/src/components/Menu/index.web.tsx b/src/components/Menu/index.web.tsx index 37ad67e29f..ab0c9d20a1 100644 --- a/src/components/Menu/index.web.tsx +++ b/src/components/Menu/index.web.tsx @@ -7,7 +7,12 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu' import {atoms as a, flatten, useTheme, web} from '#/alf' import * as Dialog from '#/components/Dialog' import {useInteractionState} from '#/components/hooks/useInteractionState' -import {Context, ItemContext} from '#/components/Menu/context' +import { + Context, + ItemContext, + useMenuContext, + useMenuItemContext, +} from '#/components/Menu/context' import { ContextType, GroupProps, @@ -40,10 +45,6 @@ export function useMenuControl(): Dialog.DialogControlProps { ) } -export function useMemoControlContext() { - return React.useContext(Context) -} - export function Root({ children, control, @@ -110,7 +111,7 @@ const RadixTriggerPassThrough = React.forwardRef( RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough' export function Trigger({children, label, role = 'button'}: TriggerProps) { - const {control} = React.useContext(Context) + const {control} = useMenuContext() const { state: hovered, onIn: onMouseEnter, @@ -203,7 +204,7 @@ export function Outer({ export function Item({children, label, onPress, ...rest}: ItemProps) { const t = useTheme() - const {control} = React.useContext(Context) + const {control} = useMenuContext() const { state: hovered, onIn: onMouseEnter, @@ -262,7 +263,7 @@ export function Item({children, label, onPress, ...rest}: ItemProps) { export function ItemText({children, style}: ItemTextProps) { const t = useTheme() - const {disabled} = React.useContext(ItemContext) + const {disabled} = useMenuItemContext() return ( Date: Thu, 12 Dec 2024 21:16:15 +0000 Subject: [PATCH 20/58] Fix double border due to progress guide (#7084) --- src/components/FeedInterstitials.tsx | 22 +++------------------- src/view/com/posts/PostFeed.tsx | 9 ++++++++- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx index 286ded13e2..85ed58280d 100644 --- a/src/components/FeedInterstitials.tsx +++ b/src/components/FeedInterstitials.tsx @@ -6,7 +6,6 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {NavigationProp} from '#/lib/routes/types' import {logEvent} from '#/lib/statsig/statsig' import {logger} from '#/logger' @@ -16,7 +15,6 @@ import {FeedDescriptor} from '#/state/queries/post-feed' import {useProfilesQuery} from '#/state/queries/profile' import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows' import {useSession} from '#/state/session' -import {useProgressGuide} from '#/state/shell/progress-guide' import * as userActionHistory from '#/state/userActionHistory' import {SeenPost} from '#/state/userActionHistory' import {atoms as a, useBreakpoints, useTheme, ViewStyleProp, web} from '#/alf' @@ -506,23 +504,9 @@ export function SuggestedFeeds() { export function ProgressGuide() { const t = useTheme() - const {isDesktop} = useWebMediaQueries() - const guide = useProgressGuide('like-10-and-follow-7') - - if (isDesktop) { - return null - } - - return guide ? ( - + return ( + - ) : null + ) } diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index d81137113b..6746703379 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -16,6 +16,7 @@ import {useQueryClient} from '@tanstack/react-query' import {DISCOVER_FEED_URI, KNOWN_SHUTDOWN_FEEDS} from '#/lib/constants' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {logEvent} from '#/lib/statsig/statsig' import {useTheme} from '#/lib/ThemeContext' import {logger} from '#/logger' @@ -32,6 +33,7 @@ import { usePostFeedQuery, } from '#/state/queries/post-feed' import {useSession} from '#/state/session' +import {useProgressGuide} from '#/state/shell/progress-guide' import {ProgressGuide, SuggestedFollows} from '#/components/FeedInterstitials' import {List, ListRef} from '../util/List' import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' @@ -252,6 +254,10 @@ let PostFeed = ({ } }, [pollInterval]) + const progressGuide = useProgressGuide('like-10-and-follow-7') + const {isDesktop} = useWebMediaQueries() + const showProgressIntersitial = progressGuide && !isDesktop + const feedItems: FeedRow[] = React.useMemo(() => { let feedKind: 'following' | 'discover' | 'profile' | undefined if (feedType === 'following') { @@ -292,7 +298,7 @@ let PostFeed = ({ if (hasSession) { if (feedKind === 'discover') { - if (sliceIndex === 0) { + if (sliceIndex === 0 && showProgressIntersitial) { arr.push({ type: 'interstitialProgressGuide', key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt, @@ -382,6 +388,7 @@ let PostFeed = ({ feedUri, feedTab, hasSession, + showProgressIntersitial, ]) // events From 10dd0f76eb2d0d05db07e27912070ba418f06b9f Mon Sep 17 00:00:00 2001 From: surfdude29 <149612116+surfdude29@users.noreply.github.com> Date: Thu, 12 Dec 2024 21:50:30 +0000 Subject: [PATCH 21/58] mark string for localization (#7083) --- src/screens/Profile/components/ProfileFeedHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/screens/Profile/components/ProfileFeedHeader.tsx b/src/screens/Profile/components/ProfileFeedHeader.tsx index 0154d535c2..fbd7d5e61d 100644 --- a/src/screens/Profile/components/ProfileFeedHeader.tsx +++ b/src/screens/Profile/components/ProfileFeedHeader.tsx @@ -502,7 +502,7 @@ function DialogInner({ - Something wrong? Let us know. + Something wrong? Let us know. @@ -488,6 +491,18 @@ function OwnHandlePage({goToServiceHandle}: {goToServiceHandle: () => void}) { )} + {currentAccount?.handle?.endsWith('.bsky.social') && ( + + + Your current handle{' '} + + {sanitizeHandle(currentAccount?.handle || '', '@')} + {' '} + will automatically remain reserved for you. You can switch back to + it at any time from this account. + + + )} - - + From 55222c5b0e57b3c7223fc13778e4cba67579da69 Mon Sep 17 00:00:00 2001 From: dan Date: Sat, 14 Dec 2024 00:01:31 +0000 Subject: [PATCH 33/58] Fix z-indexes to make tabbar scroll on Android (#7102) --- src/screens/Hashtag.tsx | 2 +- src/view/com/pager/Pager.tsx | 14 ++++++-------- src/view/com/pager/PagerWithHeader.web.tsx | 16 +++++++++------- src/view/screens/Notifications.tsx | 2 +- src/view/screens/Search/Search.tsx | 3 ++- 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/screens/Hashtag.tsx b/src/screens/Hashtag.tsx index 3e98f364bf..83eb5b80da 100644 --- a/src/screens/Hashtag.tsx +++ b/src/screens/Hashtag.tsx @@ -134,7 +134,7 @@ export default function HashtagScreen({ ( - + section.title)} {...props} /> )} diff --git a/src/view/com/pager/Pager.tsx b/src/view/com/pager/Pager.tsx index b3f936ddc4..2c0bbee520 100644 --- a/src/view/com/pager/Pager.tsx +++ b/src/view/com/pager/Pager.tsx @@ -136,14 +136,12 @@ export const Pager = forwardRef>( return ( - - {renderTabBar({ - selectedPage, - onSelect: onTabBarSelect, - dragProgress, - dragState, - })} - + {renderTabBar({ + selectedPage, + onSelect: onTabBarSelect, + dragProgress, + dragState, + })} {renderHeader?.({setMinimumHeight: noop})} {tabBarAnchor} + web([ + a.sticky, + { + top: 0, + display: isHeaderReady ? undefined : 'none', + }, + ]), + ]}> ( - + section.title)} diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx index ed62c5a51c..21f9c988fb 100644 --- a/src/view/screens/Search/Search.tsx +++ b/src/view/screens/Search/Search.tsx @@ -532,7 +532,8 @@ let SearchScreenInner = ({ renderTabBar={props => ( section.title)} {...props} /> From c5e60ba006bdb81107007a45b4d36072200e123f Mon Sep 17 00:00:00 2001 From: dan Date: Sat, 14 Dec 2024 02:25:32 +0000 Subject: [PATCH 34/58] Fix posting images on web (#7105) * Fix uploadBlob to handle blob: URI * Patch expo-image-manipulator to support "compress" argument * Narrow down the fix --- patches/expo-image-manipulator+13.0.5.patch | 48 +++++++++++++++++++++ src/lib/api/upload-blob.web.ts | 5 ++- 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 patches/expo-image-manipulator+13.0.5.patch diff --git a/patches/expo-image-manipulator+13.0.5.patch b/patches/expo-image-manipulator+13.0.5.patch new file mode 100644 index 0000000000..4ff1cc9703 --- /dev/null +++ b/patches/expo-image-manipulator+13.0.5.patch @@ -0,0 +1,48 @@ +diff --git a/node_modules/expo-image-manipulator/src/ImageManipulator.ts b/node_modules/expo-image-manipulator/src/ImageManipulator.ts +index a80d9c8..babbb3b 100644 +--- a/node_modules/expo-image-manipulator/src/ImageManipulator.ts ++++ b/node_modules/expo-image-manipulator/src/ImageManipulator.ts +@@ -43,7 +43,7 @@ export async function manipulateAsync( + context.extent(action.extent); + } + } +- const image = await context.renderAsync(); ++ const image = await context.renderAsync(saveOptions.compress); + const result = await image.saveAsync({ format, ...rest }); + + // These shared objects will not be used anymore, so free up some memory. +diff --git a/node_modules/expo-image-manipulator/src/ImageManipulatorContext.ts b/node_modules/expo-image-manipulator/src/ImageManipulatorContext.ts +index 120d8d3..f8aa49c 100644 +--- a/node_modules/expo-image-manipulator/src/ImageManipulatorContext.ts ++++ b/node_modules/expo-image-manipulator/src/ImageManipulatorContext.ts +@@ -52,7 +52,7 @@ export declare class ImageManipulatorContext extends SharedObject { + /** + * Awaits for all manipulation tasks to finish and resolves with a reference to the resulted native image. + */ +- renderAsync(): Promise; ++ renderAsync(compress?: number): Promise; + } + + export default ExpoImageManipulator.Context as typeof ImageManipulatorContext; +diff --git a/node_modules/expo-image-manipulator/src/web/ImageManipulatorContext.web.ts b/node_modules/expo-image-manipulator/src/web/ImageManipulatorContext.web.ts +index 428848c..363a57a 100644 +--- a/node_modules/expo-image-manipulator/src/web/ImageManipulatorContext.web.ts ++++ b/node_modules/expo-image-manipulator/src/web/ImageManipulatorContext.web.ts +@@ -41,7 +41,7 @@ export default class ImageManipulatorContext extends SharedObject { + return this; + } + +- async renderAsync(): Promise { ++ async renderAsync(compress?: number): Promise { + const canvas = await this.currentTask; + + return new Promise((resolve) => { +@@ -49,7 +49,7 @@ export default class ImageManipulatorContext extends SharedObject { + const url = blob ? URL.createObjectURL(blob) : canvas.toDataURL(); + + resolve(new ImageManipulatorImageRef(url, canvas.width, canvas.height)); +- }); ++ }, typeof compress === 'number' ? 'image/jpeg' : undefined, compress); + }); + } + diff --git a/src/lib/api/upload-blob.web.ts b/src/lib/api/upload-blob.web.ts index d3c52190c1..45b72f7ee7 100644 --- a/src/lib/api/upload-blob.web.ts +++ b/src/lib/api/upload-blob.web.ts @@ -11,7 +11,10 @@ export async function uploadBlob( input: string | Blob, encoding?: string, ): Promise { - if (typeof input === 'string' && input.startsWith('data:')) { + if ( + typeof input === 'string' && + (input.startsWith('data:') || input.startsWith('blob:')) + ) { const blob = await fetch(input).then(r => r.blob()) return agent.uploadBlob(blob, {encoding}) } From b806f3e85dbf64220a90c602ffbadd58252347ee Mon Sep 17 00:00:00 2001 From: Ben Harris Date: Fri, 13 Dec 2024 22:02:12 -0500 Subject: [PATCH 35/58] apply fonts to TextField (#6288) system fonts were leaking. i'm sure there's a better way to apply this without duplicating but idk it works --- src/components/forms/TextField.tsx | 83 +++++++++++++++++++----------- 1 file changed, 53 insertions(+), 30 deletions(-) diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx index 2bbf054225..410cc654e9 100644 --- a/src/components/forms/TextField.tsx +++ b/src/components/forms/TextField.tsx @@ -11,7 +11,15 @@ import { import {HITSLOP_20} from '#/lib/constants' import {mergeRefs} from '#/lib/merge-refs' -import {android, atoms as a, TextStyleProp, useTheme, web} from '#/alf' +import { + android, + applyFonts, + atoms as a, + TextStyleProp, + useAlf, + useTheme, + web, +} from '#/alf' import {useInteractionState} from '#/components/hooks/useInteractionState' import {Props as SVGIconProps} from '#/components/icons/common' import {Text} from '#/components/Typography' @@ -148,6 +156,7 @@ export function createInput(Component: typeof TextInput) { ...rest }: InputProps) { const t = useTheme() + const {fonts} = useAlf() const ctx = React.useContext(Context) const withinRoot = Boolean(ctx.inputRef) @@ -171,6 +180,48 @@ export function createInput(Component: typeof TextInput) { const refs = mergeRefs([ctx.inputRef, inputRef!].filter(Boolean)) + const flattened = StyleSheet.flatten([ + a.relative, + a.z_20, + a.flex_1, + a.text_md, + t.atoms.text, + a.px_xs, + { + // paddingVertical doesn't work w/multiline - esb + paddingTop: 12, + paddingBottom: 13, + lineHeight: a.text_md.fontSize * 1.1875, + textAlignVertical: rest.multiline ? 'top' : undefined, + minHeight: rest.multiline ? 80 : undefined, + minWidth: 0, + }, + // fix for autofill styles covering border + web({ + paddingTop: 10, + paddingBottom: 11, + marginTop: 2, + marginBottom: 2, + }), + android({ + paddingTop: 8, + paddingBottom: 8, + }), + style, + ]) + + applyFonts(flattened, fonts.family) + + // should always be defined on `typography` + // @ts-ignore + if (flattened.fontSize) { + // @ts-ignore + flattened.fontSize = Math.round( + // @ts-ignore + flattened.fontSize * fonts.scaleMultiplier, + ) + } + return ( <> Date: Sat, 14 Dec 2024 15:30:01 -0600 Subject: [PATCH 36/58] `ProfileFeed` cleanups (#7115) * Clean up feed load state * Fix feed info dialog load state --- src/screens/Profile/ProfileFeed/index.tsx | 21 ++++++-- .../Profile/components/ProfileFeedHeader.tsx | 53 ++++++++++++++----- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/src/screens/Profile/ProfileFeed/index.tsx b/src/screens/Profile/ProfileFeed/index.tsx index 7d48b5ac15..3a8686a7d9 100644 --- a/src/screens/Profile/ProfileFeed/index.tsx +++ b/src/screens/Profile/ProfileFeed/index.tsx @@ -34,9 +34,12 @@ import {FAB} from '#/view/com/util/fab/FAB' import {Button} from '#/view/com/util/forms/Button' import {ListRef} from '#/view/com/util/List' import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn' -import {LoadingScreen} from '#/view/com/util/LoadingScreen' +import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {Text} from '#/view/com/util/text/Text' -import {ProfileFeedHeader} from '#/screens/Profile/components/ProfileFeedHeader' +import { + ProfileFeedHeader, + ProfileFeedHeaderSkeleton, +} from '#/screens/Profile/components/ProfileFeedHeader' import * as Layout from '#/components/Layout' type Props = NativeStackScreenProps @@ -92,12 +95,15 @@ export function ProfileFeedScreen(props: Props) { } return resolvedUri ? ( - + ) : ( - + + + + ) } @@ -107,7 +113,12 @@ function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) { const {data: info} = useFeedSourceInfoQuery({uri: feedUri}) if (!preferences || !info) { - return + return ( + + + + + ) } return ( diff --git a/src/screens/Profile/components/ProfileFeedHeader.tsx b/src/screens/Profile/components/ProfileFeedHeader.tsx index 87b242a556..6bfbff3a08 100644 --- a/src/screens/Profile/components/ProfileFeedHeader.tsx +++ b/src/screens/Profile/components/ProfileFeedHeader.tsx @@ -1,6 +1,5 @@ import React from 'react' import {View} from 'react-native' -import {useSafeAreaInsets} from 'react-native-safe-area-context' import {AtUri} from '@atproto/api' import {msg, Plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -46,18 +45,53 @@ import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' import * as Layout from '#/components/Layout' import {InlineLinkText} from '#/components/Link' -import {Loader} from '#/components/Loader' import * as Menu from '#/components/Menu' import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' +export function ProfileFeedHeaderSkeleton() { + const t = useTheme() + + return ( + + + + + + + + + + + + ) +} + export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { const t = useTheme() const {_, i18n} = useLingui() const {hasSession} = useSession() const {gtPhone, gtMobile} = useBreakpoints() - const {top} = useSafeAreaInsets() const infoControl = Dialog.useDialogControl() const playHaptic = useHaptics() @@ -148,12 +182,7 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { return ( <> + style={[t.atoms.bg, a.z_10, web([a.sticky, a.z_10, {top: 0}])]}> @@ -356,7 +385,7 @@ function DialogInner({ const playHaptic = useHaptics() const control = Dialog.useDialogContext() const reportDialogControl = useReportDialogControl() - const [rt, loading] = useRichText(info.description.text) + const [rt] = useRichText(info.description.text) const {mutateAsync: likeFeed, isPending: isLikePending} = useLikeMutation() const {mutateAsync: unlikeFeed, isPending: isUnlikePending} = useUnlikeMutation() @@ -396,9 +425,7 @@ function DialogInner({ reportDialogControl.open() }, [reportDialogControl]) - return loading ? ( - - ) : ( + return ( From 80c0125d6bfbafe463c526f37dab62f5f6604883 Mon Sep 17 00:00:00 2001 From: dan Date: Sun, 15 Dec 2024 17:42:45 +0000 Subject: [PATCH 37/58] Fix image upload on native (#7121) --- patches/expo-image-manipulator+13.0.5.patch | 77 +++++++++++++++++---- 1 file changed, 65 insertions(+), 12 deletions(-) diff --git a/patches/expo-image-manipulator+13.0.5.patch b/patches/expo-image-manipulator+13.0.5.patch index 4ff1cc9703..02d4d5fb9a 100644 --- a/patches/expo-image-manipulator+13.0.5.patch +++ b/patches/expo-image-manipulator+13.0.5.patch @@ -1,16 +1,69 @@ -diff --git a/node_modules/expo-image-manipulator/src/ImageManipulator.ts b/node_modules/expo-image-manipulator/src/ImageManipulator.ts -index a80d9c8..babbb3b 100644 ---- a/node_modules/expo-image-manipulator/src/ImageManipulator.ts -+++ b/node_modules/expo-image-manipulator/src/ImageManipulator.ts -@@ -43,7 +43,7 @@ export async function manipulateAsync( - context.extent(action.extent); - } - } -- const image = await context.renderAsync(); +diff --git a/node_modules/expo-image-manipulator/src/ImageManipulator.web.ts b/node_modules/expo-image-manipulator/src/ImageManipulator.web.ts +new file mode 100644 +index 0000000..babbb3b +--- /dev/null ++++ b/node_modules/expo-image-manipulator/src/ImageManipulator.web.ts +@@ -0,0 +1,60 @@ ++import { useReleasingSharedObject } from 'expo-modules-core'; ++import { SharedRef } from 'expo-modules-core/types'; ++ ++import { Action, ImageResult, SaveFormat, SaveOptions } from './ImageManipulator.types'; ++import { ImageManipulatorContext } from './ImageManipulatorContext'; ++import ExpoImageManipulator from './NativeImageManipulatorModule'; ++import { validateArguments } from './validators'; ++ ++// @needsAudit ++/** ++ * Manipulate the image provided via `uri`. Available modifications are rotating, flipping (mirroring), ++ * resizing and cropping. Each invocation results in a new file. With one invocation you can provide ++ * a set of actions to perform over the image. Overwriting the source file would not have an effect ++ * in displaying the result as images are cached. ++ * @param uri URI of the file to manipulate. Should be on the local file system or a base64 data URI. ++ * @param actions An array of objects representing manipulation options. Each object should have ++ * __only one__ of the keys that corresponds to specific transformation. ++ * @param saveOptions A map defining how modified image should be saved. ++ * @return Promise which fulfils with [`ImageResult`](#imageresult) object. ++ * @deprecated It has been replaced by the new, contextual and object-oriented API. ++ * Use [`ImageManipulator.manipulate`](#manipulateuri) or [`useImageManipulator`](#useimagemanipulatoruri) instead. ++ */ ++export async function manipulateAsync( ++ uri: string, ++ actions: Action[] = [], ++ saveOptions: SaveOptions = {} ++): Promise { ++ validateArguments(uri, actions, saveOptions); ++ ++ const { format = SaveFormat.JPEG, ...rest } = saveOptions; ++ const context = ExpoImageManipulator.manipulate(uri); ++ ++ for (const action of actions) { ++ if ('resize' in action) { ++ context.resize(action.resize); ++ } else if ('rotate' in action) { ++ context.rotate(action.rotate); ++ } else if ('flip' in action) { ++ context.flip(action.flip); ++ } else if ('crop' in action) { ++ context.crop(action.crop); ++ } else if ('extent' in action && context.extent) { ++ context.extent(action.extent); ++ } ++ } + const image = await context.renderAsync(saveOptions.compress); - const result = await image.saveAsync({ format, ...rest }); - - // These shared objects will not be used anymore, so free up some memory. ++ const result = await image.saveAsync({ format, ...rest }); ++ ++ // These shared objects will not be used anymore, so free up some memory. ++ context.release(); ++ image.release(); ++ ++ return result; ++} ++ ++export function useImageManipulator(source: string | SharedRef<'image'>): ImageManipulatorContext { ++ return useReleasingSharedObject(() => ExpoImageManipulator.manipulate(source), [source]); ++} ++ ++export { ExpoImageManipulator as ImageManipulator }; diff --git a/node_modules/expo-image-manipulator/src/ImageManipulatorContext.ts b/node_modules/expo-image-manipulator/src/ImageManipulatorContext.ts index 120d8d3..f8aa49c 100644 --- a/node_modules/expo-image-manipulator/src/ImageManipulatorContext.ts From 1ac307bc42bdf370c2011e7e7f2c76baea3441d2 Mon Sep 17 00:00:00 2001 From: dan Date: Sun, 15 Dec 2024 20:30:17 +0000 Subject: [PATCH 38/58] [Experiment] Remove "Load Latest" button (#7120) * Remove "show latest" behind the gate * Add HomeBadgeProvider * Update provider state from home feed tabs * Add Home badge to native * Add Home badge to mobile web * Add Home badge to desktop web --- src/App.native.tsx | 31 ++++++++++--------- src/App.web.tsx | 27 +++++++++------- src/lib/statsig/gates.ts | 1 + src/state/home-badge.tsx | 24 ++++++++++++++ src/view/com/feeds/FeedPage.tsx | 8 +++++ .../com/util/load-latest/LoadLatestBtn.tsx | 6 ++++ src/view/shell/bottom-bar/BottomBar.tsx | 11 ++++++- src/view/shell/bottom-bar/BottomBarStyles.tsx | 11 +++++++ src/view/shell/bottom-bar/BottomBarWeb.tsx | 28 +++++++++++------ src/view/shell/desktop/LeftNav.tsx | 26 +++++++++++++++- 10 files changed, 136 insertions(+), 37 deletions(-) create mode 100644 src/state/home-badge.tsx diff --git a/src/App.native.tsx b/src/App.native.tsx index f985b96a57..39ab7ca92c 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -32,6 +32,7 @@ import { ensureGeolocationResolved, Provider as GeolocationProvider, } from '#/state/geolocation' +import {Provider as HomeBadgeProvider} from '#/state/home-badge' import {Provider as InvitesStateProvider} from '#/state/invites' import {Provider as LightboxStateProvider} from '#/state/lightbox' import {MessagesProvider} from '#/state/messages' @@ -137,20 +138,22 @@ function InnerApp() { - - - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index b7c5a56334..8d13a826e7 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -22,6 +22,7 @@ import { ensureGeolocationResolved, Provider as GeolocationProvider, } from '#/state/geolocation' +import {Provider as HomeBadgeProvider} from '#/state/home-badge' import {Provider as InvitesStateProvider} from '#/state/invites' import {Provider as LightboxStateProvider} from '#/state/lightbox' import {MessagesProvider} from '#/state/messages' @@ -120,18 +121,20 @@ function InnerApp() { - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index 3cec5d5b29..3767ec1e5d 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -2,3 +2,4 @@ export type Gate = // Keep this alphabetic please. | 'debug_show_feedcontext' // DISABLED DUE TO EME | 'post_feed_lang_window' // DISABLED DUE TO EME + | 'remove_show_latest_button' diff --git a/src/state/home-badge.tsx b/src/state/home-badge.tsx new file mode 100644 index 0000000000..59a9276ce8 --- /dev/null +++ b/src/state/home-badge.tsx @@ -0,0 +1,24 @@ +import React from 'react' + +type StateContext = boolean +type ApiContext = (hasNew: boolean) => void + +const stateContext = React.createContext(false) +const apiContext = React.createContext((_: boolean) => {}) + +export function Provider({children}: React.PropsWithChildren<{}>) { + const [state, setState] = React.useState(false) + return ( + + {children} + + ) +} + +export function useHomeBadge() { + return React.useContext(stateContext) +} + +export function useSetHomeBadge() { + return React.useContext(apiContext) +} diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx index e766b589b0..10ed60212c 100644 --- a/src/view/com/feeds/FeedPage.tsx +++ b/src/view/com/feeds/FeedPage.tsx @@ -14,6 +14,7 @@ import {s} from '#/lib/styles' import {isNative} from '#/platform/detection' import {listenSoftReset} from '#/state/events' import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback' +import {useSetHomeBadge} from '#/state/home-badge' import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed' import {truncateAndInvalidate} from '#/state/queries/util' @@ -59,6 +60,13 @@ export function FeedPage({ const feedFeedback = useFeedFeedback(feed, hasSession) const scrollElRef = React.useRef(null) const [hasNew, setHasNew] = React.useState(false) + const setHomeBadge = useSetHomeBadge() + + React.useEffect(() => { + if (isPageFocused) { + setHomeBadge(hasNew) + } + }, [isPageFocused, hasNew, setHomeBadge]) const scrollToTop = React.useCallback(() => { scrollElRef.current?.scrollToOffset({ diff --git a/src/view/com/util/load-latest/LoadLatestBtn.tsx b/src/view/com/util/load-latest/LoadLatestBtn.tsx index d98aa0fa71..b502f0b68b 100644 --- a/src/view/com/util/load-latest/LoadLatestBtn.tsx +++ b/src/view/com/util/load-latest/LoadLatestBtn.tsx @@ -9,6 +9,7 @@ import {useMinimalShellFabTransform} from '#/lib/hooks/useMinimalShellTransform' import {usePalette} from '#/lib/hooks/usePalette' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {clamp} from '#/lib/numbers' +import {useGate} from '#/lib/statsig/statsig' import {colors} from '#/lib/styles' import {isWeb} from '#/platform/detection' import {useSession} from '#/state/session' @@ -34,6 +35,11 @@ export function LoadLatestBtn({ // move button inline if it starts overlapping the left nav const isTallViewport = useMediaQuery({minHeight: 700}) + const gate = useGate() + if (gate('remove_show_latest_button')) { + return null + } + // Adjust height of the fab if we have a session only on mobile web. If we don't have a session, we want to adjust // it on both tablet and mobile since we are showing the bottom bar (see createNativeStackNavigatorWithAuth) const showBottomBar = hasSession ? isMobile : isTabletOrMobile diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index db9c049659..47a525c04d 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -15,8 +15,10 @@ import {useNavigationTabState} from '#/lib/hooks/useNavigationTabState' import {usePalette} from '#/lib/hooks/usePalette' import {clamp} from '#/lib/numbers' import {getTabState, TabState} from '#/lib/routes/helpers' +import {useGate} from '#/lib/statsig/statsig' import {s} from '#/lib/styles' import {emitSoftReset} from '#/state/events' +import {useHomeBadge} from '#/state/home-badge' import {useUnreadMessageCount} from '#/state/queries/messages/list-conversations' import {useUnreadNotifications} from '#/state/queries/notifications/unread' import {useProfileQuery} from '#/state/queries/profile' @@ -73,6 +75,8 @@ export function BottomBar({navigation}: BottomTabBarProps) { const dedupe = useDedupe() const accountSwitchControl = useDialogControl() const playHaptic = useHaptics() + const hasHomeBadge = useHomeBadge() + const gate = useGate() const iconWidth = 28 const showSignIn = React.useCallback(() => { @@ -153,6 +157,7 @@ export function BottomBar({navigation}: BottomTabBarProps) { /> ) } + hasNew={hasHomeBadge && gate('remove_show_latest_button')} onPress={onPressHome} accessibilityRole="tab" accessibilityLabel={_(msg`Home`)} @@ -334,6 +339,7 @@ interface BtnProps testID?: string icon: JSX.Element notificationCount?: string + hasNew?: boolean onPress?: (event: GestureResponderEvent) => void onLongPress?: (event: GestureResponderEvent) => void } @@ -341,6 +347,7 @@ interface BtnProps function Btn({ testID, icon, + hasNew, notificationCount, onPress, onLongPress, @@ -363,7 +370,9 @@ function Btn({ {notificationCount} - ) : undefined} + ) : hasNew ? ( + + ) : null} ) } diff --git a/src/view/shell/bottom-bar/BottomBarStyles.tsx b/src/view/shell/bottom-bar/BottomBarStyles.tsx index 9255957cb4..d80914d095 100644 --- a/src/view/shell/bottom-bar/BottomBarStyles.tsx +++ b/src/view/shell/bottom-bar/BottomBarStyles.tsx @@ -44,6 +44,17 @@ export const styles = StyleSheet.create({ color: colors.white, fontVariant: ['tabular-nums'], }, + hasNewBadge: { + position: 'absolute', + left: '52%', + marginLeft: 4, + top: 10, + width: 8, + height: 8, + backgroundColor: colors.blue3, + borderRadius: 6, + zIndex: 1, + }, ctrlIcon: { marginLeft: 'auto', marginRight: 'auto', diff --git a/src/view/shell/bottom-bar/BottomBarWeb.tsx b/src/view/shell/bottom-bar/BottomBarWeb.tsx index 127ff2b269..1855969cc4 100644 --- a/src/view/shell/bottom-bar/BottomBarWeb.tsx +++ b/src/view/shell/bottom-bar/BottomBarWeb.tsx @@ -9,6 +9,8 @@ import {useMinimalShellFooterTransform} from '#/lib/hooks/useMinimalShellTransfo import {getCurrentRoute, isTab} from '#/lib/routes/helpers' import {makeProfileLink} from '#/lib/routes/links' import {CommonNavigatorParams} from '#/lib/routes/types' +import {useGate} from '#/lib/statsig/statsig' +import {useHomeBadge} from '#/state/home-badge' import {useUnreadMessageCount} from '#/state/queries/messages/list-conversations' import {useUnreadNotifications} from '#/state/queries/notifications/unread' import {useSession} from '#/state/session' @@ -51,6 +53,8 @@ export function BottomBarWeb() { const unreadMessageCount = useUnreadMessageCount() const notificationCountStr = useUnreadNotifications() + const hasHomeBadge = useHomeBadge() + const gate = useGate() const showSignIn = React.useCallback(() => { closeAllActiveElements() @@ -75,7 +79,10 @@ export function BottomBarWeb() { ]}> {hasSession ? ( <> - + {({isActive}) => { const Icon = isActive ? HomeFilled : Home return ( @@ -105,7 +112,7 @@ export function BottomBarWeb() { 0 ? unreadMessageCount.numUnread : undefined @@ -128,7 +135,7 @@ export function BottomBarWeb() { + notificationCount={notificationCountStr}> {({isActive}) => { const Icon = isActive ? BellFilled : Bell return ( @@ -220,8 +227,9 @@ const NavItem: React.FC<{ children: (props: {isActive: boolean}) => React.ReactChild href: string routeName: string - badge?: string -}> = ({children, href, routeName, badge}) => { + hasNew?: boolean + notificationCount?: string +}> = ({children, href, routeName, hasNew, notificationCount}) => { const {_} = useLingui() const {currentAccount} = useSession() const currentRoute = useNavigationState(state => { @@ -246,13 +254,15 @@ const NavItem: React.FC<{ aria-label={routeName} accessible={true}> {children({isActive})} - {!!badge && ( + {notificationCount ? ( - {badge} + aria-label={_(msg`${notificationCount} unread items`)}> + {notificationCount} - )} + ) : hasNew ? ( + + ) : null} ) } diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index 6655572f87..d367e1b98e 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -14,8 +14,10 @@ import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {getCurrentRoute, isTab} from '#/lib/routes/helpers' import {makeProfileLink} from '#/lib/routes/links' import {CommonNavigatorParams} from '#/lib/routes/types' +import {useGate} from '#/lib/statsig/statsig' import {isInvalidHandle} from '#/lib/strings/handles' import {emitSoftReset} from '#/state/events' +import {useHomeBadge} from '#/state/home-badge' import {useFetchHandle} from '#/state/queries/handle' import {useUnreadMessageCount} from '#/state/queries/messages/list-conversations' import {useUnreadNotifications} from '#/state/queries/notifications/unread' @@ -100,12 +102,13 @@ function ProfileCard() { interface NavItemProps { count?: string + hasNew?: boolean href: string icon: JSX.Element iconFilled: JSX.Element label: string } -function NavItem({count, href, icon, iconFilled, label}: NavItemProps) { +function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) { const t = useTheme() const {_} = useLingui() const {currentAccount} = useSession() @@ -214,6 +217,24 @@ function NavItem({count, href, icon, iconFilled, label}: NavItemProps) { {count} + ) : hasNew ? ( + ) : null} {gtTablet && ( @@ -322,6 +343,8 @@ export function DesktopLeftNav() { const {_} = useLingui() const {isDesktop, isTablet} = useWebMediaQueries() const numUnreadNotifications = useUnreadNotifications() + const hasHomeBadge = useHomeBadge() + const gate = useGate() if (!hasSession && !isDesktop) { return null @@ -348,6 +371,7 @@ export function DesktopLeftNav() { <> Date: Mon, 16 Dec 2024 22:25:59 +0800 Subject: [PATCH 39/58] Fix post time localization on Android (#6742) * Fix post time localization on native * Fix timezone * Revent time.ts * Add time.android.ts * Add missing 'an' and 'ast' * Revent PostThreadItem.tsx * Resolve duplicate versions in lockfile * Move to time.native.ts * Follow #7055 to upgrade intl-datetimeformat version * Remove time.native.ts --- package.json | 1 + src/locale/i18n.ts | 29 +++++++++++++++++++++++++++++ yarn.lock | 9 +++++++++ 3 files changed, 39 insertions(+) diff --git a/package.json b/package.json index 23e2bb324a..cd73a8d210 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "@expo/webpack-config": "^19.0.0", "@floating-ui/dom": "^1.6.3", "@floating-ui/react-dom": "^2.0.8", + "@formatjs/intl-datetimeformat": "^6.17.1", "@formatjs/intl-locale": "^4.2.8", "@formatjs/intl-numberformat": "^8.15.1", "@formatjs/intl-pluralrules": "^5.4.1", diff --git a/src/locale/i18n.ts b/src/locale/i18n.ts index 00652a355a..5620ab3203 100644 --- a/src/locale/i18n.ts +++ b/src/locale/i18n.ts @@ -1,8 +1,10 @@ // Don't remove -force from these because detection is VERY slow on low-end Android. // https://github.com/formatjs/formatjs/issues/4463#issuecomment-2176070577 import '@formatjs/intl-locale/polyfill-force' +import '@formatjs/intl-datetimeformat/polyfill-force' import '@formatjs/intl-pluralrules/polyfill-force' import '@formatjs/intl-numberformat/polyfill-force' +import '@formatjs/intl-datetimeformat/locale-data/en' import '@formatjs/intl-pluralrules/locale-data/en' import '@formatjs/intl-numberformat/locale-data/en' @@ -49,6 +51,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.an: { i18n.loadAndActivate({locale, messages: messagesAn}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/es'), import('@formatjs/intl-pluralrules/locale-data/an'), import('@formatjs/intl-numberformat/locale-data/es'), ]) @@ -57,6 +60,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.ast: { i18n.loadAndActivate({locale, messages: messagesAst}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/ast'), import('@formatjs/intl-pluralrules/locale-data/ast'), import('@formatjs/intl-numberformat/locale-data/ast'), ]) @@ -65,6 +69,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.ca: { i18n.loadAndActivate({locale, messages: messagesCa}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/ca'), import('@formatjs/intl-pluralrules/locale-data/ca'), import('@formatjs/intl-numberformat/locale-data/ca'), ]) @@ -73,6 +78,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.de: { i18n.loadAndActivate({locale, messages: messagesDe}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/de'), import('@formatjs/intl-pluralrules/locale-data/de'), import('@formatjs/intl-numberformat/locale-data/de'), ]) @@ -81,6 +87,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.en_GB: { i18n.loadAndActivate({locale, messages: messagesEn_GB}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/en-GB'), import('@formatjs/intl-pluralrules/locale-data/en'), import('@formatjs/intl-numberformat/locale-data/en-GB'), ]) @@ -89,6 +96,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.es: { i18n.loadAndActivate({locale, messages: messagesEs}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/es'), import('@formatjs/intl-pluralrules/locale-data/es'), import('@formatjs/intl-numberformat/locale-data/es'), ]) @@ -97,6 +105,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.fi: { i18n.loadAndActivate({locale, messages: messagesFi}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/fi'), import('@formatjs/intl-pluralrules/locale-data/fi'), import('@formatjs/intl-numberformat/locale-data/fi'), ]) @@ -105,6 +114,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.fr: { i18n.loadAndActivate({locale, messages: messagesFr}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/fr'), import('@formatjs/intl-pluralrules/locale-data/fr'), import('@formatjs/intl-numberformat/locale-data/fr'), ]) @@ -113,6 +123,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.ga: { i18n.loadAndActivate({locale, messages: messagesGa}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/ga'), import('@formatjs/intl-pluralrules/locale-data/ga'), import('@formatjs/intl-numberformat/locale-data/ga'), ]) @@ -121,6 +132,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.gl: { i18n.loadAndActivate({locale, messages: messagesGl}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/gl'), import('@formatjs/intl-pluralrules/locale-data/gl'), import('@formatjs/intl-numberformat/locale-data/gl'), ]) @@ -129,6 +141,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.hi: { i18n.loadAndActivate({locale, messages: messagesHi}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/hi'), import('@formatjs/intl-pluralrules/locale-data/hi'), import('@formatjs/intl-numberformat/locale-data/hi'), ]) @@ -137,6 +150,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.hu: { i18n.loadAndActivate({locale, messages: messagesHu}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/hu'), import('@formatjs/intl-pluralrules/locale-data/hu'), import('@formatjs/intl-numberformat/locale-data/hu'), ]) @@ -145,6 +159,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.id: { i18n.loadAndActivate({locale, messages: messagesId}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/id'), import('@formatjs/intl-pluralrules/locale-data/id'), import('@formatjs/intl-numberformat/locale-data/id'), ]) @@ -153,6 +168,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.it: { i18n.loadAndActivate({locale, messages: messagesIt}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/it'), import('@formatjs/intl-pluralrules/locale-data/it'), import('@formatjs/intl-numberformat/locale-data/it'), ]) @@ -161,6 +177,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.ja: { i18n.loadAndActivate({locale, messages: messagesJa}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/ja'), import('@formatjs/intl-pluralrules/locale-data/ja'), import('@formatjs/intl-numberformat/locale-data/ja'), ]) @@ -169,6 +186,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.ko: { i18n.loadAndActivate({locale, messages: messagesKo}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/ko'), import('@formatjs/intl-pluralrules/locale-data/ko'), import('@formatjs/intl-numberformat/locale-data/ko'), ]) @@ -177,6 +195,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.nl: { i18n.loadAndActivate({locale, messages: messagesNl}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/nl'), import('@formatjs/intl-pluralrules/locale-data/nl'), import('@formatjs/intl-numberformat/locale-data/nl'), ]) @@ -185,6 +204,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.pl: { i18n.loadAndActivate({locale, messages: messagesPl}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/pl'), import('@formatjs/intl-pluralrules/locale-data/pl'), import('@formatjs/intl-numberformat/locale-data/pl'), ]) @@ -193,6 +213,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.pt_BR: { i18n.loadAndActivate({locale, messages: messagesPt_BR}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/pt'), import('@formatjs/intl-pluralrules/locale-data/pt'), import('@formatjs/intl-numberformat/locale-data/pt'), ]) @@ -201,6 +222,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.ru: { i18n.loadAndActivate({locale, messages: messagesRu}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/ru'), import('@formatjs/intl-pluralrules/locale-data/ru'), import('@formatjs/intl-numberformat/locale-data/ru'), ]) @@ -209,6 +231,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.th: { i18n.loadAndActivate({locale, messages: messagesTh}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/th'), import('@formatjs/intl-pluralrules/locale-data/th'), import('@formatjs/intl-numberformat/locale-data/th'), ]) @@ -217,6 +240,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.tr: { i18n.loadAndActivate({locale, messages: messagesTr}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/tr'), import('@formatjs/intl-pluralrules/locale-data/tr'), import('@formatjs/intl-numberformat/locale-data/tr'), ]) @@ -225,6 +249,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.uk: { i18n.loadAndActivate({locale, messages: messagesUk}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/uk'), import('@formatjs/intl-pluralrules/locale-data/uk'), import('@formatjs/intl-numberformat/locale-data/uk'), ]) @@ -233,6 +258,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.vi: { i18n.loadAndActivate({locale, messages: messagesVi}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/vi'), import('@formatjs/intl-pluralrules/locale-data/vi'), import('@formatjs/intl-numberformat/locale-data/vi'), ]) @@ -241,6 +267,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.zh_CN: { i18n.loadAndActivate({locale, messages: messagesZh_CN}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/zh-Hans'), import('@formatjs/intl-pluralrules/locale-data/zh'), import('@formatjs/intl-numberformat/locale-data/zh'), ]) @@ -249,6 +276,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.zh_HK: { i18n.loadAndActivate({locale, messages: messagesZh_HK}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/yue'), import('@formatjs/intl-pluralrules/locale-data/zh'), import('@formatjs/intl-numberformat/locale-data/zh'), ]) @@ -257,6 +285,7 @@ export async function dynamicActivate(locale: AppLanguage) { case AppLanguage.zh_TW: { i18n.loadAndActivate({locale, messages: messagesZh_TW}) await Promise.all([ + import('@formatjs/intl-datetimeformat/locale-data/zh-Hant'), import('@formatjs/intl-pluralrules/locale-data/zh'), import('@formatjs/intl-numberformat/locale-data/zh'), ]) diff --git a/yarn.lock b/yarn.lock index 9e2259e0d2..8dc5745e36 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4274,6 +4274,15 @@ dependencies: tslib "2" +"@formatjs/intl-datetimeformat@^6.17.1": + version "6.17.1" + resolved "https://registry.yarnpkg.com/@formatjs/intl-datetimeformat/-/intl-datetimeformat-6.17.1.tgz#d5e800891f9d79c8f1af1999f51db51f1384eca1" + integrity sha512-a18NqRo6R73xpREuMZo8FqjO+LnYFDHoeoviTh5de4ebI46wqLSDgbAIKoceuWblTQt8bvCpJIwvKgLItea88Q== + dependencies: + "@formatjs/ecma402-abstract" "2.3.1" + "@formatjs/intl-localematcher" "0.5.9" + tslib "2" + "@formatjs/intl-enumerator@1.8.7": version "1.8.7" resolved "https://registry.yarnpkg.com/@formatjs/intl-enumerator/-/intl-enumerator-1.8.7.tgz#3f004753333f80cc468ae34046bd8416772a0412" From e4e5a589b135e05187772a6961fbaff0cd5f21be Mon Sep 17 00:00:00 2001 From: dan Date: Mon, 16 Dec 2024 15:42:29 +0000 Subject: [PATCH 40/58] Fix video uploads on native (#7126) * Fix duplicate expo-modules-core * Patch expo/expo#33621 --- package.json | 2 +- patches/expo-modules-core+2.1.1.patch | 20 ++++++++++++++++++++ yarn.lock | 23 ++++++++--------------- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index cd73a8d210..f48ece6810 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@lingui/react": "^4.14.1", "@mattermost/react-native-paste-input": "^0.7.1", "@miblanchard/react-native-slider": "^2.3.1", - "@mozzius/expo-dynamic-app-icon": "^1.4.1", + "@mozzius/expo-dynamic-app-icon": "^1.5.0", "@radix-ui/react-dismissable-layer": "^1.1.1", "@radix-ui/react-dropdown-menu": "2.0.1", "@radix-ui/react-focus-guards": "^1.1.1", diff --git a/patches/expo-modules-core+2.1.1.patch b/patches/expo-modules-core+2.1.1.patch index f3d9bfd149..5695b8f44a 100644 --- a/patches/expo-modules-core+2.1.1.patch +++ b/patches/expo-modules-core+2.1.1.patch @@ -13,3 +13,23 @@ index 47c4d15..afe138d 100644 // Check for Content-Type val skipContentTypes = listOf( "text/event-stream", // Server Sent Events +diff --git a/node_modules/expo-modules-core/src/uuid/uuid.ts b/node_modules/expo-modules-core/src/uuid/uuid.ts +index 148beac..fabdff5 100644 +--- a/node_modules/expo-modules-core/src/uuid/uuid.ts ++++ b/node_modules/expo-modules-core/src/uuid/uuid.ts +@@ -5,6 +5,7 @@ const nativeUuidv4 = globalThis?.expo?.uuidv4; + const nativeUuidv5 = globalThis?.expo?.uuidv5; + + function uuidv4(): string { ++ const nativeUuidv4 = globalThis?.expo?.uuidv4; + if (!nativeUuidv4) { + throw Error( + "Native UUID version 4 generator implementation wasn't found in `expo-modules-core`" +@@ -23,6 +24,7 @@ function uuidv5(name: string, namespace: string | number[]) { + throw new Error('`namespace` must be a valid UUID string or an Array of 16 byte values'); + } + ++ const nativeUuidv5 = globalThis?.expo?.uuidv5; + if (!nativeUuidv5) { + throw Error("Native UUID type 5 generator implementation wasn't found in `expo-modules-core`"); + } diff --git a/yarn.lock b/yarn.lock index 8dc5745e36..e5bec88706 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3962,7 +3962,7 @@ resolved "https://registry.yarnpkg.com/@expo/html-elements/-/html-elements-0.4.3.tgz#32b4ca05dd13582164ed1be34ae87e22adfd1d5b" integrity sha512-UwEEdnpyhUEIDe/AkFSBUmCuwcknjAuu73fd5L9Rm/BbHczYXCrtyZmzCNVBsAiHhwUjmhNWzFlr9cAkp/sxIA== -"@expo/image-utils@0.3.23", "@expo/image-utils@0.6.3", "@expo/image-utils@^0.3.23", "@expo/image-utils@^0.6.0": +"@expo/image-utils@0.3.23", "@expo/image-utils@0.6.3", "@expo/image-utils@^0.6.0", "@expo/image-utils@^0.6.3": version "0.6.3" resolved "https://registry.yarnpkg.com/@expo/image-utils/-/image-utils-0.6.3.tgz#89c744460beefc686989b969121357bbd5520c8a" integrity sha512-v/JbCKBrHeudxn1gN1TgfPE/pWJSlLPrl29uXJBgrJFQVkViQvUHQNDhaS+UEa9wYI5HHh7XYmtzAehyG4L+GA== @@ -5090,13 +5090,13 @@ resolved "https://registry.yarnpkg.com/@miblanchard/react-native-slider/-/react-native-slider-2.3.1.tgz#79e0f1f9b1ce43ef25ee51ee9256c012e5dfa412" integrity sha512-J/hZDBWmXq8fJeOnTVHqIUVDHshqMSpJVxJ4WqwuCBKl5Rke9OBYXIdkSlgi75OgtScAr8FKK5KNkDKHUf6JIg== -"@mozzius/expo-dynamic-app-icon@^1.4.1": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@mozzius/expo-dynamic-app-icon/-/expo-dynamic-app-icon-1.4.1.tgz#245e54c31347e3ec2a1ce10f0df8cf07a0c1be6e" - integrity sha512-IiL6OiuW4kP5Jz/vrZ6U1t0m4gK1rW5VZAQzszdVcZy1cadX3EdR2/uA6jMU0qSwuesk028RhO6S0uBI9ckxBw== +"@mozzius/expo-dynamic-app-icon@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@mozzius/expo-dynamic-app-icon/-/expo-dynamic-app-icon-1.5.0.tgz#c5f88c309965b6d6b89cfd5e2c00faa7bda736af" + integrity sha512-yE2yEPO+HQmOqsX7cECh7/vu/LXnqhHGsVm3UiVi/3gaK8u5hAkPTNzZ0Qu6vnMwjPnY+uFbN6X+6Aj9c9yjMQ== dependencies: - "@expo/image-utils" "^0.3.23" - expo-modules-core "^1.0.3" + "@expo/image-utils" "^0.6.3" + expo-modules-core "^2.1.1" xcode "^3.0.1" "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": @@ -10618,20 +10618,13 @@ expo-modules-autolinking@2.0.3: require-from-string "^2.0.2" resolve-from "^5.0.0" -expo-modules-core@2.1.1: +expo-modules-core@2.1.1, expo-modules-core@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-2.1.1.tgz#970af4cfd70c8aa6fc0096dd0a6578aa003a479f" integrity sha512-yQzYCLR2mre4BNMXuqkeJ0oSNgmGEMI6BcmIzeNZbC2NFEjiaDpKvlV9bclYCtyVhUEVNbJcEPYMr6c1Y4eR4w== dependencies: invariant "^2.2.4" -expo-modules-core@^1.0.3: - version "1.12.26" - resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-1.12.26.tgz#86c4087dc6246abfc4d7f5e61097dc8cc4b22262" - integrity sha512-y8yDWjOi+rQRdO+HY+LnUlz8qzHerUaw/LUjKPU/mX8PRXP4UUPEEp5fjAwBU44xjNmYSHWZDwet4IBBE+yQUA== - dependencies: - invariant "^2.2.4" - expo-navigation-bar@~4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/expo-navigation-bar/-/expo-navigation-bar-4.0.4.tgz#058a8482665aad68d3775bcad1c7c78ae3044512" From c4fffba11b0302ccaca0392628fa048bab656aa6 Mon Sep 17 00:00:00 2001 From: Karol Stawowski <66377130+karolstawowski@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:53:00 +0100 Subject: [PATCH 41/58] feat: widen recent search profile link for mobile devices (#7119) * feat: widen recent search profile link for mobile devices * chore: apply style to non-mobile screens --- src/view/screens/Search/Search.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx index 21f9c988fb..cf00ee2bf8 100644 --- a/src/view/screens/Search/Search.tsx +++ b/src/view/screens/Search/Search.tsx @@ -1193,6 +1193,7 @@ const styles = StyleSheet.create({ }, profilePressable: { alignItems: 'center', + width: '100%', }, profileAvatar: { width: 60, From 21c288272d8e72d7b7d309ba984d1f0a8c1174c5 Mon Sep 17 00:00:00 2001 From: dan Date: Mon, 16 Dec 2024 17:35:44 +0000 Subject: [PATCH 42/58] Update more Expo packages (#7127) * Bump Expo resolutions * Bump packages * Bumpy bump --- package.json | 8 +- ...patch => expo-notifications+0.29.11.patch} | 0 ...md => expo-notifications+0.29.11.patch.md} | 0 ....26.9.patch => expo-updates+0.26.10.patch} | 0 ...patch.md => expo-updates+0.26.10.patch.md} | 0 yarn.lock | 122 +++--------------- 6 files changed, 21 insertions(+), 109 deletions(-) rename patches/{expo-notifications+0.29.10.patch => expo-notifications+0.29.11.patch} (100%) rename patches/{expo-notifications+0.29.10.patch.md => expo-notifications+0.29.11.patch.md} (100%) rename patches/{expo-updates+0.26.9.patch => expo-updates+0.26.10.patch} (100%) rename patches/{expo-updates+0.26.9.patch.md => expo-updates+0.26.10.patch.md} (100%) diff --git a/package.json b/package.json index f48ece6810..2b22284936 100644 --- a/package.json +++ b/package.json @@ -139,13 +139,13 @@ "expo-localization": "~16.0.0", "expo-media-library": "~17.0.3", "expo-navigation-bar": "~4.0.4", - "expo-notifications": "~0.29.10", + "expo-notifications": "~0.29.11", "expo-sharing": "^13.0.0", "expo-splash-screen": "~0.29.16", "expo-status-bar": "~2.0.0", "expo-system-ui": "^4.0.4", "expo-task-manager": "~12.0.3", - "expo-updates": "~0.26.9", + "expo-updates": "~0.26.10", "expo-web-browser": "~14.0.1", "fast-text-encoding": "^1.0.6", "history": "^5.3.0", @@ -268,8 +268,8 @@ "@react-native/normalize-colors": "0.76.1", "@radix-ui/react-focus-scope": "1.1.0", "@types/react": "^18", - "**/expo-constants": "16.0.1", - "**/expo-device": "6.0.2", + "**/expo-constants": "17.0.3", + "**/expo-device": "7.0.1", "**/zod": "3.23.8" }, "jest": { diff --git a/patches/expo-notifications+0.29.10.patch b/patches/expo-notifications+0.29.11.patch similarity index 100% rename from patches/expo-notifications+0.29.10.patch rename to patches/expo-notifications+0.29.11.patch diff --git a/patches/expo-notifications+0.29.10.patch.md b/patches/expo-notifications+0.29.11.patch.md similarity index 100% rename from patches/expo-notifications+0.29.10.patch.md rename to patches/expo-notifications+0.29.11.patch.md diff --git a/patches/expo-updates+0.26.9.patch b/patches/expo-updates+0.26.10.patch similarity index 100% rename from patches/expo-updates+0.26.9.patch rename to patches/expo-updates+0.26.10.patch diff --git a/patches/expo-updates+0.26.9.patch.md b/patches/expo-updates+0.26.10.patch.md similarity index 100% rename from patches/expo-updates+0.26.9.patch.md rename to patches/expo-updates+0.26.10.patch.md diff --git a/yarn.lock b/yarn.lock index e5bec88706..13b9841c66 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3800,27 +3800,6 @@ xcode "^3.0.1" xml2js "0.6.0" -"@expo/config-plugins@~8.0.0-beta.0": - version "8.0.11" - resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-8.0.11.tgz#b814395a910f4c8b7cc95d9719dccb6ca53ea4c5" - integrity sha512-oALE1HwnLFthrobAcC9ocnR9KXLzfWEjgIe4CPe+rDsfC6GDs8dGYCXfRFoCEzoLN4TGYs9RdZ8r0KoCcNrm2A== - dependencies: - "@expo/config-types" "^51.0.3" - "@expo/json-file" "~8.3.0" - "@expo/plist" "^0.1.0" - "@expo/sdk-runtime-versions" "^1.0.0" - chalk "^4.1.2" - debug "^4.3.1" - find-up "~5.0.0" - getenv "^1.0.0" - glob "7.1.6" - resolve-from "^5.0.0" - semver "^7.5.4" - slash "^3.0.0" - slugify "^1.6.6" - xcode "^3.0.1" - xml2js "0.6.0" - "@expo/config-plugins@~9.0.12": version "9.0.12" resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-9.0.12.tgz#f122b2dca22e135eadf6e73442da3ced0ce8aa0a" @@ -3841,16 +3820,6 @@ xcode "^3.0.1" xml2js "0.6.0" -"@expo/config-types@^51.0.0-unreleased": - version "51.0.0" - resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-51.0.0.tgz#f5df238cd1237d7e4d9cc8217cdef3383c2a00cf" - integrity sha512-acn03/u8mQvBhdTQtA7CNhevMltUhbSrpI01FYBJwpVntufkU++ncQujWKlgY/OwIajcfygk1AY4xcNZ5ImkRA== - -"@expo/config-types@^51.0.3": - version "51.0.3" - resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-51.0.3.tgz#520bdce5fd75f9d234fd81bd0347443086419450" - integrity sha512-hMfuq++b8VySb+m9uNNrlpbvGxYc8OcFCUX9yTmi9tlx6A4k8SDabWFBgmnr4ao3wEArvWrtUQIfQCVtPRdpKA== - "@expo/config-types@^52.0.0": version "52.0.1" resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-52.0.1.tgz#327af1b72a3a9d4556f41e083e0e284dd8198b96" @@ -3894,23 +3863,6 @@ slugify "^1.3.4" sucrase "3.35.0" -"@expo/config@~9.0.0-beta.0": - version "9.0.1" - resolved "https://registry.yarnpkg.com/@expo/config/-/config-9.0.1.tgz#e7b79de5af29d5ab2a98a62c3cda31f03bd75827" - integrity sha512-0tjaXBstTbXmD4z+UMFBkh2SZFwilizSQhW6DlaTMnPG5ezuw93zSFEWAuEC3YzkpVtNQTmYzxAYjxwh6seOGg== - dependencies: - "@babel/code-frame" "~7.10.4" - "@expo/config-plugins" "~8.0.0-beta.0" - "@expo/config-types" "^51.0.0-unreleased" - "@expo/json-file" "^8.3.0" - getenv "^1.0.0" - glob "7.1.6" - require-from-string "^2.0.2" - resolve-from "^5.0.0" - semver "^7.6.0" - slugify "^1.3.4" - sucrase "3.34.0" - "@expo/devcert@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@expo/devcert/-/devcert-1.1.2.tgz#a4923b8ea5b34fde31d6e006a40d0f594096a0ed" @@ -3978,7 +3930,7 @@ temp-dir "~2.0.0" unique-string "~2.0.0" -"@expo/json-file@^8.3.0", "@expo/json-file@~8.3.0": +"@expo/json-file@^8.3.0": version "8.3.3" resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-8.3.3.tgz#7926e3592f76030ce63d6b1308ac8f5d4d9341f4" integrity sha512-eZ5dld9AD0PrVRiIWpRkm5aIoWBw3kAyd8VkuWEy92sEthBKDDDHAnK2a0dw0Eil6j7rK7lS/Qaq/Zzngv2h5A== @@ -4070,15 +4022,6 @@ split "^1.0.1" sudo-prompt "9.1.1" -"@expo/plist@^0.1.0": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.1.3.tgz#b4fbee2c4f7a88512a4853d85319f4d95713c529" - integrity sha512-GW/7hVlAylYg1tUrEASclw1MMk9FP4ZwyFAY/SUTJIhPDQHtfOlXREyWV3hhrHdX/K+pS73GNgdfT6E/e+kBbg== - dependencies: - "@xmldom/xmldom" "~0.7.7" - base64-js "^1.2.3" - xmlbuilder "^14.0.0" - "@expo/plist@^0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.2.0.tgz#beb014c0bfd56a993086c0972ec1ca3ef3f9d36c" @@ -10459,12 +10402,13 @@ expo-clipboard@^7.0.0: resolved "https://registry.yarnpkg.com/expo-clipboard/-/expo-clipboard-7.0.0.tgz#066b1a781fdaf05e30f282522d3a58f2e651e4cf" integrity sha512-4Vuv1zZPTOiKzIeC0BIGUN8nyzkXlE6jKchtLxcoksBjHPdG5W2eH05B+hppTrK9N3+Xh02z4j3h1cFRqPJ1fw== -expo-constants@16.0.1, expo-constants@^13.0.2, expo-constants@~17.0.0, expo-constants@~17.0.3: - version "16.0.1" - resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-16.0.1.tgz#1285e29c85513c6e88e118289e2baab72596d3f7" - integrity sha512-s6aTHtglp926EsugWtxN7KnpSsE9FCEjb7CgEjQQ78Gpu4btj4wB+IXot2tlqNwqv+x7xFe5veoPGfJDGF/kVg== +expo-constants@17.0.3, expo-constants@^13.0.2, expo-constants@~17.0.0, expo-constants@~17.0.3: + version "17.0.3" + resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-17.0.3.tgz#a05b38e0417d59759ece1642b4d483889e04dbda" + integrity sha512-lnbcX2sAu8SucHXEXxSkhiEpqH+jGrf+TF+MO6sHWIESjwOUVVYlT8qYdjR9xbxWmqFtrI4KV44FkeJf2DaFjQ== dependencies: - "@expo/config" "~9.0.0-beta.0" + "@expo/config" "~10.0.4" + "@expo/env" "~0.4.0" expo-dev-client@^5.0.4: version "5.0.4" @@ -10499,14 +10443,7 @@ expo-dev-menu@6.0.11: dependencies: expo-dev-menu-interface "1.9.2" -expo-device@6.0.2, expo-device@~4.1.1: - version "6.0.2" - resolved "https://registry.yarnpkg.com/expo-device/-/expo-device-6.0.2.tgz#9bc3eccd16509c2819c225cc2ca8f7c3e3bdd11e" - integrity sha512-sCt91CuTmAuMXX4SlFOn4lIos2UIr8vb0jDstDDZXys6kErcj0uynC7bQAMreU5uRUTKMAl4MAMpKt9ufCXPBw== - dependencies: - ua-parser-js "^0.7.33" - -expo-device@~7.0.1: +expo-device@7.0.1, expo-device@~4.1.1, expo-device@~7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/expo-device/-/expo-device-7.0.1.tgz#3702fe8b4475eac63ed27f9d580ec8a78546e0d1" integrity sha512-/3lk0f9wvle+6svHqWSCBC1B5NYFmXp1D7hmIyecJJVYRLwzrwwTDyNs76oG/UDU5Appdu8QyDKycsx2hqv71w== @@ -10633,10 +10570,10 @@ expo-navigation-bar@~4.0.4: "@react-native/normalize-colors" "0.76.3" debug "^4.3.2" -expo-notifications@~0.29.10: - version "0.29.10" - resolved "https://registry.yarnpkg.com/expo-notifications/-/expo-notifications-0.29.10.tgz#1ab41840db380fc775216e1fb3c07e7a1abd71bd" - integrity sha512-sNPAQxwWVR759iCM816gEU4+8MY08CAs+Cmp8VUkBCnPWZaz2pV30nEkwRhV3wjv+Sz78oIRkkVMVaHCa1XUVA== +expo-notifications@~0.29.11: + version "0.29.11" + resolved "https://registry.yarnpkg.com/expo-notifications/-/expo-notifications-0.29.11.tgz#0691f88c91f6598671cec8e2ff12922ea1493edf" + integrity sha512-u/Csc3YNOPjjuyjAeyj5ne7XR/Z0ABYVquhSnyjEj2Fp8mSldOPCMvaEA01pTFj+8HTlkjX5RZDvQ7cR62ngOA== dependencies: "@expo/image-utils" "^0.6.0" "@ide/backoff" "^1.0.0" @@ -10698,10 +10635,10 @@ expo-updates-interface@~1.0.0: resolved "https://registry.yarnpkg.com/expo-updates-interface/-/expo-updates-interface-1.0.0.tgz#b98c66b800d29561c62409556948b2af3d5316e5" integrity sha512-93oWtvULJOj+Pp+N/lpTcFfuREX1wNeHtp7Lwn8EbzYYmdn37MvZU3TPW2tYYCZuhzmKEXnUblYcruYoDu7IrQ== -expo-updates@~0.26.9: - version "0.26.9" - resolved "https://registry.yarnpkg.com/expo-updates/-/expo-updates-0.26.9.tgz#9be11c482c572f90d479aa1f7bd1ac509d7fa102" - integrity sha512-eHdvM4+xXaLV3uuTn3ooWsDC69IPV9ukpuPDglsBWJKagVi40u7DJQ375oGNgjqxd91irEBeBSCr9tCWm905QA== +expo-updates@~0.26.10: + version "0.26.10" + resolved "https://registry.yarnpkg.com/expo-updates/-/expo-updates-0.26.10.tgz#b39c77841b609b34e0e4e239f37e8d5e4da3c5e5" + integrity sha512-ETGUaSZRL7x72RH6MbZWRpyU9GFzCixIPNUT0kf/hcD07ojyHlW5hcwgc5ve565THSvhgiumz3yImKLbKBv2JA== dependencies: "@expo/code-signing-certificates" "0.0.5" "@expo/config" "~10.0.4" @@ -11056,7 +10993,7 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" -find-up@^5.0.0, find-up@~5.0.0: +find-up@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== @@ -11351,18 +11288,6 @@ glob@7.0.6: once "^1.3.0" path-is-absolute "^1.0.0" -glob@7.1.6: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@^10.2.2: version "10.4.1" resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.1.tgz#0cfb01ab6a6b438177bfe6a58e2576f6efe909c2" @@ -17741,19 +17666,6 @@ styleq@^0.1.3: resolved "https://registry.yarnpkg.com/styleq/-/styleq-0.1.3.tgz#8efb2892debd51ce7b31dc09c227ad920decab71" integrity sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA== -sucrase@3.34.0: - version "3.34.0" - resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.34.0.tgz#1e0e2d8fcf07f8b9c3569067d92fbd8690fb576f" - integrity sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw== - dependencies: - "@jridgewell/gen-mapping" "^0.3.2" - commander "^4.0.0" - glob "7.1.6" - lines-and-columns "^1.1.6" - mz "^2.7.0" - pirates "^4.0.1" - ts-interface-checker "^0.1.9" - sucrase@3.35.0: version "3.35.0" resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.0.tgz#57f17a3d7e19b36d8995f06679d121be914ae263" From 3ed237b6290c3f3d6ce143e8c63e2e888cf7b47a Mon Sep 17 00:00:00 2001 From: dan Date: Mon, 16 Dec 2024 21:46:39 +0000 Subject: [PATCH 43/58] Bump more native deps (#7129) * Undo expo modules patch * Upgrade expo modules release * Patch facebook/react-native#46944 * Remove explicit prebuild config dep * Bump to rm duplicates --- package.json | 7 +- patches/expo-modules-core+2.1.1.patch | 35 ---- ...re+2.1.1.md => expo-modules-core+2.1.2.md} | 0 patches/expo-modules-core+2.1.2.patch | 15 ++ patches/react-native+0.76.3.patch | 37 ++++- yarn.lock | 157 +++++++++--------- 6 files changed, 135 insertions(+), 116 deletions(-) delete mode 100644 patches/expo-modules-core+2.1.1.patch rename patches/{expo-modules-core+2.1.1.md => expo-modules-core+2.1.2.md} (100%) create mode 100644 patches/expo-modules-core+2.1.2.patch diff --git a/package.json b/package.json index 2b22284936..f5f4601d6c 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ "emoji-mart": "^5.5.2", "emoji-regex": "^10.4.0", "eventemitter3": "^5.0.1", - "expo": "~52.0.17", + "expo": "~52.0.19", "expo-application": "^6.0.1", "expo-blur": "^14.0.1", "expo-build-properties": "^0.13.1", @@ -128,7 +128,7 @@ "expo-clipboard": "^7.0.0", "expo-dev-client": "^5.0.4", "expo-device": "~7.0.1", - "expo-file-system": "^18.0.4", + "expo-file-system": "^18.0.6", "expo-font": "~13.0.1", "expo-haptics": "^14.0.0", "expo-image": "~2.0.3", @@ -141,7 +141,7 @@ "expo-navigation-bar": "~4.0.4", "expo-notifications": "~0.29.11", "expo-sharing": "^13.0.0", - "expo-splash-screen": "~0.29.16", + "expo-splash-screen": "~0.29.18", "expo-status-bar": "~2.0.0", "expo-system-ui": "^4.0.4", "expo-task-manager": "~12.0.3", @@ -213,7 +213,6 @@ "@babel/runtime": "^7.26.0", "@did-plc/server": "^0.0.1", "@expo/config-plugins": "9.0.10", - "@expo/prebuild-config": "8.0.22", "@lingui/cli": "^4.14.1", "@lingui/macro": "^4.14.1", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15", diff --git a/patches/expo-modules-core+2.1.1.patch b/patches/expo-modules-core+2.1.1.patch deleted file mode 100644 index 5695b8f44a..0000000000 --- a/patches/expo-modules-core+2.1.1.patch +++ /dev/null @@ -1,35 +0,0 @@ -diff --git a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt -index 47c4d15..afe138d 100644 ---- a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt -+++ b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt -@@ -125,6 +125,10 @@ internal fun peekResponseBody( - } - - internal fun shouldParseBody(response: Response): Boolean { -+ if (response.request.url.encodedPath == "/bitdrift_public.protobuf.client.v1.ApiService/Mux") { -+ return false -+ } -+ - // Check for Content-Type - val skipContentTypes = listOf( - "text/event-stream", // Server Sent Events -diff --git a/node_modules/expo-modules-core/src/uuid/uuid.ts b/node_modules/expo-modules-core/src/uuid/uuid.ts -index 148beac..fabdff5 100644 ---- a/node_modules/expo-modules-core/src/uuid/uuid.ts -+++ b/node_modules/expo-modules-core/src/uuid/uuid.ts -@@ -5,6 +5,7 @@ const nativeUuidv4 = globalThis?.expo?.uuidv4; - const nativeUuidv5 = globalThis?.expo?.uuidv5; - - function uuidv4(): string { -+ const nativeUuidv4 = globalThis?.expo?.uuidv4; - if (!nativeUuidv4) { - throw Error( - "Native UUID version 4 generator implementation wasn't found in `expo-modules-core`" -@@ -23,6 +24,7 @@ function uuidv5(name: string, namespace: string | number[]) { - throw new Error('`namespace` must be a valid UUID string or an Array of 16 byte values'); - } - -+ const nativeUuidv5 = globalThis?.expo?.uuidv5; - if (!nativeUuidv5) { - throw Error("Native UUID type 5 generator implementation wasn't found in `expo-modules-core`"); - } diff --git a/patches/expo-modules-core+2.1.1.md b/patches/expo-modules-core+2.1.2.md similarity index 100% rename from patches/expo-modules-core+2.1.1.md rename to patches/expo-modules-core+2.1.2.md diff --git a/patches/expo-modules-core+2.1.2.patch b/patches/expo-modules-core+2.1.2.patch new file mode 100644 index 0000000000..f3d9bfd149 --- /dev/null +++ b/patches/expo-modules-core+2.1.2.patch @@ -0,0 +1,15 @@ +diff --git a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt +index 47c4d15..afe138d 100644 +--- a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt ++++ b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt +@@ -125,6 +125,10 @@ internal fun peekResponseBody( + } + + internal fun shouldParseBody(response: Response): Boolean { ++ if (response.request.url.encodedPath == "/bitdrift_public.protobuf.client.v1.ApiService/Mux") { ++ return false ++ } ++ + // Check for Content-Type + val skipContentTypes = listOf( + "text/event-stream", // Server Sent Events diff --git a/patches/react-native+0.76.3.patch b/patches/react-native+0.76.3.patch index 6f71097a18..5af24a372b 100644 --- a/patches/react-native+0.76.3.patch +++ b/patches/react-native+0.76.3.patch @@ -222,7 +222,7 @@ index 40aaf9c..1c60164 100644 { [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { diff --git a/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m b/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m -index e9ce48c..ccd9ad6 100644 +index e9ce48c..84a6fca 100644 --- a/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m +++ b/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m @@ -159,26 +159,8 @@ - (BOOL)touchesShouldCancelInContentView:(__unused UIView *)view @@ -329,3 +329,38 @@ index e9ce48c..ccd9ad6 100644 } } +@@ -1055,6 +1082,22 @@ -(type)getter \ + RCT_SET_AND_PRESERVE_OFFSET(setShowsVerticalScrollIndicator, showsVerticalScrollIndicator, BOOL) + RCT_SET_AND_PRESERVE_OFFSET(setZoomScale, zoomScale, CGFloat); + ++- (void)setScrollIndicatorInsets:(UIEdgeInsets)value ++{ ++ [_scrollView setScrollIndicatorInsets:value]; ++} ++ ++- (UIEdgeInsets)scrollIndicatorInsets ++{ ++ UIEdgeInsets verticalScrollIndicatorInsets = [_scrollView verticalScrollIndicatorInsets]; ++ UIEdgeInsets horizontalScrollIndicatorInsets = [_scrollView horizontalScrollIndicatorInsets]; ++ return UIEdgeInsetsMake( ++ verticalScrollIndicatorInsets.top, ++ horizontalScrollIndicatorInsets.left, ++ verticalScrollIndicatorInsets.bottom, ++ horizontalScrollIndicatorInsets.right); ++} ++ + - (void)setAutomaticallyAdjustsScrollIndicatorInsets:(BOOL)automaticallyAdjusts API_AVAILABLE(ios(13.0)) + { + // `automaticallyAdjustsScrollIndicatorInsets` is available since iOS 13. +diff --git a/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m b/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m +index cd1e7eb..c1d0172 100644 +--- a/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m ++++ b/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m +@@ -83,6 +83,7 @@ - (UIView *)view + RCT_EXPORT_VIEW_PROPERTY(scrollEventThrottle, NSTimeInterval) + RCT_EXPORT_VIEW_PROPERTY(zoomScale, CGFloat) + RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets) ++RCT_EXPORT_VIEW_PROPERTY(scrollIndicatorInsets, UIEdgeInsets) + RCT_EXPORT_VIEW_PROPERTY(verticalScrollIndicatorInsets, UIEdgeInsets) + RCT_EXPORT_VIEW_PROPERTY(scrollToOverflowEnabled, BOOL) + RCT_EXPORT_VIEW_PROPERTY(snapToInterval, int) diff --git a/yarn.lock b/yarn.lock index 13b9841c66..f6598f8b8d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3695,10 +3695,10 @@ mv "~2" safe-json-stringify "~1" -"@expo/cli@0.22.3": - version "0.22.3" - resolved "https://registry.yarnpkg.com/@expo/cli/-/cli-0.22.3.tgz#8dbcc9396abf01b2dd91fe7f34fc23fdd2d8cc7f" - integrity sha512-1HBtqInFDFHUJWzTJ1CJj5MR3JwvOiozmRUWF2kVQAeq/bKzSYM6We6B3XoZBM5XP6z6WtnrG87C7BjeW5E/cA== +"@expo/cli@0.22.6": + version "0.22.6" + resolved "https://registry.yarnpkg.com/@expo/cli/-/cli-0.22.6.tgz#dc27b685d2252027549d839437c5285c7814ec3f" + integrity sha512-eDjCnc3uHl2+SJ6aZ5seK0FkMp0W12oAdSI4A/yV8ecYtXzG8X87sfKAISEWt44B4DqJ0a1LEqCD6Vtvc783Mg== dependencies: "@0no-co/graphql.web" "^1.0.8" "@babel/runtime" "^7.20.0" @@ -3709,15 +3709,15 @@ "@expo/env" "~0.4.0" "@expo/image-utils" "^0.6.0" "@expo/json-file" "^9.0.0" - "@expo/metro-config" "~0.19.0" + "@expo/metro-config" "~0.19.8" "@expo/osascript" "^2.0.31" "@expo/package-manager" "^1.5.0" "@expo/plist" "^0.2.0" - "@expo/prebuild-config" "^8.0.22" + "@expo/prebuild-config" "^8.0.23" "@expo/rudder-sdk-node" "^1.1.1" "@expo/spawn-async" "^1.7.2" "@expo/xcpretty" "^4.3.0" - "@react-native/dev-middleware" "0.76.3" + "@react-native/dev-middleware" "0.76.5" "@urql/core" "^5.0.6" "@urql/exchange-retry" "^1.3.0" accepts "^1.3.8" @@ -3893,10 +3893,10 @@ dotenv-expand "~11.0.6" getenv "^1.0.0" -"@expo/fingerprint@0.11.3": - version "0.11.3" - resolved "https://registry.yarnpkg.com/@expo/fingerprint/-/fingerprint-0.11.3.tgz#e370ae8f83e0642f752b058e2102e984a0a5bc98" - integrity sha512-9lgXmcIePvZ7Wef63XtvuN3HfCUevF4E4tQPdEbH9/dUWwpOvvwQ3KT4OJ9jdh8JJ3nTdO9eDQ/8k8xr1aQ5Kg== +"@expo/fingerprint@0.11.4": + version "0.11.4" + resolved "https://registry.yarnpkg.com/@expo/fingerprint/-/fingerprint-0.11.4.tgz#f40bbc784e10a065b783091e0d060a7428d41a7c" + integrity sha512-FfcvHjrWjOJ17wiMfr1iQ1YDyjlj8qfxG+GDce0khrjNSkzRjVdCOIFsMvfVSBPnOPX5NuZlgMRvMkcPUtGClA== dependencies: "@expo/spawn-async" "^1.7.2" arg "^5.0.2" @@ -3948,34 +3948,10 @@ json5 "^2.2.3" write-file-atomic "^2.3.0" -"@expo/metro-config@0.19.6": - version "0.19.6" - resolved "https://registry.yarnpkg.com/@expo/metro-config/-/metro-config-0.19.6.tgz#81bbe70c77a322d6c688738fd7b736a3cbb7c5bd" - integrity sha512-pRwZyOstsQa1+Ecss3wOqC28wjyjq9qxvJaQL3LH4G8Sef9x2PX+ySRApeQ01nl4ZN5nlyez6iVDF51tn/WhOw== - dependencies: - "@babel/core" "^7.20.0" - "@babel/generator" "^7.20.5" - "@babel/parser" "^7.20.0" - "@babel/types" "^7.20.0" - "@expo/config" "~10.0.4" - "@expo/env" "~0.4.0" - "@expo/json-file" "~9.0.0" - "@expo/spawn-async" "^1.7.2" - chalk "^4.1.0" - debug "^4.3.2" - fs-extra "^9.1.0" - getenv "^1.0.0" - glob "^10.4.2" - jsc-safe-url "^0.2.4" - lightningcss "~1.27.0" - minimatch "^3.0.4" - postcss "~8.4.32" - resolve-from "^5.0.0" - -"@expo/metro-config@~0.19.0": - version "0.19.4" - resolved "https://registry.yarnpkg.com/@expo/metro-config/-/metro-config-0.19.4.tgz#940b6fad7809a92a8ffdb1bbe87aa805f5822c6b" - integrity sha512-2SWwYN8MZvMIRawWEr+1RBYncitPwu2VMACRYig+wBycJ9fsPb6BMVmBYi+3MHDUlJHNy/Bqfw++jn1eqBFETQ== +"@expo/metro-config@0.19.8", "@expo/metro-config@~0.19.8": + version "0.19.8" + resolved "https://registry.yarnpkg.com/@expo/metro-config/-/metro-config-0.19.8.tgz#f1ea552b6fa5217093fe364ff5ca78a7e261a28b" + integrity sha512-dVAOetouQYuOTEJ2zR0OTLNPOH6zPkeEt5fY53TK0Wxi1QmtsmH6vEWg05U4zkSJ6f1aXmQ0Za77R8QxuukESA== dependencies: "@babel/core" "^7.20.0" "@babel/generator" "^7.20.5" @@ -4031,17 +4007,17 @@ base64-js "^1.2.3" xmlbuilder "^14.0.0" -"@expo/prebuild-config@8.0.22", "@expo/prebuild-config@^8.0.22": - version "8.0.22" - resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-8.0.22.tgz#6e2762a5f333a0519f83ae05b69da45e3e26a913" - integrity sha512-Kwlf3ymHH37W2nuNA9FzYgZvrImJScLA98939kapnOxfNGAPhmhEw26sfIGmBWAa8ymdL6p+HXQ3+b/xJ74bOg== +"@expo/prebuild-config@^8.0.23": + version "8.0.23" + resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-8.0.23.tgz#2ec6d5464f35d308bdb94ba75b7e6aba0ebb507d" + integrity sha512-Zf01kFiN2PISmLb0DhIAJh76v3J2oYUKSjiAtGZLOH0HUz59by/qdyU4mGHWdeyRdCCrLUA21Rct2MBykvRMsg== dependencies: "@expo/config" "~10.0.4" "@expo/config-plugins" "~9.0.10" "@expo/config-types" "^52.0.0" "@expo/image-utils" "^0.6.0" "@expo/json-file" "^9.0.0" - "@react-native/normalize-colors" "0.76.3" + "@react-native/normalize-colors" "0.76.5" debug "^4.3.1" fs-extra "^9.0.0" resolve-from "^5.0.0" @@ -5616,7 +5592,7 @@ dependencies: "@react-native/codegen" "0.76.1" -"@react-native/babel-preset@0.76.1", "@react-native/babel-preset@0.76.3": +"@react-native/babel-preset@0.76.1", "@react-native/babel-preset@0.76.3", "@react-native/babel-preset@0.76.5": version "0.76.1" resolved "https://registry.yarnpkg.com/@react-native/babel-preset/-/babel-preset-0.76.1.tgz#2b9fd113e7c7889c1e87d6a36b7cb0f36118e7a6" integrity sha512-b6YRmA13CmVuTQKHRen/Q0glHwmZFZoEDs+MJ1NL0UNHq9V5ytvdwTW1ntkmjtXuTnPMzkwYvumJBN9UTZjkBA== @@ -5717,6 +5693,11 @@ resolved "https://registry.yarnpkg.com/@react-native/debugger-frontend/-/debugger-frontend-0.76.3.tgz#531e616f6dad159a58117efc69cec20422d15b0d" integrity sha512-pMHQ3NpPB28RxXciSvm2yD+uDx3pkhzfuWkc7VFgOduyzPSIr0zotUiOJzsAtrj8++bPbOsAraCeQhCqoOTWQw== +"@react-native/debugger-frontend@0.76.5": + version "0.76.5" + resolved "https://registry.yarnpkg.com/@react-native/debugger-frontend/-/debugger-frontend-0.76.5.tgz#0e89940543fb5029506690b83f12547d0bf42cc4" + integrity sha512-5gtsLfBaSoa9WP8ToDb/8NnDBLZjv4sybQQj7rDKytKOdsXm3Pr2y4D7x7GQQtP1ZQRqzU0X0OZrhRz9xNnOqA== + "@react-native/dev-middleware@0.76.3": version "0.76.3" resolved "https://registry.yarnpkg.com/@react-native/dev-middleware/-/dev-middleware-0.76.3.tgz#52edc76c88e0c2c436eb989551b827bf69f2a56f" @@ -5734,6 +5715,23 @@ serve-static "^1.13.1" ws "^6.2.3" +"@react-native/dev-middleware@0.76.5": + version "0.76.5" + resolved "https://registry.yarnpkg.com/@react-native/dev-middleware/-/dev-middleware-0.76.5.tgz#10d02fcc6c3c9d24f6dc147c2ef95d6fa6bd3787" + integrity sha512-f8eimsxpkvMgJia7POKoUu9uqjGF6KgkxX4zqr/a6eoR1qdEAWUd6PonSAqtag3PAqvEaJpB99gLH2ZJI1nDGg== + dependencies: + "@isaacs/ttlcache" "^1.4.1" + "@react-native/debugger-frontend" "0.76.5" + chrome-launcher "^0.15.2" + chromium-edge-launcher "^0.2.0" + connect "^3.6.5" + debug "^2.2.0" + nullthrows "^1.1.1" + open "^7.0.3" + selfsigned "^2.4.1" + serve-static "^1.13.1" + ws "^6.2.3" + "@react-native/eslint-config@^0.76.2": version "0.76.2" resolved "https://registry.yarnpkg.com/@react-native/eslint-config/-/eslint-config-0.76.2.tgz#2741eee69ff194b8adc15281c0cb9695ba015ef0" @@ -5778,7 +5776,7 @@ hermes-parser "0.23.1" nullthrows "^1.1.1" -"@react-native/normalize-colors@0.76.1", "@react-native/normalize-colors@0.76.3", "@react-native/normalize-colors@^0.73.0", "@react-native/normalize-colors@^0.74.1": +"@react-native/normalize-colors@0.76.1", "@react-native/normalize-colors@0.76.3", "@react-native/normalize-colors@0.76.5", "@react-native/normalize-colors@^0.73.0", "@react-native/normalize-colors@^0.74.1": version "0.76.1" resolved "https://registry.yarnpkg.com/@react-native/normalize-colors/-/normalize-colors-0.76.1.tgz#df8d54d78917a9f075283382fec834f5ccaecefd" integrity sha512-/+CUk/wGWIdXbJYVLw/q6Fs8Z0x91zzfXIbNiZUdSW1TNEDmytkF371H8a1/Nx3nWa1RqCMVsaZHCG4zqxeDvg== @@ -8088,10 +8086,10 @@ babel-preset-expo@^12.0.2: babel-plugin-react-native-web "~0.19.13" react-refresh "^0.14.2" -babel-preset-expo@~12.0.3: - version "12.0.3" - resolved "https://registry.yarnpkg.com/babel-preset-expo/-/babel-preset-expo-12.0.3.tgz#2ad62fe007517704841788cfea38b333e307663f" - integrity sha512-1695e8y3U/HjifKx33vcNnFMSUSXwPWwhFxRlL6NRx2TENN6gySH82gPOWgxcra6gi+EJgXx52xG3PcqTjwW6w== +babel-preset-expo@~12.0.4: + version "12.0.4" + resolved "https://registry.yarnpkg.com/babel-preset-expo/-/babel-preset-expo-12.0.4.tgz#ec965530d866c8905aac1fa478562cb08ab32a55" + integrity sha512-SAzAwqpyjA+/OFrU95OOioj6oTeCv4+rRfrNmBTy5S/gJswrZKBSPJioFudIaJBy43W+BL7HA5AspBIF6tO/aA== dependencies: "@babel/plugin-proposal-decorators" "^7.12.9" "@babel/plugin-transform-export-namespace-from" "^7.22.11" @@ -8099,7 +8097,7 @@ babel-preset-expo@~12.0.3: "@babel/plugin-transform-parameters" "^7.22.15" "@babel/preset-react" "^7.22.15" "@babel/preset-typescript" "^7.23.0" - "@react-native/babel-preset" "0.76.3" + "@react-native/babel-preset" "0.76.5" babel-plugin-react-native-web "~0.19.13" react-refresh "^0.14.2" @@ -10455,10 +10453,10 @@ expo-eas-client@~0.13.0: resolved "https://registry.yarnpkg.com/expo-eas-client/-/expo-eas-client-0.13.1.tgz#ebca627f3f58a54906394eb3f5d22f41a1822618" integrity sha512-IyeDiM6YSJG0c45kbuEo0qt76z0KTEZtisEFEtle+b+vfn9I3N+r3jbPscaI4yS3P6gpuoDyHv81YDVC6Dmkhw== -expo-file-system@^18.0.4, expo-file-system@~18.0.4: - version "18.0.4" - resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-18.0.4.tgz#eecf8dc0b3b545e9ac5cd00352665afe2d57732f" - integrity sha512-aAWEDwnu0XHOBYvQ9Q0+QIa+483vYJaC4IDsXyWQ73Rtsg273NZh5kYowY+cAocvoSmA99G6htrLBn11ax2bTQ== +expo-file-system@^18.0.6, expo-file-system@~18.0.6: + version "18.0.6" + resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-18.0.6.tgz#43f7718530d0e2aa1f49bca7ccb721007acabf2c" + integrity sha512-gGEwIJCXV3/wpIJ/wRyhmieLOSAY7HeFFjb+wEfHs04aE63JYR+rXXV4b7rBpEh1ZgNV9U91zfet/iQG7J8HBQ== dependencies: web-streams-polyfill "^3.3.2" @@ -10541,10 +10539,10 @@ expo-media-library@~17.0.3: resolved "https://registry.yarnpkg.com/expo-media-library/-/expo-media-library-17.0.3.tgz#4ee3e6a8a2544887d910a72eaf2a15858b78cc0e" integrity sha512-vo8AqWxv1C8+U8dA5W43qs8+3dgD3VZDvcCkZBQTBnGr/2Rs7x6nNQD5s7UfYyr6qmW6102JB3+OUKHpkwEssg== -expo-modules-autolinking@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/expo-modules-autolinking/-/expo-modules-autolinking-2.0.3.tgz#c0de0129bedf1b6f9aa36093e435d00509f27fcd" - integrity sha512-Q/ALJ54eS7Cr7cmbP+unEDTkHFQivQerWWrqZxuXOrSFYGCYU22+/xAZXaJOpZwseOVsP74zSkoRY/wBimVs7w== +expo-modules-autolinking@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/expo-modules-autolinking/-/expo-modules-autolinking-2.0.4.tgz#28fcd12fb0d066a2933cca3bf3b597da0f6b2f2a" + integrity sha512-e0p+19NhmD50U7s7BV7kWIypWmTNC9n/VlJKlXS05hM/zX7pe6JKmXyb+BFnXJq3SLBalLCUY0tu2gEUF3XeVg== dependencies: "@expo/spawn-async" "^1.7.2" chalk "^4.1.0" @@ -10555,7 +10553,14 @@ expo-modules-autolinking@2.0.3: require-from-string "^2.0.2" resolve-from "^5.0.0" -expo-modules-core@2.1.1, expo-modules-core@^2.1.1: +expo-modules-core@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-2.1.2.tgz#258be4fbd162b69eb4ad2789131ac2dc7e85fc08" + integrity sha512-0OhMU5S8zf9c/CRh1MwiXfOInI9wzz6yiIh5RuR/9J7N6xHRum68hInsPbaSc1UQpo08ZZLM4MPsbpoNRUoqIg== + dependencies: + invariant "^2.2.4" + +expo-modules-core@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-2.1.1.tgz#970af4cfd70c8aa6fc0096dd0a6578aa003a479f" integrity sha512-yQzYCLR2mre4BNMXuqkeJ0oSNgmGEMI6BcmIzeNZbC2NFEjiaDpKvlV9bclYCtyVhUEVNbJcEPYMr6c1Y4eR4w== @@ -10598,12 +10603,12 @@ expo-sharing@^13.0.0: resolved "https://registry.yarnpkg.com/expo-sharing/-/expo-sharing-13.0.0.tgz#fbc46f4afdaa265a2811fe88c2a589aae2d2de0f" integrity sha512-b23ymicRmYn/Pjj05sl9tFZHN5cH9I1f0yiqY1Yk8Q3oCx0Aznri82DnTYA4T/J6D9vrkraX0wQ4jWVMOffmlg== -expo-splash-screen@~0.29.16: - version "0.29.16" - resolved "https://registry.yarnpkg.com/expo-splash-screen/-/expo-splash-screen-0.29.16.tgz#80b49af2605213e661a40022d724caa1ea48ccb3" - integrity sha512-1WnExDA23hEJhz+djUthVUWxUvVtDT9sqRrpCgU4srG2OfBN0NryJ+Fbnoc1V2xw2uYc4Ij3ru0nH9a1TNvW9w== +expo-splash-screen@~0.29.18: + version "0.29.18" + resolved "https://registry.yarnpkg.com/expo-splash-screen/-/expo-splash-screen-0.29.18.tgz#96ccce3d5a03389a9061743903b0a77c22a16796" + integrity sha512-bTBY+LF6YtYen2j60yGNh2SX/tG4UXZAyBCMMriOSiZZ7LSCs3ARyEufaSiWk+ckWShTeMqItOnaAN/CAF8MJA== dependencies: - "@expo/prebuild-config" "^8.0.22" + "@expo/prebuild-config" "^8.0.23" expo-status-bar@~2.0.0: version "2.0.0" @@ -10660,26 +10665,26 @@ expo-web-browser@~14.0.1: resolved "https://registry.yarnpkg.com/expo-web-browser/-/expo-web-browser-14.0.1.tgz#97f3f141b0897364bc8364d90d6e29df0beec8aa" integrity sha512-QM9F3ie+UyIOoBvqFmT6CZojb1vMc2H+7ZlMT5dEu1PL2jtYyOeK2hLfbt/EMt7CBm/w+P29H9W9Y9gdebOkuQ== -expo@~52.0.17: - version "52.0.17" - resolved "https://registry.yarnpkg.com/expo/-/expo-52.0.17.tgz#8a3edc20dabdb69a47f7b4b92e1bb96284044c14" - integrity sha512-f0WBD2T6p9r/a8v8MqkoWQq7TmbbAgPUg2zZtOp+kBrSCb3obHeNAsPDAUFzh+jEgug2qDVVkauBJa6ACe9AMg== +expo@~52.0.19: + version "52.0.19" + resolved "https://registry.yarnpkg.com/expo/-/expo-52.0.19.tgz#1b881c96ea595da0c5f3c13f578bc368c5261f4a" + integrity sha512-wOb/wbiQa0xqQRhgVBuOhLRus05TSw6fgThVMrPQgdLo24EPuT/ZAiRVcVRdjrEbwOqCDumgQCB7636B9J+jKg== dependencies: "@babel/runtime" "^7.20.0" - "@expo/cli" "0.22.3" + "@expo/cli" "0.22.6" "@expo/config" "~10.0.6" "@expo/config-plugins" "~9.0.12" - "@expo/fingerprint" "0.11.3" - "@expo/metro-config" "0.19.6" + "@expo/fingerprint" "0.11.4" + "@expo/metro-config" "0.19.8" "@expo/vector-icons" "^14.0.0" - babel-preset-expo "~12.0.3" + babel-preset-expo "~12.0.4" expo-asset "~11.0.1" expo-constants "~17.0.3" - expo-file-system "~18.0.4" + expo-file-system "~18.0.6" expo-font "~13.0.1" expo-keep-awake "~14.0.1" - expo-modules-autolinking "2.0.3" - expo-modules-core "2.1.1" + expo-modules-autolinking "2.0.4" + expo-modules-core "2.1.2" fbemitter "^3.0.0" web-streams-polyfill "^3.3.2" whatwg-url-without-unicode "8.0.0-3" From c339dc5b05282ba917bed9d02c914453cf38d437 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 16 Dec 2024 22:06:15 +0000 Subject: [PATCH 44/58] disable automaticallAdjustsScrollIndicatorInsets (#7131) --- src/view/com/util/List.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx index 31fd0aa1d0..62c91cec66 100644 --- a/src/view/com/util/List.tsx +++ b/src/view/com/util/List.tsx @@ -53,6 +53,7 @@ let List = React.forwardRef( headerOffset, style, progressViewOffset, + automaticallyAdjustsScrollIndicatorInsets = false, ...props }, ref, @@ -151,6 +152,9 @@ let List = React.forwardRef( return ( Date: Mon, 16 Dec 2024 22:26:40 +0000 Subject: [PATCH 45/58] Adjust gates (#7132) * Remove dead gate * Adjust gates * No need to disable exposures --- src/lib/constants.ts | 10 ---------- src/lib/statsig/gates.ts | 4 +--- src/screens/Settings/AppIconSettings/index.tsx | 8 ++++---- src/screens/Settings/AppearanceSettings.tsx | 9 ++++----- src/state/queries/post-feed.ts | 5 +---- src/view/com/util/post-ctrls/PostCtrls.tsx | 7 ++++--- 6 files changed, 14 insertions(+), 29 deletions(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 5d7b5eb456..279c6f913d 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -23,16 +23,6 @@ export const STARTER_PACK_MAX_SIZE = 150 // -prf export const JOINED_THIS_WEEK = 2880000 // estimate as of 11/26/24 -export const DISCOVER_DEBUG_DIDS: Record = { - 'did:plc:oisofpd7lj26yvgiivf3lxsi': true, // hailey.at - 'did:plc:fpruhuo22xkm5o7ttr2ktxdo': true, // danabra.mov - 'did:plc:p2cp5gopk7mgjegy6wadk3ep': true, // samuel.bsky.team - 'did:plc:ragtjsm2j2vknwkz3zp4oxrd': true, // pfrazee.com - 'did:plc:vpkhqolt662uhesyj6nxm7ys': true, // why.bsky.team - 'did:plc:3jpt2mvvsumj2r7eqk4gzzjz': true, // esb.lol - 'did:plc:vjug55kidv6sye7ykr5faxxn': true, // emilyliu.me -} - const BASE_FEEDBACK_FORM_URL = `${HELP_DESK_URL}/requests/new` export function FEEDBACK_FORM_URL({ email, diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index 3767ec1e5d..6876f18c53 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -1,5 +1,3 @@ export type Gate = // Keep this alphabetic please. - | 'debug_show_feedcontext' // DISABLED DUE TO EME - | 'post_feed_lang_window' // DISABLED DUE TO EME - | 'remove_show_latest_button' + 'debug_show_feedcontext' | 'debug_subscriptions' | 'remove_show_latest_button' diff --git a/src/screens/Settings/AppIconSettings/index.tsx b/src/screens/Settings/AppIconSettings/index.tsx index 0fefca29b2..0be2894d52 100644 --- a/src/screens/Settings/AppIconSettings/index.tsx +++ b/src/screens/Settings/AppIconSettings/index.tsx @@ -5,11 +5,11 @@ import {useLingui} from '@lingui/react' import * as DynamicAppIcon from '@mozzius/expo-dynamic-app-icon' import {NativeStackScreenProps} from '@react-navigation/native-stack' -import {DISCOVER_DEBUG_DIDS} from '#/lib/constants' +import {IS_INTERNAL} from '#/lib/app-info' import {PressableScale} from '#/lib/custom-animations/PressableScale' import {CommonNavigatorParams} from '#/lib/routes/types' +import {useGate} from '#/lib/statsig/statsig' import {isAndroid} from '#/platform/detection' -import {useSession} from '#/state/session' import {AppIconImage} from '#/screens/Settings/AppIconSettings/AppIconImage' import {AppIconSet} from '#/screens/Settings/AppIconSettings/types' import {useAppIconSets} from '#/screens/Settings/AppIconSettings/useAppIconSets' @@ -23,7 +23,7 @@ export function AppIconSettingsScreen({}: Props) { const t = useTheme() const {_} = useLingui() const sets = useAppIconSets() - const {currentAccount} = useSession() + const gate = useGate() const [currentAppIcon, setCurrentAppIcon] = useState(() => getAppIconName(DynamicAppIcon.getAppIcon()), ) @@ -86,7 +86,7 @@ export function AppIconSettingsScreen({}: Props) { ))} - {DISCOVER_DEBUG_DIDS[currentAccount?.did ?? ''] && ( + {IS_INTERNAL && gate('debug_subscriptions') && ( <> export function AppearanceSettingsScreen({}: Props) { const {_} = useLingui() const {fonts} = useAlf() + const gate = useGate() const {colorMode, darkTheme} = useThemePrefs() const {setColorMode, setDarkTheme} = useSetThemePrefs() @@ -74,8 +75,6 @@ export function AppearanceSettingsScreen({}: Props) { [fonts], ) - const {currentAccount} = useSession() - return ( @@ -178,7 +177,7 @@ export function AppearanceSettingsScreen({}: Props) { onChange={onChangeFontScale} /> - {isNative && DISCOVER_DEBUG_DIDS[currentAccount?.did ?? ''] && ( + {isNative && IS_INTERNAL && gate('debug_subscriptions') && ( <> diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index 696e28f9c2..2eb604627e 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -144,11 +144,8 @@ export function usePostFeedQuery( /** * The number of posts to fetch in a single request. Because we filter * unwanted content, we may over-fetch here to try and fill pages by - * `MIN_POSTS`. + * `MIN_POSTS`. But if you're doing this, ask @why if it's ok first. */ - - // TEMPORARILY DISABLING GATE TO PREVENT EVENT CONSUMPTION @TODO EME-GATE - // const fetchLimit = gate('post_feed_lang_window') ? 100 : MIN_POSTS const fetchLimit = MIN_POSTS // Make sure this doesn't invalidate unless really needed. diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index 39caaf0987..607a480ff2 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -18,12 +18,13 @@ import {msg, plural} from '@lingui/macro' import {useLingui} from '@lingui/react' import {IS_INTERNAL} from '#/lib/app-info' -import {DISCOVER_DEBUG_DIDS, POST_CTRL_HITSLOP} from '#/lib/constants' +import {POST_CTRL_HITSLOP} from '#/lib/constants' import {CountWheel} from '#/lib/custom-animations/CountWheel' import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon' import {useHaptics} from '#/lib/haptics' import {makeProfileLink} from '#/lib/routes/links' import {shareUrl} from '#/lib/sharing' +import {useGate} from '#/lib/statsig/statsig' import {toShareUrl} from '#/lib/strings/url-helpers' import {Shadow} from '#/state/cache/types' import {useFeedFeedbackContext} from '#/state/feed-feedback' @@ -85,8 +86,8 @@ let PostCtrls = ({ const {sendInteraction} = useFeedFeedbackContext() const {captureAction} = useProgressGuideControls() const playHaptic = useHaptics() - const isDiscoverDebugUser = - IS_INTERNAL || DISCOVER_DEBUG_DIDS[currentAccount?.did ?? ''] + const gate = useGate() + const isDiscoverDebugUser = IS_INTERNAL || gate('debug_show_feedcontext') const isBlocked = Boolean( post.author.viewer?.blocking || post.author.viewer?.blockedBy || From 1a160df3962dab21fd9b4514e266652da1a14a4f Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 17 Dec 2024 00:02:56 +0000 Subject: [PATCH 46/58] add safe area view to profile error screen (#7134) --- src/view/screens/Profile.tsx | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index ebf1d955d8..24e8719e17 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -1,5 +1,6 @@ import React, {useCallback, useMemo} from 'react' import {StyleSheet} from 'react-native' +import {SafeAreaView} from 'react-native-safe-area-context' import { AppBskyActorDefs, AppBskyGraphGetActorStarterPacks, @@ -122,13 +123,15 @@ function ProfileScreenInner({route}: Props) { } if (resolveError || profileError) { return ( - + + + ) } if (profile && moderationOpts) { @@ -144,13 +147,15 @@ function ProfileScreenInner({route}: Props) { } // should never happen return ( - + + + ) } From be33c626b03bafd3c4543a3962e87f9422d7d05d Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 17 Dec 2024 00:11:03 +0000 Subject: [PATCH 47/58] alf error screen (#7135) --- src/view/com/util/error/ErrorScreen.tsx | 137 ++++++++++-------------- 1 file changed, 55 insertions(+), 82 deletions(-) diff --git a/src/view/com/util/error/ErrorScreen.tsx b/src/view/com/util/error/ErrorScreen.tsx index 846f4d2951..e9b0e50a3c 100644 --- a/src/view/com/util/error/ErrorScreen.tsx +++ b/src/view/com/util/error/ErrorScreen.tsx @@ -1,4 +1,4 @@ -import {StyleSheet, View} from 'react-native' +import {View} from 'react-native' import { FontAwesomeIcon, FontAwesomeIconStyle, @@ -7,12 +7,11 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {useTheme} from '#/lib/ThemeContext' -import {ViewHeader} from '#/view/com/util/ViewHeader' -import {Button} from '../forms/Button' -import {Text} from '../text/Text' -import {CenteredView} from '../Views' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as ArrowRotateCounterClockwiseIcon} from '#/components/icons/ArrowRotateCounterClockwise' +import * as Layout from '#/components/Layout' +import {Text} from '#/components/Typography' export function ErrorScreen({ title, @@ -29,22 +28,32 @@ export function ErrorScreen({ testID?: string showHeader?: boolean }) { - const theme = useTheme() - const {isMobile} = useWebMediaQueries() + const t = useTheme() const pal = usePalette('default') const {_} = useLingui() return ( - <> - {showHeader && isMobile && ( - + + {showHeader && ( + + + + + Error + + + + )} - - + + - + {title} - {message} + {message} {details && ( - - {details} - + + + {details} + + )} {onPressTryAgain && ( - + )} - - + + ) } - -const styles = StyleSheet.create({ - outer: { - flex: 1, - paddingVertical: 30, - paddingHorizontal: 14, - }, - title: { - textAlign: 'center', - marginBottom: 10, - }, - message: { - textAlign: 'center', - marginBottom: 20, - }, - details: { - textAlign: 'center', - paddingVertical: 10, - paddingHorizontal: 14, - overflow: 'hidden', - marginBottom: 20, - }, - btnContainer: { - alignItems: 'center', - }, - btn: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: 16, - paddingVertical: 10, - }, - btnText: { - marginLeft: 5, - }, - errorIconContainer: { - alignItems: 'center', - marginBottom: 10, - }, - errorIcon: { - borderRadius: 25, - width: 50, - height: 50, - alignItems: 'center', - justifyContent: 'center', - }, -}) From c9e429760d426df7549bfcd6292a0dc5e7fe7d7e Mon Sep 17 00:00:00 2001 From: dan Date: Tue, 17 Dec 2024 00:11:21 +0000 Subject: [PATCH 48/58] [ELI5] Tweak wording on the signup screen (#7136) * Tweak signup wordings * Update translations --- src/locale/locales/an/messages.po | 6 +++--- src/locale/locales/ast/messages.po | 6 +++--- src/locale/locales/ca/messages.po | 6 +++--- src/locale/locales/de/messages.po | 6 +++--- src/locale/locales/en-GB/messages.po | 6 +++--- src/locale/locales/en/messages.po | 6 +++--- src/locale/locales/es/messages.po | 6 +++--- src/locale/locales/fi/messages.po | 6 +++--- src/locale/locales/fr/messages.po | 6 +++--- src/locale/locales/ga/messages.po | 6 +++--- src/locale/locales/gl/messages.po | 6 +++--- src/locale/locales/hi/messages.po | 6 +++--- src/locale/locales/hu/messages.po | 6 +++--- src/locale/locales/id/messages.po | 6 +++--- src/locale/locales/it/messages.po | 6 +++--- src/locale/locales/ja/messages.po | 6 +++--- src/locale/locales/ko/messages.po | 6 +++--- src/locale/locales/nl/messages.po | 6 +++--- src/locale/locales/pl/messages.po | 6 +++--- src/locale/locales/pt-BR/messages.po | 6 +++--- src/locale/locales/ru/messages.po | 6 +++--- src/locale/locales/th/messages.po | 6 +++--- src/locale/locales/tr/messages.po | 6 +++--- src/locale/locales/uk/messages.po | 6 +++--- src/locale/locales/vi/messages.po | 6 +++--- src/locale/locales/zh-CN/messages.po | 6 +++--- src/locale/locales/zh-HK/messages.po | 6 +++--- src/locale/locales/zh-TW/messages.po | 6 +++--- src/screens/Signup/StepHandle.tsx | 4 ++-- src/screens/Signup/index.tsx | 2 +- 30 files changed, 87 insertions(+), 87 deletions(-) diff --git a/src/locale/locales/an/messages.po b/src/locale/locales/an/messages.po index ab8872e2dd..86e6a20f1e 100644 --- a/src/locale/locales/an/messages.po +++ b/src/locale/locales/an/messages.po @@ -3613,7 +3613,7 @@ msgstr "Escribe la tuya clau" #~ msgstr "Escribe lo tuyo furnidor d'aloch preferiu" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "Escribe lo tuyo identificador" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -8877,7 +8877,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "La tuya canal de Seguindo ye vueda! Sigue a mas usuarios pa veyer las suyas publicacions aquí." #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "Lo tuyo identificador completo será" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -8921,5 +8921,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "Lo tuyo reporte s'ha ninviau a lo servicio de moderación de Bluesky" #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "Lo tuyo identificador" diff --git a/src/locale/locales/ast/messages.po b/src/locale/locales/ast/messages.po index 73634d0b36..406c8ab8a4 100644 --- a/src/locale/locales/ast/messages.po +++ b/src/locale/locales/ast/messages.po @@ -3509,7 +3509,7 @@ msgstr "" #~ msgstr "" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -8543,7 +8543,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "¡El feed siguiente ta baleru! Sigui a más usuarios pa ver qué pasó." #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "L'identificador completu va ser" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -8587,5 +8587,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "L'informe unvióse al serviciu de moderación de Bluesky" #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "" diff --git a/src/locale/locales/ca/messages.po b/src/locale/locales/ca/messages.po index f4dee376f7..fcaf1fc2eb 100644 --- a/src/locale/locales/ca/messages.po +++ b/src/locale/locales/ca/messages.po @@ -4268,7 +4268,7 @@ msgstr "Introdueix la teva contrasenya" #~ msgstr "Introdueix el teu proveïdor d'allotjament preferit" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "Introdueix el teu identificador d'usuari" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -10404,7 +10404,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "El teu canal de seguint està buit! Segueix a més usuaris per a saber què està passant." #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "El teu identificador complet serà" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -10458,5 +10458,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "El teu informe s'enviarà al servei de moderació de Bluesky" #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "El teu identificador d'usuari" diff --git a/src/locale/locales/de/messages.po b/src/locale/locales/de/messages.po index 1101991e0c..afe5d2f354 100644 --- a/src/locale/locales/de/messages.po +++ b/src/locale/locales/de/messages.po @@ -3742,7 +3742,7 @@ msgstr "Gib dein Passwort ein" #~ msgstr "" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "Gib deinen Handle ein" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -9663,7 +9663,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "Dein Following-Feed ist leer! Folge mehr Benutzern, um auf dem Laufenden zu bleiben." #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "Dein vollständiger Handle lautet" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -9707,5 +9707,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "" #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "Dein Benutzerhandle" diff --git a/src/locale/locales/en-GB/messages.po b/src/locale/locales/en-GB/messages.po index 5e7badcc26..a61e096e8b 100644 --- a/src/locale/locales/en-GB/messages.po +++ b/src/locale/locales/en-GB/messages.po @@ -3509,7 +3509,7 @@ msgstr "" #~ msgstr "" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -8543,7 +8543,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "" #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -8587,5 +8587,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "" #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "" diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 97ee908555..05d57b0048 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -3509,7 +3509,7 @@ msgstr "" #~ msgstr "" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -8543,7 +8543,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "" #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -8587,5 +8587,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "" #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "" diff --git a/src/locale/locales/es/messages.po b/src/locale/locales/es/messages.po index ef1159f7eb..c7380350b3 100644 --- a/src/locale/locales/es/messages.po +++ b/src/locale/locales/es/messages.po @@ -3896,7 +3896,7 @@ msgstr "Introduce tu contraseña" #~ msgstr "Introduce tu proveedor de alojamiento preferido" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "Introduce tu nombre de usuario" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -9518,7 +9518,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "¡Tu feed de Siguiendo esta vacío! Sigue a más usuarios para ver sus posts aquí." #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "Tu nombre de usuario completo será" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -9562,5 +9562,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "Tu reporte ha sido enviado al servicio de moderación de Bluesky" #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "Tu nombre de usuario" diff --git a/src/locale/locales/fi/messages.po b/src/locale/locales/fi/messages.po index 679b3f153c..da2375ca30 100644 --- a/src/locale/locales/fi/messages.po +++ b/src/locale/locales/fi/messages.po @@ -3970,7 +3970,7 @@ msgstr "Syötä salasanasi" #~ msgstr "Syötä haluamasi palveluntarjoaja" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "Syötä käyttäjätunnuksesi" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -9627,7 +9627,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "Seuraamiesi syöte on tyhjä! Seuraa lisää käyttäjiä nähdäksesi, mitä tapahtuu." #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "Käyttäjätunnuksesi tulee olemaan" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -9671,5 +9671,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "" #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "Käyttäjätunnuksesi" diff --git a/src/locale/locales/fr/messages.po b/src/locale/locales/fr/messages.po index 64f3ac7cc9..c0973e4953 100644 --- a/src/locale/locales/fr/messages.po +++ b/src/locale/locales/fr/messages.po @@ -3501,7 +3501,7 @@ msgstr "Entrez votre mot de passe" #~ msgstr "Entrez votre hébergeur préféré" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "Entrez votre pseudo" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -8519,7 +8519,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "Votre fil d’actu des comptes suivis est vide ! Suivez plus de comptes pour voir ce qui se passe." #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "Votre nom complet sera" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -8563,5 +8563,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "Votre rapport sera envoyé au Service de Modération de Bluesky" #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "Votre pseudo" diff --git a/src/locale/locales/ga/messages.po b/src/locale/locales/ga/messages.po index 2e77bfda24..ff34bca6e2 100644 --- a/src/locale/locales/ga/messages.po +++ b/src/locale/locales/ga/messages.po @@ -3508,7 +3508,7 @@ msgstr "Cuir isteach do phasfhocal" #~ msgstr "Cuir isteach an soláthraí óstála is fearr leat" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "Cuir isteach do leasainm" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -8542,7 +8542,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "Tá an fotha de na daoine a leanann tú folamh! Lean tuilleadh úsáideoirí le feiceáil céard atá ar siúl." #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "Do leasainm iomlán anseo:" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -8586,5 +8586,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "Seolfar do thuairisc go dtí Seirbhís Modhnóireachta Bluesky" #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "Do leasainm" diff --git a/src/locale/locales/gl/messages.po b/src/locale/locales/gl/messages.po index 87730db003..ddb9961ea9 100644 --- a/src/locale/locales/gl/messages.po +++ b/src/locale/locales/gl/messages.po @@ -3896,7 +3896,7 @@ msgstr "Introduce o teu contrasinal" #~ msgstr "Introduce o teu proveedor de aloxamento preferido" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "Introduce o teu alcume" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -9523,7 +9523,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "A seguinte canle está baleira! Sigue a máis persoas para ver o que está a acontecer." #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "O teu alcume completo será" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -9567,5 +9567,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "O teu informe enviarase ao Servizo de moderación de Bluesky" #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "O teu alcume" diff --git a/src/locale/locales/hi/messages.po b/src/locale/locales/hi/messages.po index 358afb5b5a..2396970553 100644 --- a/src/locale/locales/hi/messages.po +++ b/src/locale/locales/hi/messages.po @@ -3780,7 +3780,7 @@ msgstr "अपना पासवर्ड दर्ज करें" #~ msgstr "अपना पसंदीदा होस्टिंग प्रदाता दर्ज करें" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "अपना उपयोगकर्ता हैंडल दर्ज करें" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -9026,7 +9026,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "आपका फ़ॉलोइंग फ़ीड खाली है! क्या चल रहा है जानने के लिए और उपयोगकर्ताओं को फ़ॉलो करें।" #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "आपका पूरा हैंडल होगा" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -9070,5 +9070,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "आपके शिकायत को Bluesky मॉडरेशन सेवा को भेजा जाएगा।" #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "आपका उपयोगकर्ता हैंडल" diff --git a/src/locale/locales/hu/messages.po b/src/locale/locales/hu/messages.po index 45fcf69bb3..21f2c0ad56 100644 --- a/src/locale/locales/hu/messages.po +++ b/src/locale/locales/hu/messages.po @@ -3465,7 +3465,7 @@ msgstr "Jelszó megadása" #~ msgstr "Kívánt tárhelyszolgáltató megadása" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "Felhasználónév megadása" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -8439,7 +8439,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "A Követett hírfolyamod üres. Kövess több felhasználót, hogy lásd, mi történik!" #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "A teljes felhasználóneved:" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -8482,5 +8482,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "Ez a jelentés a Bluesky moderálási szolgáltatásának lesz elküldve" #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "Saját felhasználónév" diff --git a/src/locale/locales/id/messages.po b/src/locale/locales/id/messages.po index a064bb3a3f..69beaff7c1 100644 --- a/src/locale/locales/id/messages.po +++ b/src/locale/locales/id/messages.po @@ -4007,7 +4007,7 @@ msgstr "Masukkan kata sandi Anda" #~ msgstr "Masukkan penyedia hosting pilihan Anda" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "Masukkan panggilan Anda" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -9678,7 +9678,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "Feed mengikuti Anda kosong! Ikuti lebih banyak pengguna untuk melihat apa yang terjadi." #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "Panggilan lengkap Anda akan menjadi" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -9722,5 +9722,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "Laporan Anda akan dikirim ke Layanan Moderasi Bluesky" #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "Panggilan Anda" diff --git a/src/locale/locales/it/messages.po b/src/locale/locales/it/messages.po index a8c3122a13..ef715f3298 100644 --- a/src/locale/locales/it/messages.po +++ b/src/locale/locales/it/messages.po @@ -3318,7 +3318,7 @@ msgid "Input your password" msgstr "Inserisci la tua password" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "Inserisci il tuo nome utente" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -8016,7 +8016,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "Il tuo feed seguente è vuoto! Segui più utenti per vedere cosa sta succedendo." #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "Il tuo nome utente completo sarà" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -8056,5 +8056,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "La tua segnalazione verrà inviata al Servizio Moderazione di Bluesky" #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "Il tuo nome utente" diff --git a/src/locale/locales/ja/messages.po b/src/locale/locales/ja/messages.po index 1652fe5742..c8b9721d6c 100644 --- a/src/locale/locales/ja/messages.po +++ b/src/locale/locales/ja/messages.po @@ -3317,7 +3317,7 @@ msgid "Input your password" msgstr "あなたのパスワードを入力" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "あなたのユーザーハンドルを入力" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -8047,7 +8047,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "Followingフィードは空です!もっと多くのユーザーをフォローして、近況を確認しましょう。" #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "フルハンドルは" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -8087,5 +8087,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "あなたの報告はBluesky Moderation Serviceに送られます" #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "あなたのユーザーハンドル" diff --git a/src/locale/locales/ko/messages.po b/src/locale/locales/ko/messages.po index ff932b4297..fe0b852b18 100644 --- a/src/locale/locales/ko/messages.po +++ b/src/locale/locales/ko/messages.po @@ -3317,7 +3317,7 @@ msgid "Input your password" msgstr "비밀번호를 입력합니다" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "사용자 핸들을 입력합니다" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -8019,7 +8019,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "팔로우 중 피드가 비어 있습니다. 더 많은 사용자를 팔로우하여 무슨 일이 일어나고 있는지 확인하세요." #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "내 전체 핸들:" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -8059,5 +8059,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "신고가 Bluesky Moderation Service로 보내집니다." #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "내 사용자 핸들" diff --git a/src/locale/locales/nl/messages.po b/src/locale/locales/nl/messages.po index b33050a355..217db6a6a8 100644 --- a/src/locale/locales/nl/messages.po +++ b/src/locale/locales/nl/messages.po @@ -3496,7 +3496,7 @@ msgstr "Vul je wachtwoord in" #~ msgstr "Vul je voorkeurshostingprovider in" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "Vul je gebruikershandle in." #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -8506,7 +8506,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "Je volgende feed is leeg! Volg meer gebruikers om te zien wat er gebeurt." #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "Je volledige handle wordt" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -8550,5 +8550,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "Je melding wordt verzonden naar de Bluesky-moderatieservice" #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "Je gebruikershandle" diff --git a/src/locale/locales/pl/messages.po b/src/locale/locales/pl/messages.po index 06886f883d..d8c9c0098b 100644 --- a/src/locale/locales/pl/messages.po +++ b/src/locale/locales/pl/messages.po @@ -3317,7 +3317,7 @@ msgid "Input your password" msgstr "Wpisz hasło" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "Wpisz nazwę użytkownika" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -8031,7 +8031,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "Twój kanał osób obserwowanych jest pusty! Zaobserwuj trochę więcej osób." #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "Twoją pełna nazwa będzie" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -8071,5 +8071,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "Zgłoszenie będzie wysłane do usługi moderacji Bluesky" #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "Twoja nazwa użytkownika" diff --git a/src/locale/locales/pt-BR/messages.po b/src/locale/locales/pt-BR/messages.po index e5d8cf4600..9297c95741 100644 --- a/src/locale/locales/pt-BR/messages.po +++ b/src/locale/locales/pt-BR/messages.po @@ -4003,7 +4003,7 @@ msgstr "Insira sua senha" #~ msgstr "Insira seu provedor de hospedagem" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "Insira o usuário" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -9667,7 +9667,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "Seu feed inicial está vazio! Siga mais usuários para acompanhar o que está acontecendo." #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "Seu identificador completo será" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -9711,5 +9711,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "Sua denúncia será enviada para o serviço de moderação do Bluesky" #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "Seu identificador de usuário" diff --git a/src/locale/locales/ru/messages.po b/src/locale/locales/ru/messages.po index 6a6755c6a6..4ec66227bb 100644 --- a/src/locale/locales/ru/messages.po +++ b/src/locale/locales/ru/messages.po @@ -3578,7 +3578,7 @@ msgstr "Введите ваш пароль" #~ msgstr "Введите желаемого хостинг-провайдера" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "Введите ваш псевдоним" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -8768,7 +8768,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "Ваша домашняя лента пуста! Подпишитесь на больше пользователей чтобы получать больше постов." #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "Ваш полный псевдоним будет" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -8812,5 +8812,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "Ваш отчет будет отправлен в Службу Модерации Bluesky." #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "Ваш псевдоним" diff --git a/src/locale/locales/th/messages.po b/src/locale/locales/th/messages.po index 61014c89cf..7eb3cd8a8a 100644 --- a/src/locale/locales/th/messages.po +++ b/src/locale/locales/th/messages.po @@ -3994,7 +3994,7 @@ msgstr "ป้อนรหัสผ่านของคุณ" #~ msgstr "กรอกผู้ให้บริการโฮสติ้งที่คุณต้องการ" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "กรอกชื่อผู้ใช้ของคุณ" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -9665,7 +9665,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "ฟีดผู้ติดตามของคุณว่างเปล่า! ติดตามผู้ใช้เพิ่มเติมเพื่อดูความเคลื่อนไหว" #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "ชื่อผู้ใช้เต็มของคุณจะเป็น" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -9709,5 +9709,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "รายงานของคุณจะถูกส่งไปยัง Bluesky Moderation Service" #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "แฮนด์เดิลผู้ใช้ของคุณ" diff --git a/src/locale/locales/tr/messages.po b/src/locale/locales/tr/messages.po index d8fc0c23f3..f990961f74 100644 --- a/src/locale/locales/tr/messages.po +++ b/src/locale/locales/tr/messages.po @@ -4211,7 +4211,7 @@ msgstr "Şifrenizi girin" #~ msgstr "" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "Kullanıcı adınızı girin" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -10212,7 +10212,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "Takip ettiğiniz besleme boş! Neler olduğunu görmek için daha fazla kullanıcı takip edin." #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "Tam kullanıcı adınız" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -10261,5 +10261,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "" #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "Kullanıcı adınız" diff --git a/src/locale/locales/uk/messages.po b/src/locale/locales/uk/messages.po index 97685ea93b..a6141a7f90 100644 --- a/src/locale/locales/uk/messages.po +++ b/src/locale/locales/uk/messages.po @@ -4007,7 +4007,7 @@ msgstr "Введіть ваш пароль" #~ msgstr "Введіть бажаного хостинг-провайдера" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "Введіть ваш псевдонім" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -9678,7 +9678,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "Ваша домашня стрічка порожня! Підпишіться на більше користувачів щоб отримувати більше постів." #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "Ваш повний псевдонім буде" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -9722,5 +9722,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "" #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "Ваш псевдонім" diff --git a/src/locale/locales/vi/messages.po b/src/locale/locales/vi/messages.po index ce99522bcb..dbb00bf5e7 100644 --- a/src/locale/locales/vi/messages.po +++ b/src/locale/locales/vi/messages.po @@ -3509,7 +3509,7 @@ msgstr "Nhập mật khẩu của bạn" #~ msgstr "Nhập nhà cung cấp lưu trữ theo lựa chọn của bạn" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "Nhập tên người dùng của bạn" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -8543,7 +8543,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "Bảng tin theo dõi còn trống! Hãy theo dõi thêm người dùng để biết có gì đang diễn ra." #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "Tên người dùng đầy đủ của bạn sẽ là" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -8587,5 +8587,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "Báo cáo của bạn sẽ được gởi đến dịch vụ kiểm duyệt của Bluesky" #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "Tên người dùng của bạn" diff --git a/src/locale/locales/zh-CN/messages.po b/src/locale/locales/zh-CN/messages.po index f3d392c398..dfdd5102aa 100644 --- a/src/locale/locales/zh-CN/messages.po +++ b/src/locale/locales/zh-CN/messages.po @@ -3314,7 +3314,7 @@ msgid "Input your password" msgstr "输入你的密码" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "输入你的账户代码" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -8004,7 +8004,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "你的“Following”动态源是空的!关注更多用户去看看他们发了什么。" #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "你的完整账户代码将修改为" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -8044,5 +8044,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "你的举报将发送至 Bluesky 内容审核服务" #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "你的账户代码" diff --git a/src/locale/locales/zh-HK/messages.po b/src/locale/locales/zh-HK/messages.po index a5fccf312d..368d4f3115 100644 --- a/src/locale/locales/zh-HK/messages.po +++ b/src/locale/locales/zh-HK/messages.po @@ -3314,7 +3314,7 @@ msgid "Input your password" msgstr "輸入你嘅密碼" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "輸入你嘅帳號頭銜" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -8004,7 +8004,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "你嘅「Following」動態源得個吉!跟多啲用戶睇吓發生緊啲咩事。" #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "你嘅完整帳號頭銜會係" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -8044,5 +8044,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "你嘅上報會傳送去 Bluesky 審核服務" #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "你嘅帳號頭銜" diff --git a/src/locale/locales/zh-TW/messages.po b/src/locale/locales/zh-TW/messages.po index 64f56c054c..00790512f7 100644 --- a/src/locale/locales/zh-TW/messages.po +++ b/src/locale/locales/zh-TW/messages.po @@ -3314,7 +3314,7 @@ msgid "Input your password" msgstr "輸入您的密碼" #: src/screens/Signup/StepHandle.tsx:114 -msgid "Input your user handle" +msgid "Type your desired username" msgstr "輸入您的帳號代碼" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 @@ -8004,7 +8004,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "您的「Following」動態源是空的!跟隨更多用戶來看看發生了什麼事情。" #: src/screens/Signup/StepHandle.tsx:125 -msgid "Your full handle will be" +msgid "Your full username will be" msgstr "您的完整帳號代碼將修改為" #: src/screens/Settings/components/ChangeHandleDialog.tsx:219 @@ -8044,5 +8044,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "您的檢舉將傳送至 Bluesky 內容管理服務" #: src/screens/Signup/index.tsx:142 -msgid "Your user handle" +msgid "Choose your username" msgstr "您的帳號代碼" diff --git a/src/screens/Signup/StepHandle.tsx b/src/screens/Signup/StepHandle.tsx index 0ff0506f4e..dee7df8488 100644 --- a/src/screens/Signup/StepHandle.tsx +++ b/src/screens/Signup/StepHandle.tsx @@ -111,7 +111,7 @@ export function StepHandle() { handleValueRef.current = val setDraftValue(val) }} - label={_(msg`Input your user handle`)} + label={_(msg`Type your desired username`)} defaultValue={draftValue} autoCapitalize="none" autoCorrect={false} @@ -122,7 +122,7 @@ export function StepHandle() { {draftValue !== '' && ( - Your full handle will be{' '} + Your full username will be{' '} @{createFullHandle(draftValue, state.userDomain)} diff --git a/src/screens/Signup/index.tsx b/src/screens/Signup/index.tsx index 1857981a0f..5f406eb7ae 100644 --- a/src/screens/Signup/index.tsx +++ b/src/screens/Signup/index.tsx @@ -139,7 +139,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) { {state.activeStep === SignupStep.INFO ? ( Your account ) : state.activeStep === SignupStep.HANDLE ? ( - Your user handle + Choose your username ) : ( Complete the challenge )} From 20d4266e2f8ef82b6ed1244fd929dd07c01486dc Mon Sep 17 00:00:00 2001 From: dan Date: Tue, 17 Dec 2024 03:13:51 +0000 Subject: [PATCH 49/58] Read storage on window.onstorage (#7137) --- src/state/persisted/index.web.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/state/persisted/index.web.ts b/src/state/persisted/index.web.ts index 4cfc87cdb1..f28b197715 100644 --- a/src/state/persisted/index.web.ts +++ b/src/state/persisted/index.web.ts @@ -24,6 +24,7 @@ const _emitter = new EventEmitter() export async function init() { broadcast.onmessage = onBroadcastMessage + window.onstorage = onStorage const stored = readFromStorage() if (stored) { _state = stored @@ -90,6 +91,17 @@ export async function clearStorage() { } clearStorage satisfies PersistedApi['clearStorage'] +function onStorage() { + const next = readFromStorage() + if (next === _state) { + return + } + if (next) { + _state = next + _emitter.emit('update') + } +} + async function onBroadcastMessage({data}: MessageEvent) { if ( typeof data === 'object' && From 31d2aa6716007871d50892be8b866085309ae92c Mon Sep 17 00:00:00 2001 From: dan Date: Tue, 17 Dec 2024 11:09:06 +0000 Subject: [PATCH 50/58] Refetch empty feed on focus (#7139) --- src/view/com/posts/PostFeed.tsx | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 6746703379..55d7ba053f 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -187,12 +187,16 @@ let PostFeed = ({ } try { if (await pollLatest(data.pages[0])) { - onHasNew(true) + if (isEmpty) { + refetch() + } else { + onHasNew(true) + } } } catch (e) { logger.error('Poll latest failed', {feed, message: String(e)}) } - }, [feed, data, isFetching, onHasNew, enabled, disablePoll]) + }, [feed, data, isFetching, isEmpty, onHasNew, enabled, disablePoll, refetch]) const myDid = currentAccount?.did || '' const onPostCreated = React.useCallback(() => { @@ -220,20 +224,15 @@ let PostFeed = ({ React.useEffect(() => { if (enabled && !disablePoll) { const timeSinceFirstLoad = Date.now() - lastFetchRef.current - // DISABLED need to check if this is causing random feed refreshes -prf - /*if (timeSinceFirstLoad > REFRESH_AFTER) { - // do a full refresh - scrollElRef?.current?.scrollToOffset({offset: 0, animated: false}) - queryClient.resetQueries({queryKey: RQKEY(feed)}) - } else*/ if ( - timeSinceFirstLoad > CHECK_LATEST_AFTER && + if ( + (isEmpty || timeSinceFirstLoad > CHECK_LATEST_AFTER) && checkForNewRef.current ) { // check for new on enable (aka on focus) checkForNewRef.current() } } - }, [enabled, disablePoll, feed, queryClient, scrollElRef]) + }, [enabled, disablePoll, feed, queryClient, scrollElRef, isEmpty]) React.useEffect(() => { let cleanup1: () => void | undefined, cleanup2: () => void | undefined const subscription = AppState.addEventListener('change', nextAppState => { From 07b7250682c2841e9428c509707d08c17a4317df Mon Sep 17 00:00:00 2001 From: dan Date: Tue, 17 Dec 2024 11:22:49 +0000 Subject: [PATCH 51/58] Fix notifications borders (#7140) --- src/view/com/notifications/NotificationFeed.tsx | 2 +- src/view/com/pager/TabBar.web.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/view/com/notifications/NotificationFeed.tsx b/src/view/com/notifications/NotificationFeed.tsx index 0b814e68dc..5fa40b30b5 100644 --- a/src/view/com/notifications/NotificationFeed.tsx +++ b/src/view/com/notifications/NotificationFeed.tsx @@ -134,7 +134,7 @@ export function NotificationFeed({ highlightUnread={filter === 'all'} item={item} moderationOpts={moderationOpts!} - hideTopBorder={index === 0 && item.notification.isRead} + hideTopBorder={index === 0} /> ) }, diff --git a/src/view/com/pager/TabBar.web.tsx b/src/view/com/pager/TabBar.web.tsx index cabd955f02..d44b7b60c4 100644 --- a/src/view/com/pager/TabBar.web.tsx +++ b/src/view/com/pager/TabBar.web.tsx @@ -145,7 +145,7 @@ export function TabBar({ const desktopStyles = StyleSheet.create({ outer: { flexDirection: 'row', - width: 598, + width: 600, }, contentContainer: { flexGrow: 1, From 32611391a35cbfe3f4a57882c117d52de022fb89 Mon Sep 17 00:00:00 2001 From: dan Date: Tue, 17 Dec 2024 12:22:09 +0000 Subject: [PATCH 52/58] Pipe statsig events to logger (#7141) * Pipe statsig events to logger * Log rich objects to bitdrift * Fix tests * Consolidate mocks, fix tests * Reduce log trash on native --- jest/jestSetup.js | 16 +++++++++++++ src/lib/bitdrift.ts | 1 + src/lib/bitdrift.web.ts | 4 ++++ src/lib/statsig/statsig.tsx | 26 ++++++++++++++++++--- src/logger/bitdriftTransport.ts | 5 ++-- src/logger/bitdriftTransport.web.ts | 7 ------ src/state/session/__tests__/session-test.ts | 13 ----------- 7 files changed, 46 insertions(+), 26 deletions(-) create mode 100644 src/lib/bitdrift.web.ts delete mode 100644 src/logger/bitdriftTransport.web.ts diff --git a/jest/jestSetup.js b/jest/jestSetup.js index 0ed7201180..c3160df3bc 100644 --- a/jest/jestSetup.js +++ b/jest/jestSetup.js @@ -105,3 +105,19 @@ jest.mock('expo-modules-core', () => ({ return () => null }), })) + +jest.mock('expo-localization', () => ({ + getLocales: () => [], +})) + +jest.mock('statsig-react-native-expo', () => ({ + Statsig: { + initialize() {}, + initializeCalled() { + return false + }, + }, +})) + +jest.mock('../src/lib/bitdrift', () => ({})) +jest.mock('../src/lib/statsig/statsig', () => ({})) diff --git a/src/lib/bitdrift.ts b/src/lib/bitdrift.ts index 02d074e76c..3f892f6b8c 100644 --- a/src/lib/bitdrift.ts +++ b/src/lib/bitdrift.ts @@ -1,5 +1,6 @@ import {init} from '@bitdrift/react-native' import {Statsig} from 'statsig-react-native-expo' +export {debug, error, info, warn} from '@bitdrift/react-native' import {initPromise} from './statsig/statsig' diff --git a/src/lib/bitdrift.web.ts b/src/lib/bitdrift.web.ts new file mode 100644 index 0000000000..5db69450fa --- /dev/null +++ b/src/lib/bitdrift.web.ts @@ -0,0 +1,4 @@ +export function debug() {} +export function error() {} +export function info() {} +export function warn() {} diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx index 21fa4bb57c..e0882806d5 100644 --- a/src/lib/statsig/statsig.tsx +++ b/src/lib/statsig/statsig.tsx @@ -5,6 +5,7 @@ import {sha256} from 'js-sha256' import {Statsig, StatsigProvider} from 'statsig-react-native-expo' import {BUNDLE_DATE, BUNDLE_IDENTIFIER, IS_TESTFLIGHT} from '#/lib/app-info' +import * as bitdrift from '#/lib/bitdrift' import {logger} from '#/logger' import {isWeb} from '#/platform/detection' import * as persisted from '#/state/persisted' @@ -97,19 +98,38 @@ export function logEvent( rawMetadata: LogEvents[E] & FlatJSONRecord, ) { try { - const fullMetadata = { - ...rawMetadata, - } as Record // Statsig typings are unnecessarily strict here. + const fullMetadata = toStringRecord(rawMetadata) fullMetadata.routeName = getCurrentRouteName() ?? '(Uninitialized)' if (Statsig.initializeCalled()) { Statsig.logEvent(eventName, null, fullMetadata) } + // Intentionally bypass the logger abstraction to log rich objects. + console.groupCollapsed(eventName) + console.log(fullMetadata) + console.groupEnd() + bitdrift.info(eventName, fullMetadata) } catch (e) { // A log should never interrupt the calling code, whatever happens. logger.error('Failed to log an event', {message: e}) } } +function toStringRecord( + metadata: LogEvents[E] & FlatJSONRecord, +): Record { + const record: Record = {} + for (let key in metadata) { + if (metadata.hasOwnProperty(key)) { + if (typeof metadata[key] === 'string') { + record[key] = metadata[key] + } else { + record[key] = JSON.stringify(metadata[key]) + } + } + } + return record +} + // We roll our own cache in front of Statsig because it is a singleton // and it's been difficult to get it to behave in a predictable way. // Our own cache ensures consistent evaluation within a single session. diff --git a/src/logger/bitdriftTransport.ts b/src/logger/bitdriftTransport.ts index c2235e0d4b..159b863004 100644 --- a/src/logger/bitdriftTransport.ts +++ b/src/logger/bitdriftTransport.ts @@ -3,8 +3,7 @@ import { error as bdError, info as bdInfo, warn as bdWarn, -} from '@bitdrift/react-native' - +} from '../lib/bitdrift' import {LogLevel, Transport} from './types' export function createBitdriftTransport(): Transport { @@ -18,6 +17,6 @@ export function createBitdriftTransport(): Transport { return (level, message) => { const log = logFunctions[level] - log(message.toString()) + log('' + message) } } diff --git a/src/logger/bitdriftTransport.web.ts b/src/logger/bitdriftTransport.web.ts deleted file mode 100644 index ecea3f6f3b..0000000000 --- a/src/logger/bitdriftTransport.web.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {Transport} from './index' - -export function createBitdriftTransport(): Transport { - return (_level, _message) => { - // noop - } -} diff --git a/src/state/session/__tests__/session-test.ts b/src/state/session/__tests__/session-test.ts index 7a5ddfa974..dec8ec48bd 100644 --- a/src/state/session/__tests__/session-test.ts +++ b/src/state/session/__tests__/session-test.ts @@ -4,25 +4,12 @@ import {describe, expect, it, jest} from '@jest/globals' import {agentToSessionAccountOrThrow} from '../agent' import {Action, getInitialState, reducer, State} from '../reducer' -jest.mock('statsig-react-native-expo', () => ({ - Statsig: { - initialize() {}, - initializeCalled() { - return false - }, - }, -})) - jest.mock('jwt-decode', () => ({ jwtDecode(_token: string) { return {} }, })) -jest.mock('expo-localization', () => ({ - getLocales: () => [], -})) - describe('session', () => { it('can log in and out', () => { let state = getInitialState([]) From 0cbb03cd14c226bcbfd146a586d97c62a0fc4c9d Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 17 Dec 2024 17:13:18 +0000 Subject: [PATCH 53/58] New progress guide - 10 follows (#7128) * new follow-10 progress guide * find follows dialog * wip tabs * flatlist version with search * hardcode out jake gold * lazy load followup suggestions * Update src/components/ProgressGuide/FollowDialog.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * comment out replacing, enable paging * rm autofocus * find shadow profiles in paginated search * clear search when press tabs * better tab a11y * fix label * adjust scroll indicator insets * do the same scroll indicator adjustment for searchable people list * hardcode jake to just be 'tech' * Retain state on close/reopen * only change follow btn color when not followed * add guide to inside dialog * fix task alignment * Enable contextual suggestions * WIP: show multiple suggestions * Rework so it animates well * Show more items * remove card style * move tabs to own component * split out header top * scroll active tab into view * rm log * Improve perf a bit * boost popular interests over alphabetical ones * scroll active tab into view * revert back to round buttons * Fix overrenders of the tab bar items * Fix unintended animation * Scroll initial into view if needed * Unlift state, the dialog thing breaks lifting * Persist simply * Fix empty state * Fix incorrect gate exposure * Fix another bad useGate * Nit --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> Co-authored-by: Dan Abramov --- src/alf/atoms.ts | 12 + src/components/FeedInterstitials.tsx | 2 +- src/components/ProfileCard.tsx | 7 +- src/components/ProgressGuide/FollowDialog.tsx | 829 ++++++++++++++++++ src/components/ProgressGuide/List.tsx | 44 +- src/components/ProgressGuide/Task.tsx | 14 +- .../dms/dialogs/SearchablePeopleList.tsx | 3 + src/lib/statsig/events.ts | 2 + src/lib/statsig/gates.ts | 5 +- src/screens/Onboarding/StepFinished.tsx | 8 +- src/screens/Onboarding/state.ts | 13 + src/state/queries/actor-search.ts | 26 +- src/state/queries/suggested-follows.ts | 9 +- src/state/shell/progress-guide.tsx | 96 +- src/view/com/posts/PostFeed.tsx | 6 +- src/view/com/util/List.tsx | 6 +- src/view/com/util/Toast.tsx | 4 +- 17 files changed, 1039 insertions(+), 47 deletions(-) create mode 100644 src/components/ProgressGuide/FollowDialog.tsx diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts index ad4929ec85..df2b29d8ac 100644 --- a/src/alf/atoms.ts +++ b/src/alf/atoms.ts @@ -302,6 +302,18 @@ export const atoms = { border_0: { borderWidth: 0, }, + border_t_0: { + borderTopWidth: 0, + }, + border_b_0: { + borderBottomWidth: 0, + }, + border_l_0: { + borderLeftWidth: 0, + }, + border_r_0: { + borderRightWidth: 0, + }, border: { borderWidth: StyleSheet.hairlineWidth, }, diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx index 85ed58280d..ec224eeae0 100644 --- a/src/components/FeedInterstitials.tsx +++ b/src/components/FeedInterstitials.tsx @@ -280,8 +280,8 @@ export function ProfileGrid({ profile={profile} moderationOpts={moderationOpts} logContext="FeedInterstitial" - color="secondary_inverted" shape="round" + colorInverted /> diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index 668bd0f3c7..7bec14b9cc 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -285,6 +285,7 @@ export type FollowButtonProps = { moderationOpts: ModerationOpts logContext: LogEvents['profile:follow']['logContext'] & LogEvents['profile:unfollow']['logContext'] + colorInverted?: boolean } & Partial export function FollowButton(props: FollowButtonProps) { @@ -297,6 +298,8 @@ export function FollowButtonInner({ profile: profileUnshadowed, moderationOpts, logContext, + onPress: onPressProp, + colorInverted, ...rest }: FollowButtonProps) { const {_} = useLingui() @@ -321,6 +324,7 @@ export function FollowButtonInner({ )}`, ), ) + onPressProp?.(e) } catch (err: any) { if (err?.name !== 'AbortError') { Toast.show(_(msg`An issue occurred, please try again.`), 'xmark') @@ -341,6 +345,7 @@ export function FollowButtonInner({ )}`, ), ) + onPressProp?.(e) } catch (err: any) { if (err?.name !== 'AbortError') { Toast.show(_(msg`An issue occurred, please try again.`), 'xmark') @@ -387,7 +392,7 @@ export function FollowButtonInner({ label={followLabel} size="small" variant="solid" - color="primary" + color={colorInverted ? 'secondary_inverted' : 'primary'} {...rest} onPress={onPressFollow}> diff --git a/src/components/ProgressGuide/FollowDialog.tsx b/src/components/ProgressGuide/FollowDialog.tsx new file mode 100644 index 0000000000..6ac3200df1 --- /dev/null +++ b/src/components/ProgressGuide/FollowDialog.tsx @@ -0,0 +1,829 @@ +import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react' +import {ScrollView, TextInput, useWindowDimensions, View} from 'react-native' +import Animated, { + LayoutAnimationConfig, + LinearTransition, + ZoomInEasyDown, +} from 'react-native-reanimated' +import {AppBskyActorDefs, ModerationOpts} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' +import {cleanError} from '#/lib/strings/errors' +import {logger} from '#/logger' +import {isWeb} from '#/platform/detection' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useActorSearchPaginated} from '#/state/queries/actor-search' +import {usePreferencesQuery} from '#/state/queries/preferences' +import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows' +import {useSession} from '#/state/session' +import {Follow10ProgressGuide} from '#/state/shell/progress-guide' +import {ListMethods} from '#/view/com/util/List' +import { + popularInterests, + useInterestsDisplayNames, +} from '#/screens/Onboarding/state' +import { + atoms as a, + native, + tokens, + useBreakpoints, + useTheme, + ViewStyleProp, + web, +} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {useInteractionState} from '#/components/hooks/useInteractionState' +import {MagnifyingGlass2_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass2' +import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person' +import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' +import * as ProfileCard from '#/components/ProfileCard' +import {Text} from '#/components/Typography' +import {ListFooter} from '../Lists' +import {ProgressGuideTask} from './Task' + +type Item = + | { + type: 'profile' + key: string + profile: AppBskyActorDefs.ProfileView + isSuggestion: boolean + } + | { + type: 'empty' + key: string + message: string + } + | { + type: 'placeholder' + key: string + } + | { + type: 'error' + key: string + } + +export function FollowDialog({guide}: {guide: Follow10ProgressGuide}) { + const {_} = useLingui() + const control = Dialog.useDialogControl() + const {gtMobile} = useBreakpoints() + const {height: minHeight} = useWindowDimensions() + + return ( + <> + + + + + + + ) +} + +// Fine to keep this top-level. +let lastSelectedInterest = '' +let lastSearchText = '' + +function DialogInner({guide}: {guide: Follow10ProgressGuide}) { + const {_} = useLingui() + const interestsDisplayNames = useInterestsDisplayNames() + const {data: preferences} = usePreferencesQuery() + const personalizedInterests = preferences?.interests?.tags + const interests = Object.keys(interestsDisplayNames) + .sort(boostInterests(popularInterests)) + .sort(boostInterests(personalizedInterests)) + const [selectedInterest, setSelectedInterest] = useState( + () => + lastSelectedInterest || + (personalizedInterests && interests.includes(personalizedInterests[0]) + ? personalizedInterests[0] + : interests[0]), + ) + const [searchText, setSearchText] = useState(lastSearchText) + const moderationOpts = useModerationOpts() + const listRef = useRef(null) + const inputRef = useRef(null) + const [headerHeight, setHeaderHeight] = useState(0) + const {currentAccount} = useSession() + const [suggestedAccounts, setSuggestedAccounts] = useState< + Map + >(() => new Map()) + + useEffect(() => { + lastSearchText = searchText + lastSelectedInterest = selectedInterest + }, [searchText, selectedInterest]) + + const query = searchText || selectedInterest + const { + data: searchResults, + isFetching, + error, + isError, + hasNextPage, + isFetchingNextPage, + fetchNextPage, + } = useActorSearchPaginated({ + query, + }) + + const hasSearchText = !!searchText + + const items = useMemo(() => { + const results = searchResults?.pages.flatMap(r => r.actors) + let _items: Item[] = [] + const seen = new Set() + + if (isError) { + _items.push({ + type: 'empty', + key: 'empty', + message: _(msg`We're having network issues, try again`), + }) + } else if (results) { + // First pass: search results + for (const profile of results) { + if (profile.did === currentAccount?.did) continue + if (profile.viewer?.following) continue + // my sincere apologies to Jake Gold - your bio is too keyword-filled and + // your page-rank too high, so you're at the top of half the categories -sfn + if ( + !hasSearchText && + profile.did === 'did:plc:tpg43qhh4lw4ksiffs4nbda3' && + // constrain to 'tech' + selectedInterest !== 'tech' + ) { + continue + } + seen.add(profile.did) + _items.push({ + type: 'profile', + // Don't share identity across tabs or typing attempts + key: query + ':' + profile.did, + profile, + isSuggestion: false, + }) + } + // Second pass: suggestions + _items = _items.flatMap(item => { + if (item.type !== 'profile') { + return item + } + const suggestions = suggestedAccounts.get(item.profile.did) + if (!suggestions) { + return item + } + const itemWithSuggestions = [item] + for (const suggested of suggestions) { + if (seen.has(suggested.did)) { + // Skip search results from previous step or already seen suggestions + continue + } + seen.add(suggested.did) + itemWithSuggestions.push({ + type: 'profile', + key: suggested.did, + profile: suggested, + isSuggestion: true, + }) + if (itemWithSuggestions.length === 1 + 3) { + break + } + } + return itemWithSuggestions + }) + } else { + const placeholders: Item[] = Array(10) + .fill(0) + .map((__, i) => ({ + type: 'placeholder', + key: i + '', + })) + + _items.push(...placeholders) + } + + return _items + }, [ + _, + searchResults, + isError, + currentAccount?.did, + hasSearchText, + selectedInterest, + suggestedAccounts, + query, + ]) + + if (searchText && !isFetching && !items.length && !isError) { + items.push({type: 'empty', key: 'empty', message: _(msg`No results`)}) + } + + const renderItems = useCallback( + ({item, index}: {item: Item; index: number}) => { + switch (item.type) { + case 'profile': { + return ( + + ) + } + case 'placeholder': { + return + } + case 'empty': { + return + } + default: + return null + } + }, + [moderationOpts], + ) + + const onSelectTab = useCallback( + (interest: string) => { + setSelectedInterest(interest) + inputRef.current?.clear() + setSearchText('') + listRef.current?.scrollToOffset({ + offset: 0, + animated: false, + }) + }, + [setSelectedInterest, setSearchText], + ) + + const listHeader = ( +
+ ) + + const onEndReached = useCallback(async () => { + if (isFetchingNextPage || !hasNextPage || isError) return + try { + await fetchNextPage() + } catch (err) { + logger.error('Failed to load more people to follow', {message: err}) + } + }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) + + return ( + item.key} + style={[ + a.px_0, + web([a.py_0, {height: '100vh', maxHeight: 600}]), + native({height: '100%'}), + ]} + webInnerContentContainerStyle={a.py_0} + webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]} + keyboardDismissMode="on-drag" + scrollIndicatorInsets={{top: headerHeight}} + initialNumToRender={8} + maxToRenderPerBatch={8} + onEndReached={onEndReached} + itemLayoutAnimation={LinearTransition} + ListFooterComponent={ + + } + /> + ) +} + +let Header = ({ + guide, + inputRef, + listRef, + searchText, + onSelectTab, + setHeaderHeight, + setSearchText, + interests, + selectedInterest, + interestsDisplayNames, +}: { + guide: Follow10ProgressGuide + inputRef: React.RefObject + listRef: React.RefObject + onSelectTab: (v: string) => void + searchText: string + setHeaderHeight: (v: number) => void + setSearchText: (v: string) => void + interests: string[] + selectedInterest: string + interestsDisplayNames: Record +}): React.ReactNode => { + const t = useTheme() + const control = Dialog.useDialogContext() + return ( + setHeaderHeight(evt.nativeEvent.layout.height)} + style={[ + a.relative, + web(a.pt_lg), + native(a.pt_4xl), + a.pb_xs, + a.border_b, + t.atoms.border_contrast_low, + t.atoms.bg, + ]}> + + + + { + setSearchText(text) + listRef.current?.scrollToOffset({offset: 0, animated: false}) + }} + onEscape={control.close} + /> + + + + ) +} +Header = memo(Header) + +function HeaderTop({guide}: {guide: Follow10ProgressGuide}) { + const {_} = useLingui() + const t = useTheme() + const control = Dialog.useDialogContext() + return ( + + + Find people to follow + + + + + {isWeb ? ( + + ) : null} + + ) +} + +let Tabs = ({ + onSelectTab, + interests, + selectedInterest, + hasSearchText, + interestsDisplayNames, +}: { + onSelectTab: (tab: string) => void + interests: string[] + selectedInterest: string + hasSearchText: boolean + interestsDisplayNames: Record +}): React.ReactNode => { + const listRef = useRef(null) + const [scrollX, setScrollX] = useState(0) + const [totalWidth, setTotalWidth] = useState(0) + const pendingTabOffsets = useRef<{x: number; width: number}[]>([]) + const [tabOffsets, setTabOffsets] = useState<{x: number; width: number}[]>([]) + + const onInitialLayout = useNonReactiveCallback(() => { + const index = interests.indexOf(selectedInterest) + scrollIntoViewIfNeeded(index) + }) + + useEffect(() => { + if (tabOffsets) { + onInitialLayout() + } + }, [tabOffsets, onInitialLayout]) + + function scrollIntoViewIfNeeded(index: number) { + const btnLayout = tabOffsets[index] + if (!btnLayout) return + + const viewportLeftEdge = scrollX + const viewportRightEdge = scrollX + totalWidth + const shouldScrollToLeftEdge = viewportLeftEdge > btnLayout.x + const shouldScrollToRightEdge = + viewportRightEdge < btnLayout.x + btnLayout.width + + if (shouldScrollToLeftEdge) { + listRef.current?.scrollTo({ + x: btnLayout.x - tokens.space.lg, + animated: true, + }) + } else if (shouldScrollToRightEdge) { + listRef.current?.scrollTo({ + x: btnLayout.x - totalWidth + btnLayout.width + tokens.space.lg, + animated: true, + }) + } + } + + function handleSelectTab(index: number) { + const tab = interests[index] + onSelectTab(tab) + scrollIntoViewIfNeeded(index) + } + + function handleTabLayout(index: number, x: number, width: number) { + if (!tabOffsets.length) { + pendingTabOffsets.current[index] = {x, width} + if (pendingTabOffsets.current.length === interests.length) { + setTabOffsets(pendingTabOffsets.current) + } + } + } + + return ( + o.x - tokens.space.xl) + : undefined + } + onLayout={evt => setTotalWidth(evt.nativeEvent.layout.width)} + scrollEventThrottle={200} // big throttle + onScroll={evt => setScrollX(evt.nativeEvent.contentOffset.x)}> + {interests.map((interest, i) => { + const active = interest === selectedInterest && !hasSearchText + return ( + + ) + })} + + ) +} +Tabs = memo(Tabs) + +let Tab = ({ + onSelectTab, + interest, + active, + index, + interestsDisplayName, + onLayout, +}: { + onSelectTab: (index: number) => void + interest: string + active: boolean + index: number + interestsDisplayName: string + onLayout: (index: number, x: number, width: number) => void +}): React.ReactNode => { + const {_} = useLingui() + const activeText = active ? _(msg` (active)`) : '' + return ( + + onLayout(index, e.nativeEvent.layout.x, e.nativeEvent.layout.width) + }> + + + ) +} +Tab = memo(Tab) + +let FollowProfileCard = ({ + profile, + moderationOpts, + isSuggestion, + setSuggestedAccounts, + noBorder, +}: { + profile: AppBskyActorDefs.ProfileView + moderationOpts: ModerationOpts + isSuggestion: boolean + setSuggestedAccounts: ( + updater: ( + v: Map, + ) => Map, + ) => void + noBorder?: boolean +}): React.ReactNode => { + const [hasFollowed, setHasFollowed] = useState(false) + const followupSuggestion = useSuggestedFollowsByActorQuery({ + did: profile.did, + enabled: hasFollowed, + }) + const candidates = followupSuggestion.data?.suggestions + + useEffect(() => { + // TODO: Move out of effect. + if (hasFollowed && candidates && candidates.length > 0) { + setSuggestedAccounts(suggestions => { + const newSuggestions = new Map(suggestions) + newSuggestions.set(profile.did, candidates) + return newSuggestions + }) + } + }, [hasFollowed, profile.did, candidates, setSuggestedAccounts]) + + return ( + + + setHasFollowed(true)} + noBorder={noBorder} + /> + + + ) +} +FollowProfileCard = memo(FollowProfileCard) + +function FollowProfileCardInner({ + profile, + moderationOpts, + onFollow, + noBorder, +}: { + profile: AppBskyActorDefs.ProfileView + moderationOpts: ModerationOpts + onFollow?: () => void + noBorder?: boolean +}) { + const control = Dialog.useDialogContext() + const t = useTheme() + return ( + control.close()}> + {({hovered, pressed}) => ( + + + + + + + + + + + )} + + ) +} + +function CardOuter({ + children, + style, +}: {children: React.ReactNode | React.ReactNode[]} & ViewStyleProp) { + const t = useTheme() + return ( + + {children} + + ) +} + +function SearchInput({ + onChangeText, + onEscape, + inputRef, + defaultValue, +}: { + onChangeText: (text: string) => void + onEscape: () => void + inputRef: React.RefObject + defaultValue: string +}) { + const t = useTheme() + const {_} = useLingui() + const { + state: hovered, + onIn: onMouseEnter, + onOut: onMouseLeave, + } = useInteractionState() + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() + const interacted = hovered || focused + + return ( + + + + { + if (nativeEvent.key === 'Escape') { + onEscape() + } + }} + autoCorrect={false} + autoComplete="off" + autoCapitalize="none" + accessibilityLabel={_(msg`Search profiles`)} + accessibilityHint={_(msg`Search profiles`)} + /> + + ) +} + +function ProfileCardSkeleton() { + const t = useTheme() + + return ( + + + + + + + + + ) +} + +function Empty({message}: {message: string}) { + const t = useTheme() + return ( + + + {message} + + + (╯°□°)╯︵ ┻━┻ + + ) +} + +function boostInterests(boosts?: string[]) { + return (_a: string, _b: string) => { + const indexA = boosts?.indexOf(_a) ?? -1 + const indexB = boosts?.indexOf(_b) ?? -1 + const rankA = indexA === -1 ? Infinity : indexA + const rankB = indexB === -1 ? Infinity : indexB + return rankA - rankB + } +} diff --git a/src/components/ProgressGuide/List.tsx b/src/components/ProgressGuide/List.tsx index 299d1e69fc..bbc5a0177f 100644 --- a/src/components/ProgressGuide/List.tsx +++ b/src/components/ProgressGuide/List.tsx @@ -10,12 +10,15 @@ import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times' import {Text} from '#/components/Typography' +import {FollowDialog} from './FollowDialog' import {ProgressGuideTask} from './Task' export function ProgressGuideList({style}: {style?: StyleProp}) { const t = useTheme() const {_} = useLingui() - const guide = useProgressGuide('like-10-and-follow-7') + const followProgressGuide = useProgressGuide('follow-10') + const followAndLikeProgressGuide = useProgressGuide('like-10-and-follow-7') + const guide = followProgressGuide || followAndLikeProgressGuide const {endProgressGuide} = useProgressGuideControls() if (guide) { @@ -41,18 +44,33 @@ export function ProgressGuideList({style}: {style?: StyleProp}) { - - + {guide.guide === 'follow-10' && ( + <> + + + + )} + {guide.guide === 'like-10-and-follow-7' && ( + <> + + + + )} ) } diff --git a/src/components/ProgressGuide/Task.tsx b/src/components/ProgressGuide/Task.tsx index 973ee1ac7f..b9ba3fd9ab 100644 --- a/src/components/ProgressGuide/Task.tsx +++ b/src/components/ProgressGuide/Task.tsx @@ -10,11 +10,13 @@ export function ProgressGuideTask({ total, title, subtitle, + tabularNumsTitle, }: { current: number total: number title: string subtitle?: string + tabularNumsTitle?: boolean }) { const t = useTheme() @@ -33,8 +35,16 @@ export function ProgressGuideTask({ /> )} - - {title} + + + {title} + {subtitle && ( diff --git a/src/components/dms/dialogs/SearchablePeopleList.tsx b/src/components/dms/dialogs/SearchablePeopleList.tsx index 0946d2a27c..50090cbcbb 100644 --- a/src/components/dms/dialogs/SearchablePeopleList.tsx +++ b/src/components/dms/dialogs/SearchablePeopleList.tsx @@ -63,6 +63,7 @@ export function SearchablePeopleList({ const {_} = useLingui() const moderationOpts = useModerationOpts() const control = Dialog.useDialogContext() + const [headerHeight, setHeaderHeight] = useState(0) const listRef = useRef(null) const {currentAccount} = useSession() const inputRef = useRef(null) @@ -237,6 +238,7 @@ export function SearchablePeopleList({ const listHeader = useMemo(() => { return ( setHeaderHeight(evt.nativeEvent.layout.height)} style={[ a.relative, web(a.pt_lg), @@ -315,6 +317,7 @@ export function SearchablePeopleList({ ]} webInnerContentContainerStyle={a.py_0} webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]} + scrollIndicatorInsets={{top: headerHeight}} keyboardDismissMode="on-drag" /> ) diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index e6c9c5d135..f1dfb0a947 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -162,6 +162,7 @@ export type LogEvents = { | 'StarterPackProfilesList' | 'FeedInterstitial' | 'ProfileHeaderSuggestedFollows' + | 'PostOnboardingFindFollows' } 'profile:unfollow': { logContext: @@ -177,6 +178,7 @@ export type LogEvents = { | 'StarterPackProfilesList' | 'FeedInterstitial' | 'ProfileHeaderSuggestedFollows' + | 'PostOnboardingFindFollows' } 'chat:create': { logContext: 'ProfileHeader' | 'NewChatDialog' | 'SendViaChatDialog' diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index 6876f18c53..a6c2492548 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -1,3 +1,6 @@ export type Gate = // Keep this alphabetic please. - 'debug_show_feedcontext' | 'debug_subscriptions' | 'remove_show_latest_button' + | 'debug_show_feedcontext' + | 'debug_subscriptions' + | 'new_postonboarding' + | 'remove_show_latest_button' diff --git a/src/screens/Onboarding/StepFinished.tsx b/src/screens/Onboarding/StepFinished.tsx index 0d8971b6f6..fc0ea6a247 100644 --- a/src/screens/Onboarding/StepFinished.tsx +++ b/src/screens/Onboarding/StepFinished.tsx @@ -14,7 +14,7 @@ import { TIMELINE_SAVED_FEED, } from '#/lib/constants' import {useRequestNotificationsPermission} from '#/lib/notifications/notifications' -import {logEvent} from '#/lib/statsig/statsig' +import {logEvent, useGate} from '#/lib/statsig/statsig' import {logger} from '#/logger' import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs' import {getAllListMembers} from '#/state/queries/list-members' @@ -57,6 +57,7 @@ export function StepFinished() { const setActiveStarterPack = useSetActiveStarterPack() const setHasCheckedForStarterPack = useSetHasCheckedForStarterPack() const {startProgressGuide} = useProgressGuideControls() + const gate = useGate() const finishOnboarding = React.useCallback(async () => { setSaving(true) @@ -190,7 +191,9 @@ export function StepFinished() { setSaving(false) setActiveStarterPack(undefined) setHasCheckedForStarterPack(true) - startProgressGuide('like-10-and-follow-7') + startProgressGuide( + gate('new_postonboarding') ? 'follow-10' : 'like-10-and-follow-7', + ) dispatch({type: 'finish'}) onboardDispatch({type: 'finish'}) logEvent('onboarding:finished:nextPressed', { @@ -221,6 +224,7 @@ export function StepFinished() { setActiveStarterPack, setHasCheckedForStarterPack, startProgressGuide, + gate, ]) return ( diff --git a/src/screens/Onboarding/state.ts b/src/screens/Onboarding/state.ts index 70fa696408..20d3ef2170 100644 --- a/src/screens/Onboarding/state.ts +++ b/src/screens/Onboarding/state.ts @@ -72,6 +72,19 @@ export type ApiResponseMap = { } } +// most popular selected interests +export const popularInterests = [ + 'art', + 'gaming', + 'sports', + 'comics', + 'music', + 'politics', + 'photography', + 'science', + 'news', +] + export function useInterestsDisplayNames() { const {_} = useLingui() diff --git a/src/state/queries/actor-search.ts b/src/state/queries/actor-search.ts index 479fc1a9f0..6d6c46e040 100644 --- a/src/state/queries/actor-search.ts +++ b/src/state/queries/actor-search.ts @@ -1,6 +1,7 @@ import {AppBskyActorDefs, AppBskyActorSearchActors} from '@atproto/api' import { InfiniteData, + keepPreviousData, QueryClient, QueryKey, useInfiniteQuery, @@ -13,10 +14,8 @@ import {useAgent} from '#/state/session' const RQKEY_ROOT = 'actor-search' export const RQKEY = (query: string) => [RQKEY_ROOT, query] -export const RQKEY_PAGINATED = (query: string) => [ - `${RQKEY_ROOT}_paginated`, - query, -] +const RQKEY_ROOT_PAGINATED = `${RQKEY_ROOT}_paginated` +export const RQKEY_PAGINATED = (query: string) => [RQKEY_ROOT_PAGINATED, query] export function useActorSearch({ query, @@ -42,9 +41,11 @@ export function useActorSearch({ export function useActorSearchPaginated({ query, enabled, + maintainData, }: { query: string enabled?: boolean + maintainData?: boolean }) { const agent = useAgent() return useInfiniteQuery< @@ -67,6 +68,7 @@ export function useActorSearchPaginated({ enabled: enabled && !!query, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, + placeholderData: maintainData ? keepPreviousData : undefined, }) } @@ -89,4 +91,20 @@ export function* findAllProfilesInQueryData( } } } + + const queryDatasPaginated = queryClient.getQueriesData< + InfiniteData + >({ + queryKey: [RQKEY_ROOT_PAGINATED], + }) + for (const [_queryKey, queryData] of queryDatasPaginated) { + if (!queryData) { + continue + } + for (const actor of queryData.pages.flatMap(page => page.actors)) { + if (actor.did === did) { + yield actor + } + } + } } diff --git a/src/state/queries/suggested-follows.ts b/src/state/queries/suggested-follows.ts index 07e16946e7..22033c0a8c 100644 --- a/src/state/queries/suggested-follows.ts +++ b/src/state/queries/suggested-follows.ts @@ -103,7 +103,13 @@ export function useSuggestedFollowsQuery(options?: SuggestedFollowsOptions) { }) } -export function useSuggestedFollowsByActorQuery({did}: {did: string}) { +export function useSuggestedFollowsByActorQuery({ + did, + enabled, +}: { + did: string + enabled?: boolean +}) { const agent = useAgent() return useQuery({ queryKey: suggestedFollowsByActorQueryKey(did), @@ -116,6 +122,7 @@ export function useSuggestedFollowsByActorQuery({did}: {did: string}) { : res.data.suggestions.filter(profile => !profile.viewer?.following) return {suggestions} }, + enabled, }) } diff --git a/src/state/shell/progress-guide.tsx b/src/state/shell/progress-guide.tsx index d64e9984f5..af3d60ebbd 100644 --- a/src/state/shell/progress-guide.tsx +++ b/src/state/shell/progress-guide.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, {useMemo} from 'react' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -16,20 +16,32 @@ export enum ProgressGuideAction { Follow = 'follow', } -type ProgressGuideName = 'like-10-and-follow-7' +type ProgressGuideName = 'like-10-and-follow-7' | 'follow-10' +/** + * Progress Guides that extend this interface must specify their name in the `guide` field, so it can be used as a discriminated union + */ interface BaseProgressGuide { - guide: string + guide: ProgressGuideName isComplete: boolean [key: string]: any } -interface Like10AndFollow7ProgressGuide extends BaseProgressGuide { +export interface Like10AndFollow7ProgressGuide extends BaseProgressGuide { + guide: 'like-10-and-follow-7' numLikes: number numFollows: number } -type ProgressGuide = Like10AndFollow7ProgressGuide | undefined +export interface Follow10ProgressGuide extends BaseProgressGuide { + guide: 'follow-10' + numFollows: number +} + +export type ProgressGuide = + | Like10AndFollow7ProgressGuide + | Follow10ProgressGuide + | undefined const ProgressGuideContext = React.createContext(undefined) @@ -61,15 +73,28 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const {mutateAsync, variables, isPending} = useSetActiveProgressGuideMutation() - const activeProgressGuide = ( - isPending ? variables : preferences?.bskyAppState?.activeProgressGuide - ) as ProgressGuide + const activeProgressGuide = useMemo(() => { + const rawProgressGuide = ( + isPending ? variables : preferences?.bskyAppState?.activeProgressGuide + ) as ProgressGuide + + if (!rawProgressGuide) return undefined + + // ensure the unspecced attributes have the correct types + // clone then mutate + const {...maybeWronglyTypedProgressGuide} = rawProgressGuide + if (maybeWronglyTypedProgressGuide?.guide === 'like-10-and-follow-7') { + maybeWronglyTypedProgressGuide.numLikes = + Number(maybeWronglyTypedProgressGuide.numLikes) || 0 + maybeWronglyTypedProgressGuide.numFollows = + Number(maybeWronglyTypedProgressGuide.numFollows) || 0 + } else if (maybeWronglyTypedProgressGuide?.guide === 'follow-10') { + maybeWronglyTypedProgressGuide.numFollows = + Number(maybeWronglyTypedProgressGuide.numFollows) || 0 + } - // ensure the unspecced attributes have the correct types - if (activeProgressGuide?.guide === 'like-10-and-follow-7') { - activeProgressGuide.numLikes = Number(activeProgressGuide.numLikes) || 0 - activeProgressGuide.numFollows = Number(activeProgressGuide.numFollows) || 0 - } + return maybeWronglyTypedProgressGuide + }, [isPending, variables, preferences]) const [localGuideState, setLocalGuideState] = React.useState(undefined) @@ -82,7 +107,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const firstLikeToastRef = React.useRef(null) const fifthLikeToastRef = React.useRef(null) const tenthLikeToastRef = React.useRef(null) - const guideCompleteToastRef = React.useRef(null) + + const fifthFollowToastRef = React.useRef(null) + const tenthFollowToastRef = React.useRef(null) const controls = React.useMemo(() => { return { @@ -93,7 +120,15 @@ export function Provider({children}: React.PropsWithChildren<{}>) { numLikes: 0, numFollows: 0, isComplete: false, - } + } satisfies ProgressGuide + setLocalGuideState(guideObj) + mutateAsync(guideObj) + } else if (guide === 'follow-10') { + const guideObj = { + guide: 'follow-10', + numFollows: 0, + isComplete: false, + } satisfies ProgressGuide setLocalGuideState(guideObj) mutateAsync(guideObj) } @@ -137,6 +172,26 @@ export function Provider({children}: React.PropsWithChildren<{}>) { isComplete: true, } } + } else if (guide?.guide === 'follow-10') { + if (action === ProgressGuideAction.Follow) { + guide = { + ...guide, + numFollows: (Number(guide.numFollows) || 0) + count, + } + + if (guide.numFollows === 5) { + fifthFollowToastRef.current?.open() + } + if (guide.numFollows === 10) { + tenthFollowToastRef.current?.open() + } + } + if (Number(guide.numFollows) >= 10) { + guide = { + ...guide, + isComplete: true, + } + } } setLocalGuideState(guide) @@ -167,9 +222,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) { subtitle={_(msg`The Discover feed now knows what you like`)} /> + )} diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 55d7ba053f..10eb47d0a4 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -253,9 +253,11 @@ let PostFeed = ({ } }, [pollInterval]) - const progressGuide = useProgressGuide('like-10-and-follow-7') + const followProgressGuide = useProgressGuide('follow-10') + const followAndLikeProgressGuide = useProgressGuide('like-10-and-follow-7') const {isDesktop} = useWebMediaQueries() - const showProgressIntersitial = progressGuide && !isDesktop + const showProgressIntersitial = + (followProgressGuide || followAndLikeProgressGuide) && !isDesktop const feedItems: FeedRow[] = React.useMemo(() => { let feedKind: 'following' | 'discover' | 'profile' | undefined diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx index 62c91cec66..5084af6129 100644 --- a/src/view/com/util/List.tsx +++ b/src/view/com/util/List.tsx @@ -155,7 +155,11 @@ let List = React.forwardRef( automaticallyAdjustsScrollIndicatorInsets={ automaticallyAdjustsScrollIndicatorInsets } - scrollIndicatorInsets={{top: headerOffset, right: 1}} + scrollIndicatorInsets={{ + top: headerOffset, + right: 1, + ...props.scrollIndicatorInsets, + }} contentOffset={contentOffset} refreshControl={refreshControl} onScroll={scrollHandler} diff --git a/src/view/com/util/Toast.tsx b/src/view/com/util/Toast.tsx index 7dc6837e23..c602886745 100644 --- a/src/view/com/util/Toast.tsx +++ b/src/view/com/util/Toast.tsx @@ -196,7 +196,9 @@ function Toast({ /> - {message} + + {message} + From 65d4416f9e30b58c32c02cf65bc84db53c952a87 Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 18 Dec 2024 02:53:42 +0000 Subject: [PATCH 54/58] Tweak Follow dialog Search placeholder (#7147) --- src/components/ProgressGuide/FollowDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ProgressGuide/FollowDialog.tsx b/src/components/ProgressGuide/FollowDialog.tsx index 6ac3200df1..fa233bb65a 100644 --- a/src/components/ProgressGuide/FollowDialog.tsx +++ b/src/components/ProgressGuide/FollowDialog.tsx @@ -738,7 +738,7 @@ function SearchInput({ Date: Tue, 17 Dec 2024 21:12:53 -0600 Subject: [PATCH 55/58] Fix emoji picker position (#7146) --- .../text-input/web/EmojiPicker.web.tsx | 71 ++++++++++--------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/src/view/com/composer/text-input/web/EmojiPicker.web.tsx b/src/view/com/composer/text-input/web/EmojiPicker.web.tsx index 1d5dad4861..c721729020 100644 --- a/src/view/com/composer/text-input/web/EmojiPicker.web.tsx +++ b/src/view/com/composer/text-input/web/EmojiPicker.web.tsx @@ -10,6 +10,7 @@ import {DismissableLayer} from '@radix-ui/react-dismissable-layer' import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter' import {atoms as a} from '#/alf' +import {Portal} from '#/components/Portal' const HEIGHT_OFFSET = 40 const WIDTH_OFFSET = 100 @@ -125,39 +126,41 @@ export function EmojiPicker({state, close, pinToTop}: IProps) { } return ( - - - {/* eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors */} - e.stopPropagation()}> - - evt.preventDefault()} - onDismiss={close}> - { - return (await import('./EmojiPickerData.json')).default - }} - onEmojiSelect={onInsert} - autoFocus={true} - /> - - - - - + + + + {/* eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors */} + e.stopPropagation()}> + + evt.preventDefault()} + onDismiss={close}> + { + return (await import('./EmojiPickerData.json')).default + }} + onEmojiSelect={onInsert} + autoFocus={true} + /> + + + + + + ) } From a2019aceec001e276272832b97ea5e2ec864c8a5 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 17 Dec 2024 21:45:39 -0600 Subject: [PATCH 56/58] 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 Co-authored-by: Dan Abramov Co-authored-by: Hailey --- bskyweb/cmd/bskyweb/server.go | 1 + package.json | 2 +- src/App.native.tsx | 15 +- src/App.web.tsx | 7 +- src/Navigation.tsx | 6 + src/components/GradientFill.tsx | 8 +- src/components/TrendingTopics.tsx | 223 ++++++++++++++++++ src/components/interstitials/Trending.tsx | 111 +++++++++ src/lib/routes/types.ts | 3 + src/lib/statsig/gates.ts | 1 + src/routes.ts | 1 + .../components/ExploreRecommendations.tsx | 95 ++++++++ .../components/ExploreTrendingTopics.tsx | 102 ++++++++ .../Settings/ContentAndMediaSettings.tsx | 27 +++ src/screens/Topic.tsx | 204 ++++++++++++++++ src/state/persisted/schema.ts | 2 + src/state/preferences/index.tsx | 5 +- src/state/preferences/trending.tsx | 69 ++++++ src/state/queries/index.ts | 1 + src/state/queries/service-config.ts | 32 +++ .../queries/trending/useTrendingTopics.ts | 49 ++++ src/state/trending-config.tsx | 70 ++++++ src/storage/schema.ts | 1 + src/view/com/posts/PostFeed.tsx | 25 +- src/view/screens/Search/Explore.tsx | 48 +++- src/view/shell/desktop/Feeds.tsx | 32 ++- src/view/shell/desktop/RightNav.tsx | 44 +++- .../shell/desktop/SidebarTrendingTopics.tsx | 104 ++++++++ yarn.lock | 60 ++--- 29 files changed, 1268 insertions(+), 80 deletions(-) create mode 100644 src/components/TrendingTopics.tsx create mode 100644 src/components/interstitials/Trending.tsx create mode 100644 src/screens/Search/components/ExploreRecommendations.tsx create mode 100644 src/screens/Search/components/ExploreTrendingTopics.tsx create mode 100644 src/screens/Topic.tsx create mode 100644 src/state/preferences/trending.tsx create mode 100644 src/state/queries/service-config.ts create mode 100644 src/state/queries/trending/useTrendingTopics.ts create mode 100644 src/state/trending-config.tsx create mode 100644 src/view/shell/desktop/SidebarTrendingTopics.tsx diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 19252f7654..3ef19fc462 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -235,6 +235,7 @@ func serve(cctx *cli.Context) error { // generic routes e.GET("/hashtag/:tag", server.WebGeneric) + e.GET("/topic/:topic", server.WebGeneric) e.GET("/search", server.WebGeneric) e.GET("/feeds", server.WebGeneric) e.GET("/notifications", server.WebGeneric) diff --git a/package.json b/package.json index f5f4601d6c..ff2223b489 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "icons:optimize": "svgo -f ./assets/icons" }, "dependencies": { - "@atproto/api": "^0.13.20", + "@atproto/api": "^0.13.21", "@bitdrift/react-native": "0.4.0", "@braintree/sanitize-url": "^6.0.2", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", diff --git a/src/App.native.tsx b/src/App.native.tsx index 39ab7ca92c..780295ddce 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -57,6 +57,7 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' +import {Provider as TrendingConfigProvider} from '#/state/trending-config' import {TestCtrls} from '#/view/com/testing/TestCtrls' import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext' import * as Toast from '#/view/com/util/Toast' @@ -143,12 +144,14 @@ function InnerApp() { - - - - - + + + + + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index 8d13a826e7..8a2e13600f 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -47,6 +47,7 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' +import {Provider as TrendingConfigProvider} from '#/state/trending-config' import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext' import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext' import * as Toast from '#/view/com/util/Toast' @@ -127,8 +128,10 @@ function InnerApp() { - - + + + + diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 7443128d2c..18705c5ffb 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -100,6 +100,7 @@ import {LanguageSettingsScreen} from './screens/Settings/LanguageSettings' import {PrivacyAndSecuritySettingsScreen} from './screens/Settings/PrivacyAndSecuritySettings' import {SettingsScreen} from './screens/Settings/Settings' import {ThreadPreferencesScreen} from './screens/Settings/ThreadPreferences' +import TopicScreen from './screens/Topic' const navigationRef = createNavigationContainerRef() @@ -376,6 +377,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { getComponent={() => HashtagScreen} options={{title: title(msg`Hashtag`)}} /> + TopicScreen} + options={{title: title(msg`Topic`)}} + /> MessagesConversationScreen} diff --git a/src/components/GradientFill.tsx b/src/components/GradientFill.tsx index 3dff404d74..9ad6ed7dc6 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 0000000000..6881f24bd5 --- /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 ( + + {/* + + {topic.type === 'tag' ? ( + + ) : topic.type === 'topic' ? ( + + ) : topic.type === 'feed' ? ( + + ) : ( + + )} + + */} + + + {topic.displayName} + + + ) +} + +export function TrendingTopicSkeleton({ + size = 'large', + index = 0, +}: { + size?: 'large' | 'small' + index?: number +}) { + const t = useTheme() + const isSmall = size === 'small' + return ( + + ) +} + +export function TrendingTopicLink({ + topic: raw, + children, + ...rest +}: { + topic: TrendingTopic +} & Omit) { + const topic = useTopic(raw) + + return ( + + {children} + + ) +} + +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 0000000000..3944d92f07 --- /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 ? : 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 : ( + + + + + + Trending + + + + + BETA + + + + + + + + + {isLoading ? ( + Array(TRENDING_TOPICS_COUNT) + .fill(0) + .map((_n, i) => ) + ) : !trending?.topics ? null : ( + <> + {trending.topics.map(topic => ( + + {({hovered}) => ( + + )} + + ))} + + )} + + + setTrendingDisabled(true)} + /> + + ) +} diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 238e4be4c3..d720886e9f 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -47,6 +47,7 @@ export type CommonNavigatorParams = { AppIconSettings: undefined Search: {q?: string} Hashtag: {tag: string; author?: string} + Topic: {topic: string} MessagesConversation: {conversation: string; embed?: string} MessagesSettings: undefined NotificationSettings: undefined @@ -92,6 +93,7 @@ export type FlatNavigatorParams = CommonNavigatorParams & { Feeds: undefined Notifications: undefined Hashtag: {tag: string; author?: string} + Topic: {topic: string} Messages: {pushToConversation?: string; animation?: 'push' | 'pop'} } @@ -105,6 +107,7 @@ export type AllNavigatorParams = CommonNavigatorParams & { Notifications: undefined MyProfileTab: undefined Hashtag: {tag: string; author?: string} + Topic: {topic: string} MessagesTab: undefined Messages: {animation?: 'push' | 'pop'} Start: {name: string; rkey: string} diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index a6c2492548..455a703458 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -4,3 +4,4 @@ export type Gate = | 'debug_subscriptions' | 'new_postonboarding' | 'remove_show_latest_button' + | 'trending_topics_beta' diff --git a/src/routes.ts b/src/routes.ts index 188665d849..7cd7c0880d 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -53,6 +53,7 @@ export const router = new Router({ CopyrightPolicy: '/support/copyright', // hashtags Hashtag: '/hashtag/:tag', + Topic: '/topic/:topic', // DMs Messages: '/messages', MessagesSettings: '/messages/settings', diff --git a/src/screens/Search/components/ExploreRecommendations.tsx b/src/screens/Search/components/ExploreRecommendations.tsx new file mode 100644 index 0000000000..e253cfb5ab --- /dev/null +++ b/src/screens/Search/components/ExploreRecommendations.tsx @@ -0,0 +1,95 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import {isWeb} from '#/platform/detection' +import {useTrendingSettings} from '#/state/preferences/trending' +import { + DEFAULT_LIMIT as RECOMMENDATIONS_COUNT, + useTrendingTopics, +} from '#/state/queries/trending/useTrendingTopics' +import {useTrendingConfig} from '#/state/trending-config' +import {atoms as a, useGutters, useTheme} from '#/alf' +import {Hashtag_Stroke2_Corner0_Rounded} from '#/components/icons/Hashtag' +import { + TrendingTopic, + TrendingTopicLink, + TrendingTopicSkeleton, +} from '#/components/TrendingTopics' +import {Text} from '#/components/Typography' + +export function ExploreRecommendations() { + const {enabled} = useTrendingConfig() + const {trendingDisabled} = useTrendingSettings() + return enabled && !trendingDisabled ? : null +} + +function Inner() { + const t = useTheme() + const gutters = useGutters([0, 'compact']) + const {data: trending, error, isLoading} = useTrendingTopics() + const noRecs = !isLoading && !error && !trending?.suggested?.length + + return error || noRecs ? null : ( + <> + + + + + + Recommended + + + + Feeds we think you might like. + + + + + + + {isLoading ? ( + Array(RECOMMENDATIONS_COUNT) + .fill(0) + .map((_, i) => ) + ) : !trending?.suggested ? null : ( + <> + {trending.suggested.map(topic => ( + + {({hovered}) => ( + + )} + + ))} + + )} + + + + ) +} diff --git a/src/screens/Search/components/ExploreTrendingTopics.tsx b/src/screens/Search/components/ExploreTrendingTopics.tsx new file mode 100644 index 0000000000..be347dcd4d --- /dev/null +++ b/src/screens/Search/components/ExploreTrendingTopics.tsx @@ -0,0 +1,102 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import {isWeb} from '#/platform/detection' +import {useTrendingSettings} 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 {GradientFill} from '#/components/GradientFill' +import {Trending2_Stroke2_Corner2_Rounded as Trending} from '#/components/icons/Trending2' +import { + TrendingTopic, + TrendingTopicLink, + TrendingTopicSkeleton, +} from '#/components/TrendingTopics' +import {Text} from '#/components/Typography' + +export function ExploreTrendingTopics() { + const {enabled} = useTrendingConfig() + const {trendingDisabled} = useTrendingSettings() + return enabled && !trendingDisabled ? : null +} + +function Inner() { + const t = useTheme() + const gutters = useGutters([0, 'compact']) + const {data: trending, error, isLoading} = useTrendingTopics() + const noTopics = !isLoading && !error && !trending?.topics?.length + + return error || noTopics ? null : ( + <> + + + + + + Trending + + + + + BETA + + + + + What people are posting about. + + + + + + + {isLoading ? ( + Array(TRENDING_TOPICS_COUNT) + .fill(0) + .map((_, i) => ) + ) : !trending?.topics ? null : ( + <> + {trending.topics.map(topic => ( + + {({hovered}) => ( + + )} + + ))} + + )} + + + + ) +} diff --git a/src/screens/Settings/ContentAndMediaSettings.tsx b/src/screens/Settings/ContentAndMediaSettings.tsx index 17f8fa5067..bdbe1d191b 100644 --- a/src/screens/Settings/ContentAndMediaSettings.tsx +++ b/src/screens/Settings/ContentAndMediaSettings.tsx @@ -9,6 +9,11 @@ import { useInAppBrowser, useSetInAppBrowser, } from '#/state/preferences/in-app-browser' +import { + useTrendingSettings, + useTrendingSettingsApi, +} from '#/state/preferences/trending' +import {useTrendingConfig} from '#/state/trending-config' import * as SettingsList from '#/screens/Settings/components/SettingsList' import * as Toggle from '#/components/forms/Toggle' import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble' @@ -16,6 +21,7 @@ import {Hashtag_Stroke2_Corner0_Rounded as HashtagIcon} from '#/components/icons import {Home_Stroke2_Corner2_Rounded as HomeIcon} from '#/components/icons/Home' import {Macintosh_Stroke2_Corner2_Rounded as MacintoshIcon} from '#/components/icons/Macintosh' import {Play_Stroke2_Corner2_Rounded as PlayIcon} from '#/components/icons/Play' +import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2' import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window' import * as Layout from '#/components/Layout' @@ -29,6 +35,9 @@ export function ContentAndMediaSettingsScreen({}: Props) { const setAutoplayDisabledPref = useSetAutoplayDisabled() const inAppBrowserPref = useInAppBrowser() const setUseInAppBrowser = useSetInAppBrowser() + const {enabled: trendingEnabled} = useTrendingConfig() + const {trendingDisabled} = useTrendingSettings() + const {setTrendingDisabled} = useTrendingSettingsApi() return ( @@ -104,6 +113,24 @@ export function ContentAndMediaSettingsScreen({}: Props) { + {trendingEnabled && ( + <> + + setTrendingDisabled(!value)}> + + + + Enable trending topics + + + + + + )} diff --git a/src/screens/Topic.tsx b/src/screens/Topic.tsx new file mode 100644 index 0000000000..6cd69f05f2 --- /dev/null +++ b/src/screens/Topic.tsx @@ -0,0 +1,204 @@ +import React from 'react' +import {ListRenderItemInfo, View} from 'react-native' +import {PostView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useFocusEffect} from '@react-navigation/native' +import {NativeStackScreenProps} from '@react-navigation/native-stack' + +import {HITSLOP_10} from '#/lib/constants' +import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' +import {CommonNavigatorParams} from '#/lib/routes/types' +import {shareUrl} from '#/lib/sharing' +import {cleanError} from '#/lib/strings/errors' +import {enforceLen} from '#/lib/strings/helpers' +import {useSearchPostsQuery} from '#/state/queries/search-posts' +import {useSetMinimalShellMode} from '#/state/shell' +import {Pager} from '#/view/com/pager/Pager' +import {TabBar} from '#/view/com/pager/TabBar' +import {Post} from '#/view/com/post/Post' +import {List} from '#/view/com/util/List' +import {atoms as a, web} from '#/alf' +import {Button, ButtonIcon} from '#/components/Button' +import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' +import * as Layout from '#/components/Layout' +import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' + +const renderItem = ({item}: ListRenderItemInfo) => { + return +} + +const keyExtractor = (item: PostView, index: number) => { + return `${item.uri}-${index}` +} + +export default function TopicScreen({ + route, +}: NativeStackScreenProps) { + const {topic} = route.params + const {_} = useLingui() + + const headerTitle = React.useMemo(() => { + return enforceLen(decodeURIComponent(topic), 24, true, 'middle') + }, [topic]) + + const onShare = React.useCallback(() => { + const url = new URL('https://bsky.app') + url.pathname = `/topic/${topic}` + shareUrl(url.toString()) + }, [topic]) + + const [activeTab, setActiveTab] = React.useState(0) + const setMinimalShellMode = useSetMinimalShellMode() + + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(false) + }, [setMinimalShellMode]), + ) + + const onPageSelected = React.useCallback( + (index: number) => { + setMinimalShellMode(false) + setActiveTab(index) + }, + [setMinimalShellMode], + ) + + const sections = React.useMemo(() => { + return [ + { + title: _(msg`Top`), + component: ( + + ), + }, + { + title: _(msg`Latest`), + component: ( + + ), + }, + ] + }, [_, topic, activeTab]) + + return ( + + + + + {headerTitle} + + + + + + ( + + section.title)} {...props} /> + + )} + initialPage={0}> + {sections.map((section, i) => ( + {section.component} + ))} + + + ) +} + +function TopicScreenTab({ + topic, + sort, + active, +}: { + topic: string + sort: 'top' | 'latest' + active: boolean +}) { + const {_} = useLingui() + const initialNumToRender = useInitialNumToRender() + const [isPTR, setIsPTR] = React.useState(false) + + const { + data, + isFetched, + isFetchingNextPage, + isLoading, + isError, + error, + refetch, + fetchNextPage, + hasNextPage, + } = useSearchPostsQuery({ + query: decodeURIComponent(topic), + sort, + enabled: active, + }) + + const posts = React.useMemo(() => { + return data?.pages.flatMap(page => page.posts) || [] + }, [data]) + + const onRefresh = React.useCallback(async () => { + setIsPTR(true) + await refetch() + setIsPTR(false) + }, [refetch]) + + const onEndReached = React.useCallback(() => { + if (isFetchingNextPage || !hasNextPage || error) return + fetchNextPage() + }, [isFetchingNextPage, hasNextPage, error, fetchNextPage]) + + return ( + <> + {posts.length < 1 ? ( + + ) : ( + + } + initialNumToRender={initialNumToRender} + windowSize={11} + /> + )} + + ) +} diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts index f70d774630..0a9e5b2c07 100644 --- a/src/state/persisted/schema.ts +++ b/src/state/persisted/schema.ts @@ -125,6 +125,7 @@ const schema = z.object({ subtitlesEnabled: z.boolean().optional(), /** @deprecated */ mutedThreads: z.array(z.string()), + trendingDisabled: z.boolean().optional(), }) export type Schema = z.infer @@ -170,6 +171,7 @@ export const defaults: Schema = { kawaii: false, hasCheckedForStarterPack: false, subtitlesEnabled: true, + trendingDisabled: false, } export function tryParse(rawData: string): Schema | undefined { diff --git a/src/state/preferences/index.tsx b/src/state/preferences/index.tsx index c7eaf27261..8530a8d0c8 100644 --- a/src/state/preferences/index.tsx +++ b/src/state/preferences/index.tsx @@ -10,6 +10,7 @@ import {Provider as KawaiiProvider} from './kawaii' import {Provider as LanguagesProvider} from './languages' import {Provider as LargeAltBadgeProvider} from './large-alt-badge' import {Provider as SubtitlesProvider} from './subtitles' +import {Provider as TrendingSettingsProvider} from './trending' import {Provider as UsedStarterPacksProvider} from './used-starter-packs' export { @@ -39,7 +40,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) { - {children} + + {children} + diff --git a/src/state/preferences/trending.tsx b/src/state/preferences/trending.tsx new file mode 100644 index 0000000000..bf5d8f13cc --- /dev/null +++ b/src/state/preferences/trending.tsx @@ -0,0 +1,69 @@ +import React from 'react' + +import * as persisted from '#/state/persisted' + +type StateContext = { + trendingDisabled: Exclude +} +type ApiContext = { + setTrendingDisabled( + hidden: Exclude, + ): void +} + +const StateContext = React.createContext({ + trendingDisabled: Boolean(persisted.defaults.trendingDisabled), +}) +const ApiContext = React.createContext({ + setTrendingDisabled() {}, +}) + +function usePersistedBooleanValue(key: T) { + const [value, _set] = React.useState(() => { + return Boolean(persisted.get(key)) + }) + const set = React.useCallback< + (value: Exclude) => void + >( + hidden => { + _set(Boolean(hidden)) + persisted.write(key, hidden) + }, + [key, _set], + ) + React.useEffect(() => { + return persisted.onUpdate(key, hidden => { + _set(Boolean(hidden)) + }) + }, [key, _set]) + + return [value, set] as const +} + +export function Provider({children}: React.PropsWithChildren<{}>) { + const [trendingDisabled, setTrendingDisabled] = + usePersistedBooleanValue('trendingDisabled') + + /* + * Context + */ + const state = React.useMemo(() => ({trendingDisabled}), [trendingDisabled]) + const api = React.useMemo( + () => ({setTrendingDisabled}), + [setTrendingDisabled], + ) + + return ( + + {children} + + ) +} + +export function useTrendingSettings() { + return React.useContext(StateContext) +} + +export function useTrendingSettingsApi() { + return React.useContext(ApiContext) +} diff --git a/src/state/queries/index.ts b/src/state/queries/index.ts index 0635bf316b..d4b9d94c45 100644 --- a/src/state/queries/index.ts +++ b/src/state/queries/index.ts @@ -6,6 +6,7 @@ export const STALE = { MINUTES: { ONE: 1e3 * 60, FIVE: 1e3 * 60 * 5, + THIRTY: 1e3 * 60 * 30, }, HOURS: { ONE: 1e3 * 60 * 60, diff --git a/src/state/queries/service-config.ts b/src/state/queries/service-config.ts new file mode 100644 index 0000000000..9a9db78659 --- /dev/null +++ b/src/state/queries/service-config.ts @@ -0,0 +1,32 @@ +import {useQuery} from '@tanstack/react-query' + +import {STALE} from '#/state/queries' +import {useAgent} from '#/state/session' + +type ServiceConfig = { + checkEmailConfirmed: boolean + topicsEnabled: boolean +} + +export function useServiceConfigQuery() { + const agent = useAgent() + return useQuery({ + refetchOnWindowFocus: true, + staleTime: STALE.MINUTES.FIVE, + queryKey: ['service-config'], + queryFn: async () => { + try { + const {data} = await agent.api.app.bsky.unspecced.getConfig() + return { + checkEmailConfirmed: Boolean(data.checkEmailConfirmed), + topicsEnabled: Boolean(data.topicsEnabled), + } + } catch (e) { + return { + checkEmailConfirmed: false, + topicsEnabled: false, + } + } + }, + }) +} diff --git a/src/state/queries/trending/useTrendingTopics.ts b/src/state/queries/trending/useTrendingTopics.ts new file mode 100644 index 0000000000..310f64e9f2 --- /dev/null +++ b/src/state/queries/trending/useTrendingTopics.ts @@ -0,0 +1,49 @@ +import React from 'react' +import {AppBskyUnspeccedDefs} from '@atproto/api' +import {hasMutedWord} from '@atproto/api/dist/moderation/mutewords' +import {useQuery} from '@tanstack/react-query' + +import {STALE} from '#/state/queries' +import {usePreferencesQuery} from '#/state/queries/preferences' +import {useAgent} from '#/state/session' + +export type TrendingTopic = AppBskyUnspeccedDefs.TrendingTopic + +export const DEFAULT_LIMIT = 14 + +export const trendingTopicsQueryKey = ['trending-topics'] + +export function useTrendingTopics() { + const agent = useAgent() + const {data: preferences} = usePreferencesQuery() + const mutedWords = React.useMemo(() => { + return preferences?.moderationPrefs?.mutedWords || [] + }, [preferences?.moderationPrefs]) + + return useQuery({ + refetchOnWindowFocus: true, + staleTime: STALE.MINUTES.THIRTY, + queryKey: trendingTopicsQueryKey, + async queryFn() { + const {data} = await agent.api.app.bsky.unspecced.getTrendingTopics({ + limit: DEFAULT_LIMIT, + }) + + const {topics, suggested} = data + return { + topics: topics.filter(t => { + return !hasMutedWord({ + mutedWords, + text: t.topic + ' ' + t.displayName + ' ' + t.description, + }) + }), + suggested: suggested.filter(t => { + return !hasMutedWord({ + mutedWords, + text: t.topic + ' ' + t.displayName + ' ' + t.description, + }) + }), + } + }, + }) +} diff --git a/src/state/trending-config.tsx b/src/state/trending-config.tsx new file mode 100644 index 0000000000..a7694993fb --- /dev/null +++ b/src/state/trending-config.tsx @@ -0,0 +1,70 @@ +import React from 'react' + +import {useGate} from '#/lib/statsig/statsig' +import {useLanguagePrefs} from '#/state/preferences/languages' +import {useServiceConfigQuery} from '#/state/queries/service-config' +import {device} from '#/storage' + +type Context = { + enabled: boolean +} + +const Context = React.createContext({ + enabled: false, +}) + +export function Provider({children}: React.PropsWithChildren<{}>) { + const gate = useGate() + const langPrefs = useLanguagePrefs() + const {data: config, isLoading: isInitialLoad} = useServiceConfigQuery() + const ctx = React.useMemo(() => { + if (__DEV__) { + return {enabled: true} + } + + /* + * Only English during beta period + */ + if ( + !!langPrefs.contentLanguages.length && + !langPrefs.contentLanguages.includes('en') + ) { + return {enabled: false} + } + + /* + * While loading, use cached value + */ + const cachedEnabled = device.get(['trendingBetaEnabled']) + if (isInitialLoad) { + return {enabled: Boolean(cachedEnabled)} + } + + /* + * Doing an extra check here to reduce hits to statsig. If it's disabled on + * the server, we can exit early. + */ + const enabled = Boolean(config?.topicsEnabled) + if (!enabled) { + // cache for next reload + device.set(['trendingBetaEnabled'], enabled) + return {enabled: false} + } + + /* + * Service is enabled, but also check statsig in case we're rolling back. + */ + const gateEnabled = gate('trending_topics_beta') + const _enabled = enabled && gateEnabled + + // update cache + device.set(['trendingBetaEnabled'], _enabled) + + return {enabled: _enabled} + }, [isInitialLoad, config, gate, langPrefs.contentLanguages]) + return {children} +} + +export function useTrendingConfig() { + return React.useContext(Context) +} diff --git a/src/storage/schema.ts b/src/storage/schema.ts index cf410c77de..cfca9131c7 100644 --- a/src/storage/schema.ts +++ b/src/storage/schema.ts @@ -8,4 +8,5 @@ export type Device = { geolocation?: { countryCode: string | undefined } + trendingBetaEnabled: boolean } diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 10eb47d0a4..7860d568d5 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -23,6 +23,7 @@ import {logger} from '#/logger' import {isIOS, isWeb} from '#/platform/detection' import {listenPostCreated} from '#/state/events' import {useFeedFeedbackContext} from '#/state/feed-feedback' +import {useTrendingSettings} from '#/state/preferences/trending' import {STALE} from '#/state/queries' import { FeedDescriptor, @@ -34,7 +35,9 @@ import { } from '#/state/queries/post-feed' import {useSession} from '#/state/session' import {useProgressGuide} from '#/state/shell/progress-guide' +import {useBreakpoints} from '#/alf' import {ProgressGuide, SuggestedFollows} from '#/components/FeedInterstitials' +import {TrendingInterstitial} from '#/components/interstitials/Trending' import {List, ListRef} from '../util/List' import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' @@ -90,6 +93,10 @@ type FeedRow = type: 'interstitialProgressGuide' key: string } + | { + type: 'interstitialTrending' + key: string + } export function getFeedPostSlice(feedRow: FeedRow): FeedPostSlice | null { if (feedRow.type === 'sliceItem') { @@ -156,6 +163,7 @@ let PostFeed = ({ const checkForNewRef = React.useRef<(() => void) | null>(null) const lastFetchRef = React.useRef(Date.now()) const [feedType, feedUri, feedTab] = feed.split('|') + const {gtTablet} = useBreakpoints() const opts = React.useMemo( () => ({enabled, ignoreFilterFor}), @@ -259,6 +267,8 @@ let PostFeed = ({ const showProgressIntersitial = (followProgressGuide || followAndLikeProgressGuide) && !isDesktop + const {trendingDisabled} = useTrendingSettings() + const feedItems: FeedRow[] = React.useMemo(() => { let feedKind: 'following' | 'discover' | 'profile' | undefined if (feedType === 'following') { @@ -304,7 +314,16 @@ let PostFeed = ({ type: 'interstitialProgressGuide', key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt, }) - } else if (sliceIndex === 20) { + } else if ( + sliceIndex === 15 && + !gtTablet && + !trendingDisabled + ) { + arr.push({ + type: 'interstitialTrending', + key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt, + }) + } else if (sliceIndex === 30) { arr.push({ type: 'interstitialFollows', key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt, @@ -390,6 +409,8 @@ let PostFeed = ({ feedTab, hasSession, showProgressIntersitial, + trendingDisabled, + gtTablet, ]) // events @@ -476,6 +497,8 @@ let PostFeed = ({ return } else if (row.type === 'interstitialProgressGuide') { return + } else if (row.type === 'interstitialTrending') { + return } else if (row.type === 'sliceItem') { const slice = row.slice if (slice.isFallbackMarker) { diff --git a/src/view/screens/Search/Explore.tsx b/src/view/screens/Search/Explore.tsx index bd2ebe5d5d..378ea59a4d 100644 --- a/src/view/screens/Search/Explore.tsx +++ b/src/view/screens/Search/Explore.tsx @@ -24,6 +24,8 @@ import { ProfileCardFeedLoadingPlaceholder, } from '#/view/com/util/LoadingPlaceholder' import {UserAvatar} from '#/view/com/util/UserAvatar' +import {ExploreRecommendations} from '#/screens/Search/components/ExploreRecommendations' +import {ExploreTrendingTopics} from '#/screens/Search/components/ExploreTrendingTopics' import {atoms as a, useTheme, ViewStyleProp} from '#/alf' import {Button} from '#/components/Button' import * as FeedCard from '#/components/FeedCard' @@ -239,6 +241,14 @@ type ExploreScreenItems = style?: ViewStyleProp['style'] icon: React.ComponentType } + | { + type: 'trendingTopics' + key: string + } + | { + type: 'recommendations' + key: string + } | { type: 'profile' key: string @@ -325,17 +335,27 @@ export function Explore() { ]) const items = React.useMemo(() => { - const i: ExploreScreenItems[] = [ - { - type: 'header', - key: 'suggested-follows-header', - title: _(msg`Suggested accounts`), - description: _( - msg`Follow more accounts to get connected to your interests and build your network.`, - ), - icon: Person, - }, - ] + const i: ExploreScreenItems[] = [] + + i.push({ + type: 'trendingTopics', + key: `trending-topics`, + }) + + i.push({ + type: 'recommendations', + key: `recommendations`, + }) + + i.push({ + type: 'header', + key: 'suggested-follows-header', + title: _(msg`Suggested accounts`), + description: _( + msg`Follow more accounts to get connected to your interests and build your network.`, + ), + icon: Person, + }) if (profiles) { // Currently the responses contain duplicate items. @@ -490,6 +510,12 @@ export function Explore() { /> ) } + case 'trendingTopics': { + return + } + case 'recommendations': { + return + } case 'profile': { return ( diff --git a/src/view/shell/desktop/Feeds.tsx b/src/view/shell/desktop/Feeds.tsx index 83b5420ce7..1d515df558 100644 --- a/src/view/shell/desktop/Feeds.tsx +++ b/src/view/shell/desktop/Feeds.tsx @@ -14,7 +14,7 @@ import {createStaticClick, InlineLinkText} from '#/components/Link' export function DesktopFeeds() { const t = useTheme() const {_} = useLingui() - const {data: pinnedFeedInfos} = usePinnedFeedsInfos() + const {data: pinnedFeedInfos, error, isLoading} = usePinnedFeedsInfos() const selectedFeed = useSelectedFeed() const setSelectedFeed = useSetSelectedFeed() const navigation = useNavigation() @@ -25,14 +25,40 @@ export function DesktopFeeds() { return getCurrentRoute(state) }) - if (!pinnedFeedInfos) { + if (isLoading) { + return ( + + {Array(5) + .fill(0) + .map((_, i) => ( + + ))} + + ) + } + + if (error || !pinnedFeedInfos) { return null } return ( >({}) + + React.useEffect(() => { + return navigation.addListener('state', e => { + try { + const {state} = e.data + const lastRoute = state.routes[state.routes.length - 1] + const {params} = lastRoute + setParams(params) + } catch (e) {} + }) + }, [navigation, setParams]) + + return params +} + export function DesktopRightNav({routeName}: {routeName: string}) { const t = useTheme() const {_} = useLingui() const {hasSession, currentAccount} = useSession() const kawaii = useKawaiiMode() const gutters = useGutters(['base', 0, 'base', 'wide']) + const isSearchScreen = routeName === 'Search' + const webqueryParams = useWebQueryParams() + const searchQuery = webqueryParams?.q + const showTrending = !isSearchScreen || (isSearchScreen && !!searchQuery) const {isTablet} = useWebMediaQueries() if (isTablet) { @@ -29,6 +55,7 @@ export function DesktopRightNav({routeName}: {routeName: string}) { - {routeName !== 'Search' && ( - - - - )} + {!isSearchScreen && } + {hasSession && ( <> - - - - + + + )} + {showTrending && } + {hasSession && ( <> diff --git a/src/view/shell/desktop/SidebarTrendingTopics.tsx b/src/view/shell/desktop/SidebarTrendingTopics.tsx new file mode 100644 index 0000000000..e22fad54d7 --- /dev/null +++ b/src/view/shell/desktop/SidebarTrendingTopics.tsx @@ -0,0 +1,104 @@ +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import { + useTrendingSettings, + useTrendingSettingsApi, +} from '#/state/preferences/trending' +import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics' +import {useTrendingConfig} from '#/state/trending-config' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonIcon} from '#/components/Button' +import {Divider} from '#/components/Divider' +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' + +const TRENDING_LIMIT = 6 + +export function SidebarTrendingTopics() { + const {enabled} = useTrendingConfig() + const {trendingDisabled} = useTrendingSettings() + return !enabled ? null : trendingDisabled ? null : +} + +function Inner() { + const t = useTheme() + const {_} = useLingui() + const trendingPrompt = Prompt.usePromptControl() + const {setTrendingDisabled} = useTrendingSettingsApi() + const {data: trending, error, isLoading} = useTrendingTopics() + const noTopics = !isLoading && !error && !trending?.topics?.length + + return error || noTopics ? null : ( + <> + + + + + Trending + + + + + + {isLoading ? ( + Array(TRENDING_LIMIT) + .fill(0) + .map((_n, i) => ( + + )) + ) : !trending?.topics ? null : ( + <> + {trending.topics.slice(0, TRENDING_LIMIT).map(topic => ( + + {({hovered}) => ( + + )} + + ))} + + )} + + + setTrendingDisabled(true)} + /> + + + ) +} diff --git a/yarn.lock b/yarn.lock index f6598f8b8d..61ed0f66c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -72,6 +72,20 @@ tlds "^1.234.0" zod "^3.23.8" +"@atproto/api@^0.13.21": + version "0.13.21" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.21.tgz#8ee27a07e5a024b5bf32408d9bd623dd598ad1cc" + integrity sha512-iOxSj2YS3Fx9IPz1NivKrSsdYPNbBgpnUH7+WhKYAMvDFDUe2PZe7taau8wsUjJAu/H3S0Mk2TDh5e/7tCRwHA== + dependencies: + "@atproto/common-web" "^0.3.1" + "@atproto/lexicon" "^0.4.4" + "@atproto/syntax" "^0.3.1" + "@atproto/xrpc" "^0.6.5" + await-lock "^2.2.2" + multiformats "^9.9.0" + tlds "^1.234.0" + zod "^3.23.8" + "@atproto/aws@^0.2.10": version "0.2.10" resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.10.tgz#e0b888fd50308cc24b7086cf3ec209587c13bbe4" @@ -3247,7 +3261,7 @@ "@babel/parser" "^7.25.9" "@babel/types" "^7.25.9" -"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3": +"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3", "@babel/traverse@^7.25.3", "@babel/traverse@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84" integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw== @@ -3308,19 +3322,6 @@ debug "^4.3.1" globals "^11.1.0" -"@babel/traverse@^7.25.3", "@babel/traverse@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84" - integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw== - dependencies: - "@babel/code-frame" "^7.25.9" - "@babel/generator" "^7.25.9" - "@babel/parser" "^7.25.9" - "@babel/template" "^7.25.9" - "@babel/types" "^7.25.9" - debug "^4.3.1" - globals "^11.1.0" - "@babel/types@^7.0.0", "@babel/types@^7.20.0", "@babel/types@^7.20.7", "@babel/types@^7.22.10", "@babel/types@^7.22.5", "@babel/types@^7.3.3", "@babel/types@^7.4.4": version "7.22.10" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.10.tgz#4a9e76446048f2c66982d1a989dd12b8a2d2dc03" @@ -17456,16 +17457,7 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -17565,7 +17557,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -17579,13 +17571,6 @@ strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -18860,7 +18845,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -18878,15 +18863,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From ff02868afc959087c2d7dfd1507572db90f08437 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 17 Dec 2024 19:50:59 -0800 Subject: [PATCH 57/58] Layout tweaks (#7150) * Reduce weight of right sidebar active feed * ProfileFeedHeader tweaks --------- Co-authored-by: Eric Bailey --- .../Profile/components/ProfileFeedHeader.tsx | 31 +++++++++++-------- src/view/shell/desktop/Feeds.tsx | 2 +- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/screens/Profile/components/ProfileFeedHeader.tsx b/src/screens/Profile/components/ProfileFeedHeader.tsx index 6bfbff3a08..cf305ac4dd 100644 --- a/src/screens/Profile/components/ProfileFeedHeader.tsx +++ b/src/screens/Profile/components/ProfileFeedHeader.tsx @@ -30,8 +30,8 @@ import * as Dialog from '#/components/Dialog' import {Divider} from '#/components/Divider' import {useRichText} from '#/components/hooks/useRichText' import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' -import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' +import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid' import { Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled, Heart2_Stroke2_Corner0_Rounded as Heart, @@ -91,7 +91,7 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { const t = useTheme() const {_, i18n} = useLingui() const {hasSession} = useSession() - const {gtPhone, gtMobile} = useBreakpoints() + const {gtMobile} = useBreakpoints() const infoControl = Dialog.useDialogControl() const playHaptic = useHaptics() @@ -191,9 +191,8 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { style={[ a.justify_start, { - paddingVertical: isWeb ? 4 : 6, - paddingHorizontal: 8, - paddingRight: 12, + paddingVertical: isWeb ? 2 : 4, + paddingRight: 8, }, ]} onPress={() => { @@ -207,10 +206,18 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { a.absolute, a.inset_0, a.rounded_sm, - a.transition_transform, + a.transition_all, t.atoms.bg_contrast_25, - pressed && t.atoms.bg_contrast_50, + { + opacity: 0, + left: isWeb ? -2 : -4, + right: 0, + }, + pressed && { + opacity: 1, + }, hovered && { + opacity: 1, transform: [{scaleX: 1.01}, {scaleY: 1.1}], }, ]} @@ -219,7 +226,7 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { {info.avatar && ( - + )} @@ -237,10 +244,9 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { {sanitizeHandle(info.creatorHandle, '@')} @@ -256,10 +262,9 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { /> {formatCount(i18n, likeCount)} @@ -268,7 +273,7 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { - diff --git a/src/view/shell/desktop/Feeds.tsx b/src/view/shell/desktop/Feeds.tsx index 1d515df558..7a56722cc1 100644 --- a/src/view/shell/desktop/Feeds.tsx +++ b/src/view/shell/desktop/Feeds.tsx @@ -88,7 +88,7 @@ export function DesktopFeeds() { a.text_md, a.leading_snug, current - ? [a.font_heavy, t.atoms.text] + ? [a.font_bold, t.atoms.text] : [t.atoms.text_contrast_medium], ]} numberOfLines={1}> From 3262b8342026388a31547645c8543068cada6daf Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 17 Dec 2024 21:29:45 -0800 Subject: [PATCH 58/58] Fix tests --- __e2e__/flows/curate-lists.yml | 8 ++++---- __e2e__/flows/feed-reorder.yml | 24 ++++++++-------------- __e2e__/flows/home-screen.yml | 4 +++- __e2e__/flows/mod-lists.yml | 2 +- __e2e__/setupApp.yml | 2 ++ src/lib/notifications/notifications.e2e.ts | 11 ++++++++++ 6 files changed, 29 insertions(+), 22 deletions(-) create mode 100644 src/lib/notifications/notifications.e2e.ts diff --git a/__e2e__/flows/curate-lists.yml b/__e2e__/flows/curate-lists.yml index 662ec84233..41c3f2c7cc 100644 --- a/__e2e__/flows/curate-lists.yml +++ b/__e2e__/flows/curate-lists.yml @@ -26,7 +26,7 @@ appId: xyz.blueskyweb.app - tapOn: "Save" - assertNotVisible: id: "createOrEditListModal" -- tapOn: "About" +- tapOn: "People" - assertVisible: "Good Ppl" - assertVisible: "They good" @@ -93,10 +93,10 @@ appId: xyz.blueskyweb.app - tapOn: "Save" - assertNotVisible: id: "createOrEditListModal" -- tapOn: "About" +- tapOn: "People" - assertVisible: "Good Ppl" - assertVisible: "They good" -- tapOn: "About" +- tapOn: "People" - tapOn: label: "Adds users on curatelists from the list" @@ -145,7 +145,7 @@ appId: xyz.blueskyweb.app id: "e2eGotoLists" - tapOn: "Good Ppl" -- tapOn: "About" +- tapOn: "People" - assertVisible: label: "Removes users on curatelists from the list" id: "user-bob.test" diff --git a/__e2e__/flows/feed-reorder.yml b/__e2e__/flows/feed-reorder.yml index c502373c42..6e37321221 100644 --- a/__e2e__/flows/feed-reorder.yml +++ b/__e2e__/flows/feed-reorder.yml @@ -11,10 +11,8 @@ appId: xyz.blueskyweb.app # Pin alice's feed - extendedWaitUntil: - visible: - id: "viewHeaderDrawerBtn" -- tapOn: - id: "viewHeaderDrawerBtn" + visible: "Open drawer menu" +- tapOn: "Open drawer menu" - tapOn: id: "profileCardButton" - tapOn: @@ -32,8 +30,7 @@ appId: xyz.blueskyweb.app text: "alice-favs" # Set alice-favs first -- tapOn: - id: "viewHeaderDrawerBtn" +- tapOn: "Open drawer menu" - tapOn: id: "menuItemButton-Feeds" - tapOn: @@ -44,8 +41,7 @@ appId: xyz.blueskyweb.app - tapOn: label: "Save button" id: "saveChangesBtn" -- tapOn: - id: "viewHeaderDrawerBtn" +- tapOn: "Go back" - assertVisible: id: "homeScreenFeedTabs-selector-0" text: "alice-favs" @@ -54,8 +50,7 @@ appId: xyz.blueskyweb.app text: "Following" # Set following first -- tapOn: - id: "viewHeaderDrawerBtn" +- tapOn: "Open drawer menu" - tapOn: id: "menuItemButton-Feeds" - tapOn: @@ -66,8 +61,7 @@ appId: xyz.blueskyweb.app - tapOn: label: "Save button" id: "saveChangesBtn" -- tapOn: - id: "viewHeaderDrawerBtn" +- tapOn: "Go back" - assertVisible: id: "homeScreenFeedTabs-selector-0" text: "Following" @@ -76,8 +70,7 @@ appId: xyz.blueskyweb.app text: "alice-favs" # Remove following -- tapOn: - id: "viewHeaderDrawerBtn" +- tapOn: "Open drawer menu" - tapOn: id: "menuItemButton-Feeds" - tapOn: @@ -88,8 +81,7 @@ appId: xyz.blueskyweb.app - tapOn: label: "Save button" id: "saveChangesBtn" -- tapOn: - id: "viewHeaderDrawerBtn" +- tapOn: "Go back" - assertVisible: id: "homeScreenFeedTabs-selector-0" text: "alice-favs" diff --git a/__e2e__/flows/home-screen.yml b/__e2e__/flows/home-screen.yml index b7e96282d7..e818168065 100644 --- a/__e2e__/flows/home-screen.yml +++ b/__e2e__/flows/home-screen.yml @@ -27,7 +27,9 @@ appId: xyz.blueskyweb.app - tapOn: id: "bottomBarHomeBtn" - tapOn: - id: "viewHeaderDrawerBtn" + id: "bottomBarHomeBtn" +- tapOn: + id: "bottomBarHomeBtn" - assertNotVisible: "Feeds ✨" - tapOn: diff --git a/__e2e__/flows/mod-lists.yml b/__e2e__/flows/mod-lists.yml index 54832a07eb..ef757c5b11 100644 --- a/__e2e__/flows/mod-lists.yml +++ b/__e2e__/flows/mod-lists.yml @@ -14,7 +14,7 @@ appId: xyz.blueskyweb.app id: "e2eGotoModeration" - tapOn: id: "moderationlistsBtn" -- tapOn: "New" +- tapOn: "New list" - tapOn: id: "editNameInput" - inputText: "Muted Users" diff --git a/__e2e__/setupApp.yml b/__e2e__/setupApp.yml index 8c3ffd2d3b..25f9aa8470 100644 --- a/__e2e__/setupApp.yml +++ b/__e2e__/setupApp.yml @@ -6,6 +6,8 @@ appId: xyz.blueskyweb.app - waitForAnimationToEnd - tapOn: "http://localhost:8081" - waitForAnimationToEnd +- extendedWaitUntil: + visible: "Continue" - swipe: from: "Bluesky" direction: DOWN diff --git a/src/lib/notifications/notifications.e2e.ts b/src/lib/notifications/notifications.e2e.ts new file mode 100644 index 0000000000..0586ac1bf8 --- /dev/null +++ b/src/lib/notifications/notifications.e2e.ts @@ -0,0 +1,11 @@ +export function useNotificationsRegistration() {} + +export function useRequestNotificationsPermission() { + return async ( + _context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' | 'Home', + ) => {} +} + +export async function decrementBadgeCount(_by: number) {} + +export async function resetBadgeCount() {}