From 8a6d83de3b5723497e2bbebf10290cde15cfe1d7 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 11 Sep 2024 23:04:40 +0100 Subject: [PATCH 01/86] make container relative (#5280) --- bskyembed/src/components/embed.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bskyembed/src/components/embed.tsx b/bskyembed/src/components/embed.tsx index 3b4f5e77..1ed107b5 100644 --- a/bskyembed/src/components/embed.tsx +++ b/bskyembed/src/components/embed.tsx @@ -372,7 +372,7 @@ function VideoEmbed({content}: {content: AppBskyEmbedVideo.View}) { return (
Date: Thu, 12 Sep 2024 00:42:21 +0200 Subject: [PATCH 02/86] remove double closing tag (#5257) --- bskyweb/templates/base.html | 1 - 1 file changed, 1 deletion(-) diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html index c2480279..aa7efc5e 100644 --- a/bskyweb/templates/base.html +++ b/bskyweb/templates/base.html @@ -259,7 +259,6 @@ pointer-events: none !important; } - {% include "scripts.html" %} From cff7cbb4aa0a945399f4d44bb56a12ae0ed27278 Mon Sep 17 00:00:00 2001 From: Eduardo Tachotte <58338880+0xEDU@users.noreply.github.com> Date: Wed, 11 Sep 2024 20:28:23 -0300 Subject: [PATCH 03/86] Add autoCapitalize to password field (#5216) --- src/screens/Signup/StepInfo/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/screens/Signup/StepInfo/index.tsx b/src/screens/Signup/StepInfo/index.tsx index 47fb4c70..e0a7912f 100644 --- a/src/screens/Signup/StepInfo/index.tsx +++ b/src/screens/Signup/StepInfo/index.tsx @@ -172,6 +172,7 @@ export function StepInfo({ defaultValue={state.password} secureTextEntry autoComplete="new-password" + autoCapitalize="none" /> From ae71f5ce84165b683b880c4a585b5a617f2c36bb Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 11 Sep 2024 19:56:00 -0500 Subject: [PATCH 04/86] NUX API (#5278) * Set up nux API * Bump SDK * Naming * Imports --- package.json | 2 +- src/state/queries/nuxs/definitions.ts | 29 +++++++++ src/state/queries/nuxs/index.ts | 83 ++++++++++++++++++++++++++ src/state/queries/nuxs/types.ts | 9 +++ src/state/queries/nuxs/util.ts | 52 ++++++++++++++++ src/state/queries/preferences/const.ts | 1 + yarn.lock | 35 +++++------ 7 files changed, 193 insertions(+), 18 deletions(-) create mode 100644 src/state/queries/nuxs/definitions.ts create mode 100644 src/state/queries/nuxs/index.ts create mode 100644 src/state/queries/nuxs/types.ts create mode 100644 src/state/queries/nuxs/util.ts diff --git a/package.json b/package.json index eff665a6..92b6cfe1 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web" }, "dependencies": { - "@atproto/api": "0.13.5", + "@atproto/api": "^0.13.7", "@bam.tech/react-native-image-resizer": "^3.0.4", "@braintree/sanitize-url": "^6.0.2", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", diff --git a/src/state/queries/nuxs/definitions.ts b/src/state/queries/nuxs/definitions.ts new file mode 100644 index 00000000..c5cb1e9d --- /dev/null +++ b/src/state/queries/nuxs/definitions.ts @@ -0,0 +1,29 @@ +import zod from 'zod' + +import {BaseNux} from '#/state/queries/nuxs/types' + +export enum Nux { + One = 'one', + Two = 'two', +} + +export const nuxNames = new Set(Object.values(Nux)) + +export type AppNux = + | BaseNux<{ + id: Nux.One + data: { + likes: number + } + }> + | BaseNux<{ + id: Nux.Two + data: undefined + }> + +export const NuxSchemas = { + [Nux.One]: zod.object({ + likes: zod.number(), + }), + [Nux.Two]: undefined, +} diff --git a/src/state/queries/nuxs/index.ts b/src/state/queries/nuxs/index.ts new file mode 100644 index 00000000..2945e67e --- /dev/null +++ b/src/state/queries/nuxs/index.ts @@ -0,0 +1,83 @@ +import {useMutation, useQueryClient} from '@tanstack/react-query' + +import {AppNux, Nux} from '#/state/queries/nuxs/definitions' +import {parseAppNux, serializeAppNux} from '#/state/queries/nuxs/util' +import { + preferencesQueryKey, + usePreferencesQuery, +} from '#/state/queries/preferences' +import {useAgent} from '#/state/session' + +export {Nux} from '#/state/queries/nuxs/definitions' + +export function useNuxs() { + const {data, ...rest} = usePreferencesQuery() + + if (data && rest.isSuccess) { + const nuxs = data.bskyAppState.nuxs + ?.map(parseAppNux) + ?.filter(Boolean) as AppNux[] + + if (nuxs) { + return { + nuxs, + ...rest, + } + } + } + + return { + nuxs: undefined, + ...rest, + } +} + +export function useNux(id: T) { + const {nuxs, ...rest} = useNuxs() + + if (nuxs && rest.isSuccess) { + const nux = nuxs.find(nux => nux.id === id) + + if (nux) { + return { + nux: nux as Extract, + ...rest, + } + } + } + + return { + nux: undefined, + ...rest, + } +} + +export function useUpsertNuxMutation() { + const queryClient = useQueryClient() + const agent = useAgent() + + return useMutation({ + mutationFn: async (nux: AppNux) => { + await agent.bskyAppUpsertNux(serializeAppNux(nux)) + // triggers a refetch + await queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + }) + }, + }) +} + +export function useRemoveNuxsMutation() { + const queryClient = useQueryClient() + const agent = useAgent() + + return useMutation({ + mutationFn: async (ids: string[]) => { + await agent.bskyAppRemoveNuxs(ids) + // triggers a refetch + await queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + }) + }, + }) +} diff --git a/src/state/queries/nuxs/types.ts b/src/state/queries/nuxs/types.ts new file mode 100644 index 00000000..5b791847 --- /dev/null +++ b/src/state/queries/nuxs/types.ts @@ -0,0 +1,9 @@ +import {AppBskyActorDefs} from '@atproto/api' + +export type Data = Record | undefined + +export type BaseNux< + T extends Pick & {data: Data}, +> = T & { + completed: boolean +} diff --git a/src/state/queries/nuxs/util.ts b/src/state/queries/nuxs/util.ts new file mode 100644 index 00000000..d65b86a3 --- /dev/null +++ b/src/state/queries/nuxs/util.ts @@ -0,0 +1,52 @@ +import {AppBskyActorDefs, nuxSchema} from '@atproto/api' + +import { + AppNux, + Nux, + nuxNames, + NuxSchemas, +} from '#/state/queries/nuxs/definitions' + +export function parseAppNux(nux: AppBskyActorDefs.Nux): AppNux | undefined { + if (!nuxNames.has(nux.id as Nux)) return + if (!nuxSchema.safeParse(nux).success) return + + const {data, ...rest} = nux + + const schema = NuxSchemas[nux.id as Nux] + + if (schema && data) { + const parsedData = JSON.parse(data) + + if (!schema.safeParse(parsedData).success) return + + return { + ...rest, + data: parsedData, + } as AppNux + } + + return { + ...rest, + data: undefined, + } as AppNux +} + +export function serializeAppNux(nux: AppNux): AppBskyActorDefs.Nux { + const {data, ...rest} = nux + const schema = NuxSchemas[nux.id as Nux] + + const result: AppBskyActorDefs.Nux = { + ...rest, + data: undefined, + } + + if (schema) { + schema.parse(data) + result.data = JSON.stringify(data) + } + + nuxSchema.parse(result) + + return result +} diff --git a/src/state/queries/preferences/const.ts b/src/state/queries/preferences/const.ts index 1ae7d206..e07f40ec 100644 --- a/src/state/queries/preferences/const.ts +++ b/src/state/queries/preferences/const.ts @@ -37,5 +37,6 @@ export const DEFAULT_LOGGED_OUT_PREFERENCES: UsePreferencesQueryResponse = { bskyAppState: { queuedNudges: [], activeProgressGuide: undefined, + nuxs: [], }, } diff --git a/yarn.lock b/yarn.lock index cc440109..b2e389aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -72,19 +72,6 @@ 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.5": - version "0.13.5" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.5.tgz#04305cdb0a467ba366305c5e95cebb7ce0d39735" - integrity sha512-yT/YimcKYkrI0d282Zxo7O30OSYR+KDW89f81C6oYZfDRBcShC1aniVV8kluP5LrEAg8O27yrOSnBgx2v7XPew== - dependencies: - "@atproto/common-web" "^0.3.0" - "@atproto/lexicon" "^0.4.1" - "@atproto/syntax" "^0.3.0" - "@atproto/xrpc" "^0.6.1" - await-lock "^2.2.2" - multiformats "^9.9.0" - tlds "^1.234.0" - "@atproto/api@^0.13.0": version "0.13.0" resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.0.tgz#d1c65a407f1c3c6aba5be9425f4f739a01419bd8" @@ -98,6 +85,20 @@ multiformats "^9.9.0" tlds "^1.234.0" +"@atproto/api@^0.13.7": + version "0.13.7" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.7.tgz#072eba2025d5251505f17b0b5d2de33749ea5ee4" + integrity sha512-41kSLmFWDbuPOenb52WRq1lnBkSZrL+X29tWcvEt6SZXK4xBoKAalw1MjF+oabhzff12iMtNaNvmmt2fu1L+cw== + dependencies: + "@atproto/common-web" "^0.3.0" + "@atproto/lexicon" "^0.4.1" + "@atproto/syntax" "^0.3.0" + "@atproto/xrpc" "^0.6.2" + await-lock "^2.2.2" + multiformats "^9.9.0" + tlds "^1.234.0" + zod "^3.23.8" + "@atproto/aws@^0.2.2": version "0.2.2" resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.2.tgz#703e5e06f288bcf61c6d99a990738f1e7299e653" @@ -443,10 +444,10 @@ "@atproto/lexicon" "^0.4.1" zod "^3.23.8" -"@atproto/xrpc@^0.6.1": - version "0.6.1" - resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.1.tgz#dcd1315c8c60eef5af2db7fa4e35a38ebc6d79d5" - integrity sha512-Zy5ydXEdk6sY7FDUZcEVfCL1jvbL4tXu5CcdPqbEaW6LQtk9GLds/DK1bCX9kswTGaBC88EMuqQMfkxOhp2t4A== +"@atproto/xrpc@^0.6.2": + version "0.6.2" + resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.2.tgz#634228a7e533de01bda2214837d11574fdadad55" + integrity sha512-as/gb08xJb02HAGNrSQSumCe10WnOAcnM6bR6KMatQyQJuEu7OY6ZDSTM/4HfjjoxsNqdvPmbYuoUab1bKTNlA== dependencies: "@atproto/lexicon" "^0.4.1" zod "^3.23.8" From 76c584d981f195a580e132b786e101b3d0d32380 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 9 Sep 2024 20:57:32 -0500 Subject: [PATCH 05/86] WIP --- src/App.native.tsx | 2 + src/App.web.tsx | 2 + src/components/dialogs/nudges/TenMillion.tsx | 100 +++++++++++++++++++ src/components/dialogs/nudges/index.tsx | 53 ++++++++++ src/lib/hooks/useIntentHandler.ts | 6 +- src/view/shell/Composer.web.tsx | 1 + 6 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 src/components/dialogs/nudges/TenMillion.tsx create mode 100644 src/components/dialogs/nudges/index.tsx diff --git a/src/App.native.tsx b/src/App.native.tsx index 780d4058..95625bdf 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -63,6 +63,7 @@ import {Provider as PortalProvider} from '#/components/Portal' import {Splash} from '#/Splash' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' import {AudioCategory, PlatformInfo} from '../modules/expo-bluesky-swiss-army' +import {NudgeDialogs} from '#/components/dialogs/nudges' SplashScreen.preventAutoHideAsync() @@ -131,6 +132,7 @@ function InnerApp() { style={s.h100pct}> + diff --git a/src/App.web.tsx b/src/App.web.tsx index 3017a3a2..79120ffd 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -50,6 +50,7 @@ import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' import {Provider as PortalProvider} from '#/components/Portal' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' +import {NudgeDialogs} from '#/components/dialogs/nudges' function InnerApp() { const [isReady, setIsReady] = React.useState(false) @@ -113,6 +114,7 @@ function InnerApp() { + diff --git a/src/components/dialogs/nudges/TenMillion.tsx b/src/components/dialogs/nudges/TenMillion.tsx new file mode 100644 index 00000000..9b5d5eae --- /dev/null +++ b/src/components/dialogs/nudges/TenMillion.tsx @@ -0,0 +1,100 @@ +import React from 'react' +import {useLingui} from '@lingui/react' +import {msg} from '@lingui/macro' +import {View} from 'react-native' +import ViewShot from 'react-native-view-shot' + +import {atoms as a, useBreakpoints, tokens} from '#/alf' +import * as Dialog from '#/components/Dialog' +import {Text} from '#/components/Typography' +import {GradientFill} from '#/components/GradientFill' +import {Button, ButtonText} from '#/components/Button' +import {useComposerControls} from 'state/shell' + +import {useContext} from '#/components/dialogs/nudges' + +export function TenMillion() { + const {_} = useLingui() + const {controls} = useContext() + const {gtMobile} = useBreakpoints() + const {openComposer} = useComposerControls() + + const imageRef = React.useRef(null) + + const share = () => { + if (imageRef.current && imageRef.current.capture) { + imageRef.current.capture().then(uri => { + controls.tenMillion.close(() => { + setTimeout(() => { + openComposer({ + text: '10 milly, babyyy', + imageUris: [ + { + uri, + width: 1000, + height: 1000, + }, + ], + }) + }, 1e3) + }) + }) + } + } + + return ( + + + + + + + + + + 10 milly, babyyy + + + + + + + + ) +} diff --git a/src/components/dialogs/nudges/index.tsx b/src/components/dialogs/nudges/index.tsx new file mode 100644 index 00000000..357d4e2b --- /dev/null +++ b/src/components/dialogs/nudges/index.tsx @@ -0,0 +1,53 @@ +import React from 'react' + +import * as Dialog from '#/components/Dialog' + +import {TenMillion} from '#/components/dialogs/nudges/TenMillion' + +type Context = { + controls: { + tenMillion: Dialog.DialogOuterProps['control'] + } +} + +const Context = React.createContext({ + // @ts-ignore + controls: {} +}) + +export function useContext() { + return React.useContext(Context) +} + +let SHOWN = false + +export function NudgeDialogs() { + const tenMillion = Dialog.useDialogControl() + + const ctx = React.useMemo(() => { + return { + controls: { + tenMillion + } + } + }, [tenMillion]) + + React.useEffect(() => { + const t = setTimeout(() => { + if (!SHOWN) { + SHOWN = true + ctx.controls.tenMillion.open() + } + }, 2e3) + + return () => { + clearTimeout(t) + } + }, [ctx]) + + return ( + + + + ) +} diff --git a/src/lib/hooks/useIntentHandler.ts b/src/lib/hooks/useIntentHandler.ts index 8cccda48..67f1c2c3 100644 --- a/src/lib/hooks/useIntentHandler.ts +++ b/src/lib/hooks/useIntentHandler.ts @@ -71,7 +71,7 @@ export function useIntentHandler() { }, [incomingUrl, composeIntent, verifyEmailIntent]) } -function useComposeIntent() { +export function useComposeIntent() { const closeAllActiveElements = useCloseAllActiveElements() const {openComposer} = useComposerControls() const {hasSession} = useSession() @@ -97,6 +97,10 @@ function useComposeIntent() { if (part.includes('https://') || part.includes('http://')) { return false } + console.log({ + part, + text: VALID_IMAGE_REGEX.test(part), + }) // We also should just filter out cases that don't have all the info we need return VALID_IMAGE_REGEX.test(part) }) diff --git a/src/view/shell/Composer.web.tsx b/src/view/shell/Composer.web.tsx index 42696139..ee1ed662 100644 --- a/src/view/shell/Composer.web.tsx +++ b/src/view/shell/Composer.web.tsx @@ -63,6 +63,7 @@ export function Composer({}: {winHeight: number}) { mention={state.mention} openEmojiPicker={onOpenPicker} text={state.text} + imageUris={state.imageUris} /> From 3c8b3b47823475b93a92dcf82a4cabbda625c323 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 10 Sep 2024 14:23:30 -0500 Subject: [PATCH 06/86] Progress on desktoip --- src/alf/index.tsx | 18 +- src/components/dialogs/nudges/TenMillion.tsx | 332 ++++++++++++++++--- src/view/icons/Logomark.tsx | 29 ++ 3 files changed, 332 insertions(+), 47 deletions(-) create mode 100644 src/view/icons/Logomark.tsx diff --git a/src/alf/index.tsx b/src/alf/index.tsx index 5fa7d3b1..d699de6a 100644 --- a/src/alf/index.tsx +++ b/src/alf/index.tsx @@ -18,9 +18,17 @@ export * from '#/alf/util/themeSelector' export const Context = React.createContext<{ themeName: ThemeName theme: Theme + themes: ReturnType }>({ themeName: 'light', theme: defaultTheme, + themes: createThemes({ + hues: { + primary: BLUE_HUE, + negative: RED_HUE, + positive: GREEN_HUE, + }, + }), }) export function ThemeProvider({ @@ -42,18 +50,22 @@ export function ThemeProvider({ ({ + themes, themeName: themeName, theme: theme, }), - [theme, themeName], + [theme, themeName, themes], )}> {children} ) } -export function useTheme() { - return React.useContext(Context).theme +export function useTheme(theme?: ThemeName) { + const ctx = React.useContext(Context) + return React.useMemo(() => { + return theme ? ctx.themes[theme] : ctx.theme + }, [theme, ctx]) } export function useBreakpoints() { diff --git a/src/components/dialogs/nudges/TenMillion.tsx b/src/components/dialogs/nudges/TenMillion.tsx index 9b5d5eae..86905697 100644 --- a/src/components/dialogs/nudges/TenMillion.tsx +++ b/src/components/dialogs/nudges/TenMillion.tsx @@ -1,25 +1,74 @@ import React from 'react' -import {useLingui} from '@lingui/react' -import {msg} from '@lingui/macro' import {View} from 'react-native' import ViewShot from 'react-native-view-shot' +import {moderateProfile} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' -import {atoms as a, useBreakpoints, tokens} from '#/alf' -import * as Dialog from '#/components/Dialog' -import {Text} from '#/components/Typography' -import {GradientFill} from '#/components/GradientFill' -import {Button, ButtonText} from '#/components/Button' +import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {sanitizeHandle} from '#/lib/strings/handles' +import {isNative} from '#/platform/detection' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useProfileQuery} from '#/state/queries/profile' +import {useSession} from '#/state/session' import {useComposerControls} from 'state/shell' - +import {formatCount} from '#/view/com/util/numeric/format' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import {Logomark} from '#/view/icons/Logomark' +import { + atoms as a, + ThemeProvider, + tokens, + useBreakpoints, + useTheme, +} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' import {useContext} from '#/components/dialogs/nudges' +import {Divider} from '#/components/Divider' +import {GradientFill} from '#/components/GradientFill' +import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' +import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image' +import {Loader} from '#/components/Loader' +import {Text} from '#/components/Typography' + +const RATIO = 8 / 10 +const WIDTH = 2000 +const HEIGHT = WIDTH * RATIO + +function getFontSize(count: number) { + const length = count.toString().length + if (length < 7) { + return 80 + } else if (length < 5) { + return 100 + } else { + return 70 + } +} export function TenMillion() { - const {_} = useLingui() + const t = useTheme() + const lightTheme = useTheme('light') + const {_, i18n} = useLingui() const {controls} = useContext() const {gtMobile} = useBreakpoints() const {openComposer} = useComposerControls() - const imageRef = React.useRef(null) + const {currentAccount} = useSession() + const {isLoading: isProfileLoading, data: profile} = useProfileQuery({ + did: currentAccount!.did, + }) // TODO PWI + const moderationOpts = useModerationOpts() + const moderation = React.useMemo(() => { + return profile && moderationOpts + ? moderateProfile(profile, moderationOpts) + : undefined + }, [profile, moderationOpts]) + + const isLoading = isProfileLoading || !moderation || !profile + + const userNumber = 56738 const share = () => { if (imageRef.current && imageRef.current.capture) { @@ -31,8 +80,8 @@ export function TenMillion() { imageUris: [ { uri, - width: 1000, - height: 1000, + width: WIDTH, + height: HEIGHT, }, ], }) @@ -48,52 +97,247 @@ export function TenMillion() { + style={[ + { + padding: 0, + }, + // gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full, + ]}> - + - + + + - 10 milly, babyyy + {isLoading ? ( + + ) : ( + + + + + + {/* Centered content */} + + + + Celebrating {formatCount(i18n, 10000000)} users + {' '} + 🎉 + + + + # + + {i18n.number(userNumber)} + + + {/* End centered content */} + + + + + + + {sanitizeDisplayName( + profile.displayName || + sanitizeHandle(profile.handle), + moderation.ui('displayName'), + )} + + + + {sanitizeHandle(profile.handle, '@')} + + + {profile.createdAt && ( + + {i18n.date(profile.createdAt, { + dateStyle: 'long', + })} + + )} + + + + + + )} + + - + + + + + You're part of the next wave of the internet. + + + + Online culture is too important to be controlled by a few + corporations.{' '} + + We’re dedicated to building an open foundation for the social + internet so that we can all shape its future. + + + + + + + + Brag a little ;) + + + + + + - + ) diff --git a/src/view/icons/Logomark.tsx b/src/view/icons/Logomark.tsx new file mode 100644 index 00000000..5715a1a4 --- /dev/null +++ b/src/view/icons/Logomark.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import Svg, {Path, PathProps, SvgProps} from 'react-native-svg' + +import {usePalette} from '#/lib/hooks/usePalette' + +const ratio = 54 / 61 + +export function Logomark({ + fill, + ...rest +}: {fill?: PathProps['fill']} & SvgProps) { + const pal = usePalette('default') + // @ts-ignore it's fiiiiine + const size = parseInt(rest.width || 32) + + return ( + + + + ) +} From eaf0081623154df995e81f2ae430a723539df800 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 10 Sep 2024 16:20:19 -0500 Subject: [PATCH 07/86] WIP, avi not working on web --- src/components/dialogs/nudges/TenMillion.tsx | 443 +++++++++++-------- src/lib/canvas.ts | 15 + src/view/com/util/UserAvatar.tsx | 4 + 3 files changed, 276 insertions(+), 186 deletions(-) create mode 100644 src/lib/canvas.ts diff --git a/src/components/dialogs/nudges/TenMillion.tsx b/src/components/dialogs/nudges/TenMillion.tsx index 86905697..2be5e349 100644 --- a/src/components/dialogs/nudges/TenMillion.tsx +++ b/src/components/dialogs/nudges/TenMillion.tsx @@ -1,10 +1,12 @@ import React from 'react' import {View} from 'react-native' import ViewShot from 'react-native-view-shot' +import {Image} from 'expo-image' import {moderateProfile} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {getCanvas} from '#/lib/canvas' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {isNative} from '#/platform/detection' @@ -32,6 +34,7 @@ import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Ima import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' +const DEBUG = false const RATIO = 8 / 10 const WIDTH = 2000 const HEIGHT = WIDTH * RATIO @@ -47,6 +50,22 @@ function getFontSize(count: number) { } } +function Frame({children}: {children: React.ReactNode}) { + return ( + + {children} + + ) +} + export function TenMillion() { const t = useTheme() const lightTheme = useTheme('light') @@ -54,7 +73,6 @@ export function TenMillion() { const {controls} = useContext() const {gtMobile} = useBreakpoints() const {openComposer} = useComposerControls() - const imageRef = React.useRef(null) const {currentAccount} = useSession() const {isLoading: isProfileLoading, data: profile} = useProfileQuery({ did: currentAccount!.did, @@ -65,32 +83,236 @@ export function TenMillion() { ? moderateProfile(profile, moderationOpts) : undefined }, [profile, moderationOpts]) + const [uri, setUri] = React.useState(null) - const isLoading = isProfileLoading || !moderation || !profile + const isLoadingData = isProfileLoading || !moderation || !profile + const isLoadingImage = !uri - const userNumber = 56738 + const userNumber = 56738 // TODO + + const captureInProgress = React.useRef(false) + const imageRef = React.useRef(null) const share = () => { - if (imageRef.current && imageRef.current.capture) { - imageRef.current.capture().then(uri => { - controls.tenMillion.close(() => { - setTimeout(() => { - openComposer({ - text: '10 milly, babyyy', - imageUris: [ - { - uri, - width: WIDTH, - height: HEIGHT, - }, - ], - }) - }, 1e3) - }) + if (uri) { + controls.tenMillion.close(() => { + setTimeout(() => { + openComposer({ + text: '10 milly, babyyy', + imageUris: [ + { + uri, + width: WIDTH, + height: HEIGHT, + }, + ], + }) + }, 1e3) }) } } + const onCanvasReady = async () => { + if ( + imageRef.current && + imageRef.current.capture && + !captureInProgress.current + ) { + captureInProgress.current = true + const uri = await imageRef.current.capture() + setUri(uri) + } + } + + const download = async () => { + if (uri) { + const canvas = await getCanvas(uri) + const imgHref = canvas + .toDataURL('image/png') + .replace('image/png', 'image/octet-stream') + const link = document.createElement('a') + link.setAttribute('download', `Bluesky 10M Users.png`) + link.setAttribute('href', imgHref) + link.click() + } + } + + const canvas = isLoadingData ? null : ( + + + + + + + + + + + + + + {/* Centered content */} + + + + Celebrating {formatCount(i18n, 10000000)} users + {' '} + 🎉 + + + + # + + {i18n.number(userNumber)} + + + {/* End centered content */} + + + + + + + {sanitizeDisplayName( + profile.displayName || + sanitizeHandle(profile.handle), + moderation.ui('displayName'), + )} + + + + {sanitizeHandle(profile.handle, '@')} + + + {profile.createdAt && ( + + {i18n.date(profile.createdAt, { + dateStyle: 'long', + })} + + )} + + + + + + + + + + + + ) + return ( @@ -101,7 +323,6 @@ export function TenMillion() { { padding: 0, }, - // gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full, ]}> - + - - - - - {isLoading ? ( - - ) : ( - - - - - - {/* Centered content */} - - - - Celebrating {formatCount(i18n, 10000000)} users - {' '} - 🎉 - - - - # - - {i18n.number(userNumber)} - - - {/* End centered content */} - - - - - - - {sanitizeDisplayName( - profile.displayName || - sanitizeHandle(profile.handle), - moderation.ui('displayName'), - )} - - - - {sanitizeHandle(profile.handle, '@')} - - - {profile.createdAt && ( - - {i18n.date(profile.createdAt, { - dateStyle: 'long', - })} - - )} - - - - - - )} - - + style={[a.absolute, a.inset_0, a.align_center, a.justify_center]}> + + {isLoadingData || isLoadingImage ? ( + + ) : ( + + )} - + + + {canvas} + onPress={download}> diff --git a/src/components/icons/Download.tsx b/src/components/icons/Download.tsx new file mode 100644 index 00000000..86b49428 --- /dev/null +++ b/src/components/icons/Download.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Download_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 3a1 1 0 0 1 1 1v8.086l1.793-1.793a1 1 0 1 1 1.414 1.414l-3.5 3.5a1 1 0 0 1-1.414 0l-3.5-3.5a1 1 0 1 1 1.414-1.414L11 12.086V4a1 1 0 0 1 1-1ZM4 14a1 1 0 0 1 1 1v4h14v-4a1 1 0 1 1 2 0v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1Z', +}) From 11ecea22d422138b8346dccded7dcdd70191fae4 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 10 Sep 2024 18:45:08 -0500 Subject: [PATCH 09/86] Add badges, clean up spacing --- .../nudges/TenMillion/icons/OnePercent.tsx | 15 ++++ .../TenMillion/icons/PointOnePercent.tsx | 15 ++++ .../nudges/TenMillion/icons/TenPercent.tsx | 15 ++++ .../TenMillion/icons/TwentyFivePercent.tsx | 15 ++++ .../{TenMillion.tsx => TenMillion/index.tsx} | 79 ++++++++++++++++--- 5 files changed, 126 insertions(+), 13 deletions(-) create mode 100644 src/components/dialogs/nudges/TenMillion/icons/OnePercent.tsx create mode 100644 src/components/dialogs/nudges/TenMillion/icons/PointOnePercent.tsx create mode 100644 src/components/dialogs/nudges/TenMillion/icons/TenPercent.tsx create mode 100644 src/components/dialogs/nudges/TenMillion/icons/TwentyFivePercent.tsx rename src/components/dialogs/nudges/{TenMillion.tsx => TenMillion/index.tsx} (84%) diff --git a/src/components/dialogs/nudges/TenMillion/icons/OnePercent.tsx b/src/components/dialogs/nudges/TenMillion/icons/OnePercent.tsx new file mode 100644 index 00000000..9c8d47af --- /dev/null +++ b/src/components/dialogs/nudges/TenMillion/icons/OnePercent.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import Svg, {Path} from 'react-native-svg' + +export function OnePercent({fill}: {fill?: string}) { + return ( + + + + ) +} diff --git a/src/components/dialogs/nudges/TenMillion/icons/PointOnePercent.tsx b/src/components/dialogs/nudges/TenMillion/icons/PointOnePercent.tsx new file mode 100644 index 00000000..1f9467e4 --- /dev/null +++ b/src/components/dialogs/nudges/TenMillion/icons/PointOnePercent.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import Svg, {Path} from 'react-native-svg' + +export function PointOnePercent({fill}: {fill?: string}) { + return ( + + + + ) +} diff --git a/src/components/dialogs/nudges/TenMillion/icons/TenPercent.tsx b/src/components/dialogs/nudges/TenMillion/icons/TenPercent.tsx new file mode 100644 index 00000000..4197be83 --- /dev/null +++ b/src/components/dialogs/nudges/TenMillion/icons/TenPercent.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import Svg, {Path} from 'react-native-svg' + +export function TenPercent({fill}: {fill?: string}) { + return ( + + + + ) +} diff --git a/src/components/dialogs/nudges/TenMillion/icons/TwentyFivePercent.tsx b/src/components/dialogs/nudges/TenMillion/icons/TwentyFivePercent.tsx new file mode 100644 index 00000000..0d379714 --- /dev/null +++ b/src/components/dialogs/nudges/TenMillion/icons/TwentyFivePercent.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import Svg, {Path} from 'react-native-svg' + +export function TwentyFivePercent({fill}: {fill?: string}) { + return ( + + + + ) +} diff --git a/src/components/dialogs/nudges/TenMillion.tsx b/src/components/dialogs/nudges/TenMillion/index.tsx similarity index 84% rename from src/components/dialogs/nudges/TenMillion.tsx rename to src/components/dialogs/nudges/TenMillion/index.tsx index 5aa45f21..fdac91f4 100644 --- a/src/components/dialogs/nudges/TenMillion.tsx +++ b/src/components/dialogs/nudges/TenMillion/index.tsx @@ -10,7 +10,7 @@ import {getCanvas} from '#/lib/canvas' import {shareUrl} from '#/lib/sharing' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' -import {isAndroid, isNative, isWeb} from '#/platform/detection' +import {isNative} from '#/platform/detection' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useProfileQuery} from '#/state/queries/profile' import {useSession} from '#/state/session' @@ -28,6 +28,9 @@ import { import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {useContext} from '#/components/dialogs/nudges' +import {OnePercent} from '#/components/dialogs/nudges/TenMillion/icons/OnePercent' +import {PointOnePercent} from '#/components/dialogs/nudges/TenMillion/icons/PointOnePercent' +import {TenPercent} from '#/components/dialogs/nudges/TenMillion/icons/TenPercent' import {Divider} from '#/components/Divider' import {GradientFill} from '#/components/GradientFill' import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' @@ -35,6 +38,7 @@ import {Download_Stroke2_Corner0_Rounded as Download} from '#/components/icons/D import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' +// import {TwentyFivePercent} from '#/components/dialogs/nudges/TenMillion/icons/TwentyFivePercent' const DEBUG = false const RATIO = 8 / 10 @@ -52,6 +56,20 @@ function getFontSize(count: number) { } } +function getPercentBadge(percent: number) { + if (percent <= 0.001) { + return PointOnePercent + } else if (percent <= 0.01) { + return OnePercent + } else if (percent <= 0.1) { + return TenPercent + } + // else if (percent <= 0.25) { + // return TwentyFivePercent + // } + return null +} + function Frame({children}: {children: React.ReactNode}) { return ( { if (uri) { @@ -151,7 +171,6 @@ export function TenMillion() { imageRef.current.capture // && // cavasRelayout === 'updated' ) { - console.log('LAYOUT') const uri = await imageRef.current.capture() setUri(uri) } @@ -230,7 +249,7 @@ export function TenMillion() { @@ -246,16 +265,22 @@ export function TenMillion() { {' '} 🎉 - + # @@ -275,6 +300,26 @@ export function TenMillion() { {i18n.number(userNumber)} + + {Badge && ( + + + + )} {/* End centered content */} @@ -398,15 +443,23 @@ export function TenMillion() { You're part of the next wave of the internet. - - Online culture is too important to be controlled by a few - corporations.{' '} + + + Online culture is too important to be controlled by a few + corporations. + {' '} - We’re dedicated to building an open foundation for the social - internet so that we can all shape its future. + + We’re dedicated to building an open foundation for the social + internet so that we can all shape its future. + + + Congratulations. We're glad you're here. + + Date: Wed, 11 Sep 2024 09:51:40 -0500 Subject: [PATCH 10/86] Copy --- .../dialogs/nudges/TenMillion/index.tsx | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/src/components/dialogs/nudges/TenMillion/index.tsx b/src/components/dialogs/nudges/TenMillion/index.tsx index fdac91f4..f42cd228 100644 --- a/src/components/dialogs/nudges/TenMillion/index.tsx +++ b/src/components/dialogs/nudges/TenMillion/index.tsx @@ -440,24 +440,14 @@ export function TenMillion() { fontWeight: '900', }, ]}> - You're part of the next wave of the internet. + Thanks for being an early part of Bluesky. - + - Online culture is too important to be controlled by a few - corporations. + We're rebuilding the social internet together. Congratulations, + we're glad you're here. {' '} - - - We’re dedicated to building an open foundation for the social - internet so that we can all shape its future. - - - - - - Congratulations. We're glad you're here. @@ -471,7 +461,7 @@ export function TenMillion() { a.pt_xl, ]}> - Brag a little ;) + Brag a little! From f8edd11bc5788666e0e6d55c1082971c901f089e Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 11 Sep 2024 09:57:37 -0500 Subject: [PATCH 11/86] Don't open for logged out users --- src/components/dialogs/nudges/TenMillion/index.tsx | 7 ++++++- src/components/dialogs/nudges/index.tsx | 13 ++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/components/dialogs/nudges/TenMillion/index.tsx b/src/components/dialogs/nudges/TenMillion/index.tsx index f42cd228..c2c6926f 100644 --- a/src/components/dialogs/nudges/TenMillion/index.tsx +++ b/src/components/dialogs/nudges/TenMillion/index.tsx @@ -87,6 +87,11 @@ function Frame({children}: {children: React.ReactNode}) { } export function TenMillion() { + const {hasSession} = useSession() + return hasSession ? : null +} + +export function TenMillionInner() { const t = useTheme() const lightTheme = useTheme('light') const {_, i18n} = useLingui() @@ -96,7 +101,7 @@ export function TenMillion() { const {currentAccount} = useSession() const {isLoading: isProfileLoading, data: profile} = useProfileQuery({ did: currentAccount!.did, - }) // TODO PWI + }) const moderationOpts = useModerationOpts() const moderation = React.useMemo(() => { return profile && moderationOpts diff --git a/src/components/dialogs/nudges/index.tsx b/src/components/dialogs/nudges/index.tsx index 357d4e2b..eabe60c1 100644 --- a/src/components/dialogs/nudges/index.tsx +++ b/src/components/dialogs/nudges/index.tsx @@ -1,7 +1,7 @@ import React from 'react' +import {useSession} from '#/state/session' import * as Dialog from '#/components/Dialog' - import {TenMillion} from '#/components/dialogs/nudges/TenMillion' type Context = { @@ -12,7 +12,7 @@ type Context = { const Context = React.createContext({ // @ts-ignore - controls: {} + controls: {}, }) export function useContext() { @@ -22,17 +22,20 @@ export function useContext() { let SHOWN = false export function NudgeDialogs() { + const {hasSession} = useSession() const tenMillion = Dialog.useDialogControl() const ctx = React.useMemo(() => { return { controls: { - tenMillion - } + tenMillion, + }, } }, [tenMillion]) React.useEffect(() => { + if (!hasSession) return + const t = setTimeout(() => { if (!SHOWN) { SHOWN = true @@ -43,7 +46,7 @@ export function NudgeDialogs() { return () => { clearTimeout(t) } - }, [ctx]) + }, [ctx, hasSession]) return ( From 77d60a5b8047c2a4b18206e99d89d25222b4601c Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 11 Sep 2024 18:18:04 -0500 Subject: [PATCH 12/86] Hook up data --- .../dialogs/nudges/TenMillion/index.tsx | 78 ++++++++++++++++++- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/src/components/dialogs/nudges/TenMillion/index.tsx b/src/components/dialogs/nudges/TenMillion/index.tsx index c2c6926f..e110ed1f 100644 --- a/src/components/dialogs/nudges/TenMillion/index.tsx +++ b/src/components/dialogs/nudges/TenMillion/index.tsx @@ -6,6 +6,7 @@ import {moderateProfile} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {networkRetry} from '#/lib/async/retry' import {getCanvas} from '#/lib/canvas' import {shareUrl} from '#/lib/sharing' import {sanitizeDisplayName} from '#/lib/strings/display-names' @@ -13,7 +14,7 @@ import {sanitizeHandle} from '#/lib/strings/handles' import {isNative} from '#/platform/detection' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useProfileQuery} from '#/state/queries/profile' -import {useSession} from '#/state/session' +import {useAgent, useSession} from '#/state/session' import {useComposerControls} from 'state/shell' import {formatCount} from '#/view/com/util/numeric/format' // import {UserAvatar} from '#/view/com/util/UserAvatar' @@ -109,14 +110,56 @@ export function TenMillionInner() { : undefined }, [profile, moderationOpts]) const [uri, setUri] = React.useState(null) + const [userNumber, setUserNumber] = React.useState(0) + const [error, setError] = React.useState('') - const isLoadingData = isProfileLoading || !moderation || !profile + const isLoadingData = + isProfileLoading || !moderation || !profile || !userNumber const isLoadingImage = !uri - const userNumber = 56_738 // TODO const percent = userNumber / 10_000_000 const Badge = getPercentBadge(percent) + const agent = useAgent() + React.useEffect(() => { + async function fetchUserNumber() { + if (agent.session?.accessJwt) { + const res = await fetch( + `https://bsky.social/xrpc/com.atproto.temp.getSignupNumber`, + { + headers: { + Authorization: `Bearer ${agent.session.accessJwt}`, + }, + }, + ) + + if (!res.ok) { + throw new Error('Network request failed') + } + + const data = await res.json() + + if (data.number) { + setUserNumber(data.number) + } + } + } + + networkRetry(3, fetchUserNumber).catch(() => { + setError( + _( + msg`Oh no! We couldn't fetch your user number. Rest assured, we're glad you're here ❤️`, + ), + ) + }) + }, [ + _, + agent.session?.accessJwt, + setUserNumber, + controls.tenMillion, + setError, + ]) + const sharePost = () => { if (uri) { controls.tenMillion.close(() => { @@ -421,7 +464,34 @@ export function TenMillionInner() { - {isLoadingData || isLoadingImage ? ( + {error ? ( + + + (╯°□°)╯︵ ┻━┻ + + + {error} + + + ) : isLoadingData || isLoadingImage ? ( ) : ( Date: Wed, 11 Sep 2024 20:01:27 -0500 Subject: [PATCH 13/86] Rename --- src/App.native.tsx | 4 ++-- src/App.web.tsx | 4 ++-- .../{nudges => nuxs}/TenMillion/icons/OnePercent.tsx | 0 .../TenMillion/icons/PointOnePercent.tsx | 0 .../{nudges => nuxs}/TenMillion/icons/TenPercent.tsx | 0 .../TenMillion/icons/TwentyFivePercent.tsx | 0 .../dialogs/{nudges => nuxs}/TenMillion/index.tsx | 10 +++++----- src/components/dialogs/{nudges => nuxs}/index.tsx | 4 ++-- 8 files changed, 11 insertions(+), 11 deletions(-) rename src/components/dialogs/{nudges => nuxs}/TenMillion/icons/OnePercent.tsx (100%) rename src/components/dialogs/{nudges => nuxs}/TenMillion/icons/PointOnePercent.tsx (100%) rename src/components/dialogs/{nudges => nuxs}/TenMillion/icons/TenPercent.tsx (100%) rename src/components/dialogs/{nudges => nuxs}/TenMillion/icons/TwentyFivePercent.tsx (100%) rename src/components/dialogs/{nudges => nuxs}/TenMillion/index.tsx (97%) rename src/components/dialogs/{nudges => nuxs}/index.tsx (90%) diff --git a/src/App.native.tsx b/src/App.native.tsx index 95625bdf..83f133e9 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -57,13 +57,13 @@ import * as Toast from '#/view/com/util/Toast' import {Shell} from '#/view/shell' import {ThemeProvider as Alf} from '#/alf' import {useColorModeTheme} from '#/alf/util/useColorModeTheme' +import {NuxDialogs} from '#/components/dialogs/nuxs' import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' import {Provider as PortalProvider} from '#/components/Portal' import {Splash} from '#/Splash' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' import {AudioCategory, PlatformInfo} from '../modules/expo-bluesky-swiss-army' -import {NudgeDialogs} from '#/components/dialogs/nudges' SplashScreen.preventAutoHideAsync() @@ -132,7 +132,7 @@ function InnerApp() { style={s.h100pct}> - + diff --git a/src/App.web.tsx b/src/App.web.tsx index 79120ffd..ff9944fa 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -46,11 +46,11 @@ import {ToastContainer} from '#/view/com/util/Toast.web' import {Shell} from '#/view/shell/index' import {ThemeProvider as Alf} from '#/alf' import {useColorModeTheme} from '#/alf/util/useColorModeTheme' +import {NuxDialogs} from '#/components/dialogs/nuxs' import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' import {Provider as PortalProvider} from '#/components/Portal' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' -import {NudgeDialogs} from '#/components/dialogs/nudges' function InnerApp() { const [isReady, setIsReady] = React.useState(false) @@ -114,7 +114,7 @@ function InnerApp() { - + diff --git a/src/components/dialogs/nudges/TenMillion/icons/OnePercent.tsx b/src/components/dialogs/nuxs/TenMillion/icons/OnePercent.tsx similarity index 100% rename from src/components/dialogs/nudges/TenMillion/icons/OnePercent.tsx rename to src/components/dialogs/nuxs/TenMillion/icons/OnePercent.tsx diff --git a/src/components/dialogs/nudges/TenMillion/icons/PointOnePercent.tsx b/src/components/dialogs/nuxs/TenMillion/icons/PointOnePercent.tsx similarity index 100% rename from src/components/dialogs/nudges/TenMillion/icons/PointOnePercent.tsx rename to src/components/dialogs/nuxs/TenMillion/icons/PointOnePercent.tsx diff --git a/src/components/dialogs/nudges/TenMillion/icons/TenPercent.tsx b/src/components/dialogs/nuxs/TenMillion/icons/TenPercent.tsx similarity index 100% rename from src/components/dialogs/nudges/TenMillion/icons/TenPercent.tsx rename to src/components/dialogs/nuxs/TenMillion/icons/TenPercent.tsx diff --git a/src/components/dialogs/nudges/TenMillion/icons/TwentyFivePercent.tsx b/src/components/dialogs/nuxs/TenMillion/icons/TwentyFivePercent.tsx similarity index 100% rename from src/components/dialogs/nudges/TenMillion/icons/TwentyFivePercent.tsx rename to src/components/dialogs/nuxs/TenMillion/icons/TwentyFivePercent.tsx diff --git a/src/components/dialogs/nudges/TenMillion/index.tsx b/src/components/dialogs/nuxs/TenMillion/index.tsx similarity index 97% rename from src/components/dialogs/nudges/TenMillion/index.tsx rename to src/components/dialogs/nuxs/TenMillion/index.tsx index e110ed1f..663c0956 100644 --- a/src/components/dialogs/nudges/TenMillion/index.tsx +++ b/src/components/dialogs/nuxs/TenMillion/index.tsx @@ -28,10 +28,10 @@ import { } from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' -import {useContext} from '#/components/dialogs/nudges' -import {OnePercent} from '#/components/dialogs/nudges/TenMillion/icons/OnePercent' -import {PointOnePercent} from '#/components/dialogs/nudges/TenMillion/icons/PointOnePercent' -import {TenPercent} from '#/components/dialogs/nudges/TenMillion/icons/TenPercent' +import {useContext} from '#/components/dialogs/nuxs' +import {OnePercent} from '#/components/dialogs/nuxs/TenMillion/icons/OnePercent' +import {PointOnePercent} from '#/components/dialogs/nuxs/TenMillion/icons/PointOnePercent' +import {TenPercent} from '#/components/dialogs/nuxs/TenMillion/icons/TenPercent' import {Divider} from '#/components/Divider' import {GradientFill} from '#/components/GradientFill' import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' @@ -39,7 +39,7 @@ import {Download_Stroke2_Corner0_Rounded as Download} from '#/components/icons/D import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' -// import {TwentyFivePercent} from '#/components/dialogs/nudges/TenMillion/icons/TwentyFivePercent' +// import {TwentyFivePercent} from '#/components/dialogs/nuxs/TenMillion/icons/TwentyFivePercent' const DEBUG = false const RATIO = 8 / 10 diff --git a/src/components/dialogs/nudges/index.tsx b/src/components/dialogs/nuxs/index.tsx similarity index 90% rename from src/components/dialogs/nudges/index.tsx rename to src/components/dialogs/nuxs/index.tsx index eabe60c1..401dd3e6 100644 --- a/src/components/dialogs/nudges/index.tsx +++ b/src/components/dialogs/nuxs/index.tsx @@ -2,7 +2,7 @@ import React from 'react' import {useSession} from '#/state/session' import * as Dialog from '#/components/Dialog' -import {TenMillion} from '#/components/dialogs/nudges/TenMillion' +import {TenMillion} from '#/components/dialogs/nuxs/TenMillion' type Context = { controls: { @@ -21,7 +21,7 @@ export function useContext() { let SHOWN = false -export function NudgeDialogs() { +export function NuxDialogs() { const {hasSession} = useSession() const tenMillion = Dialog.useDialogControl() From 9bb385a4dd54aca2b21533b7dd919ac8d0b4aeef Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 11 Sep 2024 21:20:39 -0500 Subject: [PATCH 14/86] Refactor, integrate nux, snoozing --- .../dialogs/nuxs/TenMillion/index.tsx | 182 ++++++++++-------- src/components/dialogs/nuxs/index.tsx | 84 +++++--- src/components/dialogs/nuxs/snoozing.ts | 18 ++ src/state/queries/nuxs/definitions.ts | 25 +-- src/storage/schema.ts | 4 +- 5 files changed, 182 insertions(+), 131 deletions(-) create mode 100644 src/components/dialogs/nuxs/snoozing.ts diff --git a/src/components/dialogs/nuxs/TenMillion/index.tsx b/src/components/dialogs/nuxs/TenMillion/index.tsx index 663c0956..d96456d4 100644 --- a/src/components/dialogs/nuxs/TenMillion/index.tsx +++ b/src/components/dialogs/nuxs/TenMillion/index.tsx @@ -1,5 +1,6 @@ import React from 'react' import {View} from 'react-native' +import Animated, {FadeIn} from 'react-native-reanimated' import ViewShot from 'react-native-view-shot' import {Image} from 'expo-image' import {moderateProfile} from '@atproto/api' @@ -17,7 +18,6 @@ import {useProfileQuery} from '#/state/queries/profile' import {useAgent, useSession} from '#/state/session' import {useComposerControls} from 'state/shell' import {formatCount} from '#/view/com/util/numeric/format' -// import {UserAvatar} from '#/view/com/util/UserAvatar' import {Logomark} from '#/view/icons/Logomark' import { atoms as a, @@ -28,7 +28,7 @@ import { } from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' -import {useContext} from '#/components/dialogs/nuxs' +import {useNuxDialogContext} from '#/components/dialogs/nuxs' import {OnePercent} from '#/components/dialogs/nuxs/TenMillion/icons/OnePercent' import {PointOnePercent} from '#/components/dialogs/nuxs/TenMillion/icons/PointOnePercent' import {TenPercent} from '#/components/dialogs/nuxs/TenMillion/icons/TenPercent' @@ -39,7 +39,6 @@ import {Download_Stroke2_Corner0_Rounded as Download} from '#/components/icons/D import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' -// import {TwentyFivePercent} from '#/components/dialogs/nuxs/TenMillion/icons/TwentyFivePercent' const DEBUG = false const RATIO = 8 / 10 @@ -65,9 +64,6 @@ function getPercentBadge(percent: number) { } else if (percent <= 0.1) { return TenPercent } - // else if (percent <= 0.25) { - // return TwentyFivePercent - // } return null } @@ -88,41 +84,13 @@ function Frame({children}: {children: React.ReactNode}) { } export function TenMillion() { - const {hasSession} = useSession() - return hasSession ? : null -} - -export function TenMillionInner() { - const t = useTheme() - const lightTheme = useTheme('light') - const {_, i18n} = useLingui() - const {controls} = useContext() - const {gtMobile} = useBreakpoints() - const {openComposer} = useComposerControls() - const {currentAccount} = useSession() - const {isLoading: isProfileLoading, data: profile} = useProfileQuery({ - did: currentAccount!.did, - }) - const moderationOpts = useModerationOpts() - const moderation = React.useMemo(() => { - return profile && moderationOpts - ? moderateProfile(profile, moderationOpts) - : undefined - }, [profile, moderationOpts]) - const [uri, setUri] = React.useState(null) - const [userNumber, setUserNumber] = React.useState(0) - const [error, setError] = React.useState('') - - const isLoadingData = - isProfileLoading || !moderation || !profile || !userNumber - const isLoadingImage = !uri - - const percent = userNumber / 10_000_000 - const Badge = getPercentBadge(percent) - const agent = useAgent() + const nuxDialogs = useNuxDialogContext() + const [userNumber, setUserNumber] = React.useState(0) + React.useEffect(() => { async function fetchUserNumber() { + // TODO check for 3p PDS if (agent.session?.accessJwt) { const res = await fetch( `https://bsky.social/xrpc/com.atproto.temp.getSignupNumber`, @@ -146,26 +114,83 @@ export function TenMillionInner() { } networkRetry(3, fetchUserNumber).catch(() => { - setError( - _( - msg`Oh no! We couldn't fetch your user number. Rest assured, we're glad you're here ❤️`, - ), - ) + nuxDialogs.dismissActiveNux() }) }, [ - _, agent.session?.accessJwt, setUserNumber, - controls.tenMillion, - setError, + nuxDialogs.dismissActiveNux, + nuxDialogs, ]) - const sharePost = () => { + return userNumber ? : null +} + +export function TenMillionInner({userNumber}: {userNumber: number}) { + const t = useTheme() + const lightTheme = useTheme('light') + const {_, i18n} = useLingui() + const control = Dialog.useDialogControl() + const {gtMobile} = useBreakpoints() + const {openComposer} = useComposerControls() + const {currentAccount} = useSession() + const { + isLoading: isProfileLoading, + data: profile, + error: profileError, + } = useProfileQuery({ + did: currentAccount!.did, + }) + const moderationOpts = useModerationOpts() + const nuxDialogs = useNuxDialogContext() + const moderation = React.useMemo(() => { + return profile && moderationOpts + ? moderateProfile(profile, moderationOpts) + : undefined + }, [profile, moderationOpts]) + const [uri, setUri] = React.useState(null) + const percent = userNumber / 10_000_000 + const Badge = getPercentBadge(percent) + const isLoadingData = isProfileLoading || !moderation || !profile + const isLoadingImage = !uri + + const error: string = React.useMemo(() => { + if (profileError) { + return _( + msg`Oh no! We weren't able to generate an image for you to share. Rest assured, we're glad you're here 🦋`, + ) + } + return '' + }, [_, profileError]) + + /* + * Opening and closing + */ + React.useEffect(() => { + const timeout = setTimeout(() => { + control.open() + }, 3e3) + return () => { + clearTimeout(timeout) + } + }, [control]) + const onClose = React.useCallback(() => { + nuxDialogs.dismissActiveNux() + }, [nuxDialogs]) + + /* + * Actions + */ + const sharePost = React.useCallback(() => { if (uri) { - controls.tenMillion.close(() => { + control.close(() => { setTimeout(() => { openComposer({ - text: '10 milly, babyyy', + text: _( + msg`I'm user #${i18n.number( + userNumber, + )} out of 10M. What a ride 😎`, + ), // TODO imageUris: [ { uri, @@ -177,17 +202,15 @@ export function TenMillionInner() { }, 1e3) }) } - } - - const onNativeShare = () => { + }, [_, i18n, control, openComposer, uri, userNumber]) + const onNativeShare = React.useCallback(() => { if (uri) { - controls.tenMillion.close(() => { + control.close(() => { shareUrl(uri) }) } - } - - const download = async () => { + }, [uri, control]) + const download = React.useCallback(async () => { if (uri) { const canvas = await getCanvas(uri) const imgHref = canvas @@ -198,32 +221,24 @@ export function TenMillionInner() { link.setAttribute('href', imgHref) link.click() } - } + }, [uri]) + /* + * Canvas stuff + */ const imageRef = React.useRef(null) - // const captureInProgress = React.useRef(false) - // const [cavasRelayout, setCanvasRelayout] = React.useState('key') - // const onCanvasReady = async () => { - // if ( - // imageRef.current && - // imageRef.current.capture && - // !captureInProgress.current - // ) { - // captureInProgress.current = true - // setCanvasRelayout('updated') - // } - // } - const onCanvasLayout = async () => { + const captureInProgress = React.useRef(false) + const onCanvasReady = React.useCallback(async () => { if ( imageRef.current && - imageRef.current.capture // && - // cavasRelayout === 'updated' + imageRef.current.capture && + !captureInProgress.current ) { + captureInProgress.current = true const uri = await imageRef.current.capture() setUri(uri) } - } - + }, [setUri]) const canvas = isLoadingData ? null : ( + ) : ( - + + + )} diff --git a/src/components/dialogs/nuxs/index.tsx b/src/components/dialogs/nuxs/index.tsx index 401dd3e6..6c4598cd 100644 --- a/src/components/dialogs/nuxs/index.tsx +++ b/src/components/dialogs/nuxs/index.tsx @@ -1,56 +1,80 @@ import React from 'react' +import {Nux, useNuxs, useUpsertNuxMutation} from '#/state/queries/nuxs' import {useSession} from '#/state/session' -import * as Dialog from '#/components/Dialog' +import {isSnoozed, snooze} from '#/components/dialogs/nuxs/snoozing' import {TenMillion} from '#/components/dialogs/nuxs/TenMillion' type Context = { - controls: { - tenMillion: Dialog.DialogOuterProps['control'] - } + activeNux: Nux | undefined + dismissActiveNux: () => void } +const queuedNuxs = [Nux.TenMillionDialog] + const Context = React.createContext({ - // @ts-ignore - controls: {}, + activeNux: undefined, + dismissActiveNux: () => {}, }) -export function useContext() { +export function useNuxDialogContext() { return React.useContext(Context) } -let SHOWN = false - export function NuxDialogs() { const {hasSession} = useSession() - const tenMillion = Dialog.useDialogControl() + return hasSession ? : null +} + +function Inner() { + const {nuxs} = useNuxs() + const [snoozed, setSnoozed] = React.useState(() => { + return isSnoozed() + }) + const [activeNux, setActiveNux] = React.useState() + const {mutate: upsertNux} = useUpsertNuxMutation() + + const snoozeNuxDialog = React.useCallback(() => { + snooze() + setSnoozed(true) + }, [setSnoozed]) + + const dismissActiveNux = React.useCallback(() => { + setActiveNux(undefined) + upsertNux({ + id: activeNux!, + completed: true, + data: undefined, + }) + }, [activeNux, setActiveNux, upsertNux]) + + React.useEffect(() => { + if (snoozed) return + if (!nuxs) return + + for (const id of queuedNuxs) { + const nux = nuxs.find(nux => nux.id === id) + + if (nux && nux.completed) continue + + setActiveNux(id) + // snooze immediately upon enabling + snoozeNuxDialog() + + break + } + }, [nuxs, snoozed, snoozeNuxDialog]) const ctx = React.useMemo(() => { return { - controls: { - tenMillion, - }, + activeNux, + dismissActiveNux, } - }, [tenMillion]) - - React.useEffect(() => { - if (!hasSession) return - - const t = setTimeout(() => { - if (!SHOWN) { - SHOWN = true - ctx.controls.tenMillion.open() - } - }, 2e3) - - return () => { - clearTimeout(t) - } - }, [ctx, hasSession]) + }, [activeNux, dismissActiveNux]) return ( - + {activeNux === Nux.TenMillionDialog && } ) } diff --git a/src/components/dialogs/nuxs/snoozing.ts b/src/components/dialogs/nuxs/snoozing.ts new file mode 100644 index 00000000..a36efd8e --- /dev/null +++ b/src/components/dialogs/nuxs/snoozing.ts @@ -0,0 +1,18 @@ +import {simpleAreDatesEqual} from '#/lib/strings/time' +import {device} from '#/storage' + +export function snooze() { + device.set(['lastNuxDialog'], new Date().toISOString()) +} + +export function isSnoozed() { + const lastNuxDialog = device.get(['lastNuxDialog']) + if (!lastNuxDialog) return false + const last = new Date(lastNuxDialog) + const now = new Date() + // already snoozed today + if (simpleAreDatesEqual(last, now)) { + return true + } + return false +} diff --git a/src/state/queries/nuxs/definitions.ts b/src/state/queries/nuxs/definitions.ts index c5cb1e9d..865967d3 100644 --- a/src/state/queries/nuxs/definitions.ts +++ b/src/state/queries/nuxs/definitions.ts @@ -3,27 +3,16 @@ import zod from 'zod' import {BaseNux} from '#/state/queries/nuxs/types' export enum Nux { - One = 'one', - Two = 'two', + TenMillionDialog = 'TenMillionDialog', } export const nuxNames = new Set(Object.values(Nux)) -export type AppNux = - | BaseNux<{ - id: Nux.One - data: { - likes: number - } - }> - | BaseNux<{ - id: Nux.Two - data: undefined - }> +export type AppNux = BaseNux<{ + id: Nux.TenMillionDialog + data: undefined +}> -export const NuxSchemas = { - [Nux.One]: zod.object({ - likes: zod.number(), - }), - [Nux.Two]: undefined, +export const NuxSchemas: Record | undefined> = { + [Nux.TenMillionDialog]: undefined, } diff --git a/src/storage/schema.ts b/src/storage/schema.ts index 6522d75a..be074db4 100644 --- a/src/storage/schema.ts +++ b/src/storage/schema.ts @@ -1,4 +1,6 @@ /** * Device data that's specific to the device and does not vary based account */ -export type Device = {} +export type Device = { + lastNuxDialog: string +} From c8b133863df5c6b417562f71f8a3c6feef280139 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 11 Sep 2024 21:28:34 -0500 Subject: [PATCH 15/86] Fix some nux types --- src/components/dialogs/nuxs/index.tsx | 9 ++++++--- src/state/queries/nuxs/types.ts | 4 +--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/dialogs/nuxs/index.tsx b/src/components/dialogs/nuxs/index.tsx index 6c4598cd..36db7764 100644 --- a/src/components/dialogs/nuxs/index.tsx +++ b/src/components/dialogs/nuxs/index.tsx @@ -40,13 +40,16 @@ function Inner() { }, [setSnoozed]) const dismissActiveNux = React.useCallback(() => { + if (!activeNux) return setActiveNux(undefined) + const nux = nuxs?.find(nux => nux.id === activeNux) upsertNux({ - id: activeNux!, + id: activeNux, completed: true, - data: undefined, + data: nux?.data, + expiresAt: nux?.expiresAt, }) - }, [activeNux, setActiveNux, upsertNux]) + }, [activeNux, setActiveNux, upsertNux, nuxs]) React.useEffect(() => { if (snoozed) return diff --git a/src/state/queries/nuxs/types.ts b/src/state/queries/nuxs/types.ts index 5b791847..2331582a 100644 --- a/src/state/queries/nuxs/types.ts +++ b/src/state/queries/nuxs/types.ts @@ -4,6 +4,4 @@ export type Data = Record | undefined export type BaseNux< T extends Pick & {data: Data}, -> = T & { - completed: boolean -} +> = Pick & T From 6e78ce53d74e79e2349ab357c7270e30742d33a5 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 11 Sep 2024 21:47:25 -0500 Subject: [PATCH 16/86] Dev helpers, string cleanup --- .../dialogs/nuxs/TenMillion/index.tsx | 9 ++++----- src/components/dialogs/nuxs/index.tsx | 20 +++++++++++++++++-- src/components/dialogs/nuxs/snoozing.ts | 4 ++++ src/storage/schema.ts | 2 +- 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/components/dialogs/nuxs/TenMillion/index.tsx b/src/components/dialogs/nuxs/TenMillion/index.tsx index d96456d4..5da295ab 100644 --- a/src/components/dialogs/nuxs/TenMillion/index.tsx +++ b/src/components/dialogs/nuxs/TenMillion/index.tsx @@ -430,7 +430,6 @@ export function TenMillionInner({userNumber}: {userNumber: number}) { style={[ a.text_sm, a.font_semibold, - , a.leading_tight, lightTheme.atoms.text_contrast_low, ]}> @@ -533,13 +532,13 @@ export function TenMillionInner({userNumber}: {userNumber: number}) { fontWeight: '900', }, ]}> - Thanks for being an early part of Bluesky. + You're part of the next wave of the internet. - We're rebuilding the social internet together. Congratulations, - we're glad you're here. + Thanks for being part of our first 10 million users. We're glad + you're here. {' '} @@ -554,7 +553,7 @@ export function TenMillionInner({userNumber}: {userNumber: number}) { a.pt_xl, ]}> - Brag a little! + Brag a little!
) From 08ac3a27c2be2a3bf345e6d2b34779653183cf7d Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 13 Sep 2024 12:05:15 -0500 Subject: [PATCH 41/86] Add events --- src/components/dialogs/nuxs/TenMillion/index.tsx | 5 +++++ src/lib/statsig/events.ts | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/src/components/dialogs/nuxs/TenMillion/index.tsx b/src/components/dialogs/nuxs/TenMillion/index.tsx index 801ceb99..8d5511fd 100644 --- a/src/components/dialogs/nuxs/TenMillion/index.tsx +++ b/src/components/dialogs/nuxs/TenMillion/index.tsx @@ -12,6 +12,7 @@ import {useLingui} from '@lingui/react' import {networkRetry} from '#/lib/async/retry' import {getCanvas} from '#/lib/canvas' import {shareUrl} from '#/lib/sharing' +import {logEvent} from '#/lib/statsig/statsig' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {isIOS, isNative} from '#/platform/detection' @@ -199,6 +200,7 @@ export function TenMillionInner({userNumber}: {userNumber: number}) { if (uri) { control.close(() => { setTimeout(() => { + logEvent('tmd:post', {}) openComposer({ text: _( msg`Bluesky now has over 10 million users, and I was #${i18n.number( @@ -220,6 +222,7 @@ export function TenMillionInner({userNumber}: {userNumber: number}) { const onNativeShare = React.useCallback(() => { if (uri) { control.close(() => { + logEvent('tmd:share', {}) shareUrl(uri) }) } @@ -240,6 +243,7 @@ export function TenMillionInner({userNumber}: {userNumber: number}) { try { await MediaLibrary.createAssetAsync(uri) + logEvent('tmd:download', {}) Toast.show(_(msg`Image saved to your camera roll!`)) } catch (e: unknown) { console.log(e) @@ -258,6 +262,7 @@ export function TenMillionInner({userNumber}: {userNumber: number}) { link.setAttribute('download', `Bluesky 10M Users.png`) link.setAttribute('href', imgHref) link.click() + logEvent('tmd:download', {}) } }, [uri]) diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index 18718949..c9bc8fef 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -225,4 +225,8 @@ export type LogEvents = { 'test:gate1:sometimes': {} 'test:gate2:always': {} 'test:gate2:sometimes': {} + + 'tmd:share': {} + 'tmd:download': {} + 'tmd:post': {} } From 78a531f5ffe9287b5384ec1649dfbc45435ced28 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 13 Sep 2024 13:02:47 -0500 Subject: [PATCH 42/86] Disable pointer events on media border (#5327) --- src/components/MediaInsetBorder.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/MediaInsetBorder.tsx b/src/components/MediaInsetBorder.tsx index 839d79ca..ef8b00e2 100644 --- a/src/components/MediaInsetBorder.tsx +++ b/src/components/MediaInsetBorder.tsx @@ -34,6 +34,9 @@ export function MediaInsetBorder({ : t.atoms.border_contrast_high, {opacity: 0.6}, ], + { + pointerEvents: 'none', + }, style, ]}> {children} From 26508cfe6a89df4ae1ab1256753faa860597bbc8 Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 13 Sep 2024 12:44:42 -0700 Subject: [PATCH 43/86] [Video] Remove `expo-video`, use `bluesky-video` (#5282) Co-authored-by: Samuel Newman --- app.config.js | 1 - package.json | 4 +- src/App.native.tsx | 79 +++-- src/components/video/PlayButtonIcon.tsx | 2 +- src/view/com/composer/videos/VideoPreview.tsx | 24 +- src/view/com/util/List.tsx | 8 +- .../post-embeds/ActiveVideoNativeContext.tsx | 65 ---- src/view/com/util/post-embeds/VideoEmbed.tsx | 142 ++------ .../VideoEmbedInner/TimeIndicator.tsx | 15 +- .../VideoEmbedInner/VideoEmbedInnerNative.tsx | 305 ++++++++++-------- yarn.lock | 9 +- 11 files changed, 269 insertions(+), 385 deletions(-) delete mode 100644 src/view/com/util/post-embeds/ActiveVideoNativeContext.tsx diff --git a/app.config.js b/app.config.js index 25014ee8..ddd72f75 100644 --- a/app.config.js +++ b/app.config.js @@ -211,7 +211,6 @@ module.exports = function (config) { sounds: PLATFORM === 'ios' ? ['assets/dm.aiff'] : ['assets/dm.mp3'], }, ], - 'expo-video', 'react-native-compressor', './plugins/starterPackAppClipExtension/withStarterPackAppClip.js', './plugins/withAndroidManifestPlugin.js', diff --git a/package.json b/package.json index 92b6cfe1..5401d5f7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bsky.app", - "version": "1.91.0", + "version": "1.91.1", "private": true, "engines": { "node": ">=18" @@ -68,6 +68,7 @@ "@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/react-native-fontawesome": "^0.3.2", + "@haileyok/bluesky-video": "0.1.2", "@lingui/react": "^4.5.0", "@mattermost/react-native-paste-input": "^0.7.1", "@miblanchard/react-native-slider": "^2.3.1", @@ -139,7 +140,6 @@ "expo-system-ui": "~3.0.4", "expo-task-manager": "~11.8.1", "expo-updates": "~0.25.14", - "expo-video": "https://github.com/bluesky-social/expo/raw/expo-video-1.2.4-patch/packages/expo-video/expo-video-v1.2.4-2.tgz", "expo-web-browser": "~13.0.3", "fast-text-encoding": "^1.0.6", "history": "^5.3.0", diff --git a/src/App.native.tsx b/src/App.native.tsx index 83f133e9..04fea126 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -52,7 +52,6 @@ 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 {TestCtrls} from '#/view/com/testing/TestCtrls' -import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoNativeContext' import * as Toast from '#/view/com/util/Toast' import {Shell} from '#/view/shell' import {ThemeProvider as Alf} from '#/alf' @@ -63,7 +62,6 @@ import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialo import {Provider as PortalProvider} from '#/components/Portal' import {Splash} from '#/Splash' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' -import {AudioCategory, PlatformInfo} from '../modules/expo-bluesky-swiss-army' SplashScreen.preventAutoHideAsync() @@ -110,45 +108,42 @@ function InnerApp() { - - - - - - - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -159,8 +154,6 @@ function App() { const [isReady, setReady] = useState(false) React.useEffect(() => { - PlatformInfo.setAudioCategory(AudioCategory.Ambient) - PlatformInfo.setAudioActive(false) initPersistedState().then(() => setReady(true)) }, []) diff --git a/src/components/video/PlayButtonIcon.tsx b/src/components/video/PlayButtonIcon.tsx index 90e93f74..8e0a6bb7 100644 --- a/src/components/video/PlayButtonIcon.tsx +++ b/src/components/video/PlayButtonIcon.tsx @@ -4,7 +4,7 @@ import {View} from 'react-native' import {atoms as a, useTheme} from '#/alf' import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play' -export function PlayButtonIcon({size = 36}: {size?: number}) { +export function PlayButtonIcon({size = 32}: {size?: number}) { const t = useTheme() const bg = t.name === 'light' ? t.palette.contrast_25 : t.palette.contrast_975 const fg = t.name === 'light' ? t.palette.contrast_975 : t.palette.contrast_25 diff --git a/src/view/com/composer/videos/VideoPreview.tsx b/src/view/com/composer/videos/VideoPreview.tsx index 60b467d6..b1bfd671 100644 --- a/src/view/com/composer/videos/VideoPreview.tsx +++ b/src/view/com/composer/videos/VideoPreview.tsx @@ -1,8 +1,7 @@ -/* eslint-disable @typescript-eslint/no-shadow */ import React from 'react' import {View} from 'react-native' import {ImagePickerAsset} from 'expo-image-picker' -import {useVideoPlayer, VideoView} from 'expo-video' +import {BlueskyVideoView} from '@haileyok/bluesky-video' import {CompressedVideo} from '#/lib/media/video/types' import {clamp} from '#/lib/numbers' @@ -22,15 +21,8 @@ export function VideoPreview({ clear: () => void }) { const t = useTheme() + const playerRef = React.useRef(null) const autoplayDisabled = useAutoplayDisabled() - const player = useVideoPlayer(video.uri, player => { - player.loop = true - player.muted = true - if (!autoplayDisabled) { - player.play() - } - }) - let aspectRatio = asset.width / asset.height if (isNaN(aspectRatio)) { @@ -50,12 +42,12 @@ export function VideoPreview({ t.atoms.border_contrast_low, {backgroundColor: 'black'}, ]}> - {autoplayDisabled && ( diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx index 79dd2f49..f9aeae1a 100644 --- a/src/view/com/util/List.tsx +++ b/src/view/com/util/List.tsx @@ -1,6 +1,7 @@ import React, {memo} from 'react' import {FlatListProps, RefreshControl, ViewToken} from 'react-native' import {runOnJS, useSharedValue} from 'react-native-reanimated' +import {updateActiveVideoViewAsync} from '@haileyok/bluesky-video' import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' import {usePalette} from '#/lib/hooks/usePalette' @@ -8,7 +9,6 @@ import {useScrollHandlers} from '#/lib/ScrollContext' import {useDedupe} from 'lib/hooks/useDedupe' import {addStyle} from 'lib/styles' import {isIOS} from 'platform/detection' -import {updateActiveViewAsync} from '../../../../modules/expo-bluesky-swiss-army/src/VisibilityView' import {FlatList_INTERNAL} from './Views' export type ListMethods = FlatList_INTERNAL @@ -69,7 +69,7 @@ function ListImpl( onBeginDragFromContext?.(e, ctx) }, onEndDrag(e, ctx) { - runOnJS(updateActiveViewAsync)() + runOnJS(updateActiveVideoViewAsync)() onEndDragFromContext?.(e, ctx) }, onScroll(e, ctx) { @@ -84,13 +84,13 @@ function ListImpl( } if (isIOS) { - runOnJS(dedupe)(updateActiveViewAsync) + runOnJS(dedupe)(updateActiveVideoViewAsync) } }, // Note: adding onMomentumBegin here makes simulator scroll // lag on Android. So either don't add it, or figure out why. onMomentumEnd(e, ctx) { - runOnJS(updateActiveViewAsync)() + runOnJS(updateActiveVideoViewAsync)() onMomentumEndFromContext?.(e, ctx) }, }) diff --git a/src/view/com/util/post-embeds/ActiveVideoNativeContext.tsx b/src/view/com/util/post-embeds/ActiveVideoNativeContext.tsx deleted file mode 100644 index 95fa0bb0..00000000 --- a/src/view/com/util/post-embeds/ActiveVideoNativeContext.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react' -import {useVideoPlayer, VideoPlayer} from 'expo-video' - -import {isAndroid, isNative} from '#/platform/detection' - -const Context = React.createContext<{ - activeSource: string - activeViewId: string | undefined - setActiveSource: (src: string | null, viewId: string | null) => void - player: VideoPlayer -} | null>(null) - -export function Provider({children}: {children: React.ReactNode}) { - if (!isNative) { - throw new Error('ActiveVideoProvider may only be used on native.') - } - - const [activeSource, setActiveSource] = React.useState('') - const [activeViewId, setActiveViewId] = React.useState() - - const player = useVideoPlayer(activeSource, p => { - p.muted = true - p.loop = true - // We want to immediately call `play` so we get the loading state - p.play() - }) - - const setActiveSourceOuter = (src: string | null, viewId: string | null) => { - // HACK - // expo-video doesn't like it when you try and move a `player` to another `VideoView`. Instead, we need to actually - // unregister that player to let the new screen register it. This is only a problem on Android, so we only need to - // apply it there. - if (src === activeSource && isAndroid) { - setActiveSource('') - setTimeout(() => { - setActiveSource(src ? src : '') - }, 100) - } else { - setActiveSource(src ? src : '') - } - setActiveViewId(viewId ? viewId : '') - } - - return ( - - {children} - - ) -} - -export function useActiveVideoNative() { - const context = React.useContext(Context) - if (!context) { - throw new Error( - 'useActiveVideoNative must be used within a ActiveVideoNativeProvider', - ) - } - return context -} diff --git a/src/view/com/util/post-embeds/VideoEmbed.tsx b/src/view/com/util/post-embeds/VideoEmbed.tsx index a672830d..267b5d18 100644 --- a/src/view/com/util/post-embeds/VideoEmbed.tsx +++ b/src/view/com/util/post-embeds/VideoEmbed.tsx @@ -1,22 +1,18 @@ -import React, {useCallback, useEffect, useId, useState} from 'react' +import React, {useCallback, useState} from 'react' import {View} from 'react-native' import {ImageBackground} from 'expo-image' -import {PlayerError, VideoPlayerStatus} from 'expo-video' import {AppBskyEmbedVideo} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {clamp} from '#/lib/numbers' -import {useAutoplayDisabled} from 'state/preferences' import {VideoEmbedInnerNative} from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative' import {atoms as a} from '#/alf' import {Button} from '#/components/Button' -import {useIsWithinMessage} from '#/components/dms/MessageContext' +import {useThrottledValue} from '#/components/hooks/useThrottledValue' import {Loader} from '#/components/Loader' import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' -import {VisibilityView} from '../../../../../modules/expo-bluesky-swiss-army' import {ErrorBoundary} from '../ErrorBoundary' -import {useActiveVideoNative} from './ActiveVideoNativeContext' import * as VideoFallback from './VideoEmbedInner/VideoFallback' interface Props { @@ -59,113 +55,36 @@ export function VideoEmbed({embed}: Props) { function InnerWrapper({embed}: Props) { const {_} = useLingui() - const {activeSource, activeViewId, setActiveSource, player} = - useActiveVideoNative() - const viewId = useId() + const ref = React.useRef<{togglePlayback: () => void}>(null) - const [playerStatus, setPlayerStatus] = useState< - VideoPlayerStatus | 'paused' - >('paused') - const [isMuted, setIsMuted] = useState(player.muted) - const [isFullscreen, setIsFullscreen] = React.useState(false) - const [timeRemaining, setTimeRemaining] = React.useState(0) - const isWithinMessage = useIsWithinMessage() - const disableAutoplay = useAutoplayDisabled() || isWithinMessage - const isActive = embed.playlist === activeSource && activeViewId === viewId - // There are some different loading states that we should pay attention to and show a spinner for - const isLoading = - isActive && - (playerStatus === 'waitingToPlayAtSpecifiedRate' || - playerStatus === 'loading') - // This happens whenever the visibility view decides that another video should start playing - const showOverlay = !isActive || isLoading || playerStatus === 'paused' + const [status, setStatus] = React.useState<'playing' | 'paused' | 'pending'>( + 'pending', + ) + const [isLoading, setIsLoading] = React.useState(false) + const [isActive, setIsActive] = React.useState(false) + const showSpinner = useThrottledValue(isActive && isLoading, 100) - // send error up to error boundary - const [error, setError] = useState(null) - if (error) { - throw error - } + const showOverlay = + !isActive || + isLoading || + (status === 'paused' && !isActive) || + status === 'pending' - useEffect(() => { - if (isActive) { - // eslint-disable-next-line @typescript-eslint/no-shadow - const volumeSub = player.addListener('volumeChange', ({isMuted}) => { - setIsMuted(isMuted) - }) - const timeSub = player.addListener( - 'timeRemainingChange', - secondsRemaining => { - setTimeRemaining(secondsRemaining) - }, - ) - const statusSub = player.addListener( - 'statusChange', - (status, oldStatus, playerError) => { - setPlayerStatus(status) - if (status === 'error') { - setError(playerError ?? new Error('Unknown player error')) - } - if (status === 'readyToPlay' && oldStatus !== 'readyToPlay') { - player.play() - } - }, - ) - return () => { - volumeSub.remove() - timeSub.remove() - statusSub.remove() - } + React.useEffect(() => { + if (!isActive && status !== 'pending') { + setStatus('pending') } - }, [player, isActive, disableAutoplay]) - - // The source might already be active (for example, if you are scrolling a list of quotes and its all the same - // video). In those cases, just start playing. Otherwise, setting the active source will result in the video - // start playback immediately - const startPlaying = (ignoreAutoplayPreference: boolean) => { - if (disableAutoplay && !ignoreAutoplayPreference) { - return - } - - if (isActive) { - player.play() - } else { - setActiveSource(embed.playlist, viewId) - } - } - - const onVisibilityStatusChange = (isVisible: boolean) => { - // When `isFullscreen` is true, it means we're actually still exiting the fullscreen player. Ignore these change - // events - if (isFullscreen) { - return - } - if (isVisible) { - startPlaying(false) - } else { - // Clear the active source so the video view unmounts when autoplay is disabled. Otherwise, leave it mounted - // until it gets replaced by another video - if (disableAutoplay) { - setActiveSource(null, null) - } else { - player.muted = true - if (player.playing) { - player.pause() - } - } - } - } + }, [isActive, status]) return ( - - {isActive ? ( - - ) : null} + <> + - + ) } diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/TimeIndicator.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/TimeIndicator.tsx index be3f9071..66e1df50 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/TimeIndicator.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/TimeIndicator.tsx @@ -1,4 +1,5 @@ import React from 'react' +import {StyleProp, ViewStyle} from 'react-native' import Animated, {FadeInDown, FadeOutDown} from 'react-native-reanimated' import {atoms as a, native, useTheme} from '#/alf' @@ -8,7 +9,13 @@ import {Text} from '#/components/Typography' * Absolutely positioned time indicator showing how many seconds are remaining * Time is in seconds */ -export function TimeIndicator({time}: {time: number}) { +export function TimeIndicator({ + time, + style, +}: { + time: number + style?: StyleProp +}) { const t = useTheme() if (isNaN(time)) { @@ -22,18 +29,20 @@ export function TimeIndicator({time}: {time: number}) { void - timeRemaining: number - isMuted: boolean -}) { - const {_} = useLingui() - const {player} = useActiveVideoNative() - const ref = useRef(null) +export const VideoEmbedInnerNative = React.forwardRef( + function VideoEmbedInnerNative( + { + embed, + setStatus, + setIsLoading, + setIsActive, + }: { + embed: AppBskyEmbedVideo.View + setStatus: (status: 'playing' | 'paused') => void + setIsLoading: (isLoading: boolean) => void + setIsActive: (isActive: boolean) => void + }, + ref: React.Ref<{togglePlayback: () => void}>, + ) { + const {_} = useLingui() + const videoRef = useRef(null) + const autoplayDisabled = useAutoplayDisabled() + const isWithinMessage = useIsWithinMessage() - const enterFullscreen = useCallback(() => { - ref.current?.enterFullscreen() - }, []) + const [isMuted, setIsMuted] = React.useState(true) + const [isPlaying, setIsPlaying] = React.useState(false) + const [timeRemaining, setTimeRemaining] = React.useState(0) + const [error, setError] = React.useState() - let aspectRatio = 16 / 9 + React.useImperativeHandle(ref, () => ({ + togglePlayback: () => { + videoRef.current?.togglePlayback() + }, + })) - if (embed.aspectRatio) { - const {width, height} = embed.aspectRatio - aspectRatio = width / height - aspectRatio = clamp(aspectRatio, 1 / 1, 3 / 1) - } + if (error) { + throw new Error(error) + } - return ( - - { - PlatformInfo.setAudioCategory(AudioCategory.Playback) - PlatformInfo.setAudioActive(true) - player.muted = false - setIsFullscreen(true) - if (isAndroid) { - player.play() + let aspectRatio = 16 / 9 + + if (embed.aspectRatio) { + const {width, height} = embed.aspectRatio + aspectRatio = width / height + aspectRatio = clamp(aspectRatio, 1 / 1, 3 / 1) + } + + return ( + + { + setIsActive(e.nativeEvent.isActive) + }} + onLoadingChange={e => { + setIsLoading(e.nativeEvent.isLoading) + }} + onMutedChange={e => { + setIsMuted(e.nativeEvent.isMuted) + }} + onStatusChange={e => { + setStatus(e.nativeEvent.status) + setIsPlaying(e.nativeEvent.status === 'playing') + }} + onTimeRemainingChange={e => { + setTimeRemaining(e.nativeEvent.timeRemaining) + }} + onError={e => { + setError(e.nativeEvent.error) + }} + ref={videoRef} + accessibilityLabel={ + embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`) } - }} - onFullscreenExit={() => { - PlatformInfo.setAudioCategory(AudioCategory.Ambient) - PlatformInfo.setAudioActive(false) - player.muted = true - player.playbackRate = 1 - setIsFullscreen(false) - }} - accessibilityLabel={ - embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`) - } - accessibilityHint="" - /> - - - - ) -} + accessibilityHint="" + /> + { + videoRef.current?.enterFullscreen() + }} + toggleMuted={() => { + videoRef.current?.toggleMuted() + }} + togglePlayback={() => { + videoRef.current?.togglePlayback() + }} + isMuted={isMuted} + isPlaying={isPlaying} + timeRemaining={timeRemaining} + /> + + + ) + }, +) function VideoControls({ - player, enterFullscreen, + toggleMuted, + togglePlayback, timeRemaining, + isPlaying, isMuted, }: { - player: VideoPlayer enterFullscreen: () => void + toggleMuted: () => void + togglePlayback: () => void timeRemaining: number + isPlaying: boolean isMuted: boolean }) { const {_} = useLingui() const t = useTheme() - const onPressFullscreen = useCallback(() => { - switch (player.status) { - case 'idle': - case 'loading': - case 'readyToPlay': { - if (!player.playing) player.play() - enterFullscreen() - break - } - case 'error': { - player.replay() - break - } - } - }, [player, enterFullscreen]) - - const toggleMuted = useCallback(() => { - const muted = !player.muted - // We want to set this to the _inverse_ of the new value, because we actually want for the audio to be mixed when - // the video is muted, and vice versa. - const mix = !muted - const category = muted ? AudioCategory.Ambient : AudioCategory.Playback - - PlatformInfo.setAudioCategory(category) - PlatformInfo.setAudioActive(mix) - player.muted = muted - }, [player]) - // show countdown when: // 1. timeRemaining is a number - was seeing NaNs // 2. duration is greater than 0 - means metadata has loaded @@ -140,44 +139,80 @@ function VideoControls({ return ( - {showTime && } - - - {isMuted ? ( - - ) : ( - - )} - - + + {isPlaying ? ( + + ) : ( + + )} + + {showTime && } + + + {isMuted ? ( + + ) : ( + + )} + ) } + +function ControlButton({ + onPress, + children, + label, + accessibilityHint, + style, +}: { + onPress: () => void + children: React.ReactNode + label: string + accessibilityHint: string + style?: StyleProp +}) { + return ( + + + {children} + + + ) +} diff --git a/yarn.lock b/yarn.lock index b2e389aa..5fd07230 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4104,6 +4104,11 @@ resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== +"@haileyok/bluesky-video@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@haileyok/bluesky-video/-/bluesky-video-0.1.2.tgz#53abb04c22885fcf8a1d8a7510d2cfbe7d45ff91" + integrity sha512-OPltVPNhjrm/+d4YYbaSsKLK7VQWC62ci8J05GO4I/PhWsYLWsAu79CGfZ1YTpfpIjYXyo0HjMmiig5X/hhOsQ== + "@hapi/accept@^6.0.3": version "6.0.3" resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-6.0.3.tgz#eef0800a4f89cd969da8e5d0311dc877c37279ab" @@ -12415,10 +12420,6 @@ expo-updates@~0.25.14: ignore "^5.3.1" resolve-from "^5.0.0" -"expo-video@https://github.com/bluesky-social/expo/raw/expo-video-1.2.4-patch/packages/expo-video/expo-video-v1.2.4-2.tgz": - version "1.2.4" - resolved "https://github.com/bluesky-social/expo/raw/expo-video-1.2.4-patch/packages/expo-video/expo-video-v1.2.4-2.tgz#4127dd5cea5fdf7ab745104c73b8ecf5506f5d34" - expo-web-browser@~13.0.3: version "13.0.3" resolved "https://registry.yarnpkg.com/expo-web-browser/-/expo-web-browser-13.0.3.tgz#dceb05dbc187b498ca937b02adf385b0232a4e92" From 791bc7afbe0efd9519740b999799e6002b0fc835 Mon Sep 17 00:00:00 2001 From: dan Date: Fri, 13 Sep 2024 21:11:17 +0100 Subject: [PATCH 44/86] Fix lexicon validation in PWI Discover (#5329) --- src/lib/api/feed/custom.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/api/feed/custom.ts b/src/lib/api/feed/custom.ts index 6db96a8d..f3ac45b6 100644 --- a/src/lib/api/feed/custom.ts +++ b/src/lib/api/feed/custom.ts @@ -2,6 +2,7 @@ import { AppBskyFeedDefs, AppBskyFeedGetFeed as GetCustomFeed, BskyAgent, + jsonStringToLex, } from '@atproto/api' import {getContentLanguages} from '#/state/preferences/languages' @@ -111,7 +112,7 @@ async function loggedOutFetch({ }&limit=${limit}&lang=${contentLangs}`, {method: 'GET', headers: {'Accept-Language': contentLangs}}, ) - let data = res.ok ? await res.json() : null + let data = res.ok ? jsonStringToLex(await res.text()) : null if (data?.feed?.length) { return { success: true, @@ -126,7 +127,7 @@ async function loggedOutFetch({ }&limit=${limit}`, {method: 'GET', headers: {'Accept-Language': ''}}, ) - data = res.ok ? await res.json() : null + data = res.ok ? jsonStringToLex(await res.text()) : null if (data?.feed?.length) { return { success: true, From 843f9925f5d0773db321e617c1bd0be6a308ef7f Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 13 Sep 2024 14:07:13 -0700 Subject: [PATCH 45/86] [Video] Remember mute state while scrolling (#5331) --- package.json | 2 +- src/App.native.tsx | 72 +++++++++--------- src/App.web.tsx | 75 ++++++++++--------- .../VideoEmbedInner/VideoEmbedInnerNative.tsx | 15 ++-- .../util/post-embeds/VideoVolumeContext.tsx | 32 ++++++++ yarn.lock | 8 +- 6 files changed, 121 insertions(+), 83 deletions(-) create mode 100644 src/view/com/util/post-embeds/VideoVolumeContext.tsx diff --git a/package.json b/package.json index 5401d5f7..1cff0d45 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/react-native-fontawesome": "^0.3.2", - "@haileyok/bluesky-video": "0.1.2", + "@haileyok/bluesky-video": "0.1.4", "@lingui/react": "^4.5.0", "@mattermost/react-native-paste-input": "^0.7.1", "@miblanchard/react-native-slider": "^2.3.1", diff --git a/src/App.native.tsx b/src/App.native.tsx index 04fea126..2ec666e2 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -52,6 +52,7 @@ 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 {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' import {Shell} from '#/view/shell' import {ThemeProvider as Alf} from '#/alf' @@ -109,40 +110,43 @@ function InnerApp() { - - - - - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index ff9944fa..bef32082 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -41,6 +41,7 @@ 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 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' import {ToastContainer} from '#/view/com/util/Toast.web' import {Shell} from '#/view/shell/index' @@ -95,42 +96,44 @@ function InnerApp() { - - - - - - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx index 39ed990a..ee71daa8 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx @@ -9,6 +9,7 @@ import {useLingui} from '@lingui/react' import {HITSLOP_30} from '#/lib/constants' import {clamp} from '#/lib/numbers' import {useAutoplayDisabled} from '#/state/preferences' +import {useVideoVolumeState} from 'view/com/util/post-embeds/VideoVolumeContext' import {atoms as a, useTheme} from '#/alf' import {useIsWithinMessage} from '#/components/dms/MessageContext' import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute' @@ -37,8 +38,8 @@ export const VideoEmbedInnerNative = React.forwardRef( const videoRef = useRef(null) const autoplayDisabled = useAutoplayDisabled() const isWithinMessage = useIsWithinMessage() + const {muted, setMuted} = useVideoVolumeState() - const [isMuted, setIsMuted] = React.useState(true) const [isPlaying, setIsPlaying] = React.useState(false) const [timeRemaining, setTimeRemaining] = React.useState(0) const [error, setError] = React.useState() @@ -66,7 +67,7 @@ export const VideoEmbedInnerNative = React.forwardRef( { setIsActive(e.nativeEvent.isActive) @@ -75,7 +76,7 @@ export const VideoEmbedInnerNative = React.forwardRef( setIsLoading(e.nativeEvent.isLoading) }} onMutedChange={e => { - setIsMuted(e.nativeEvent.isMuted) + setMuted(e.nativeEvent.isMuted) }} onStatusChange={e => { setStatus(e.nativeEvent.status) @@ -103,7 +104,6 @@ export const VideoEmbedInnerNative = React.forwardRef( togglePlayback={() => { videoRef.current?.togglePlayback() }} - isMuted={isMuted} isPlaying={isPlaying} timeRemaining={timeRemaining} /> @@ -119,17 +119,16 @@ function VideoControls({ togglePlayback, timeRemaining, isPlaying, - isMuted, }: { enterFullscreen: () => void toggleMuted: () => void togglePlayback: () => void timeRemaining: number isPlaying: boolean - isMuted: boolean }) { const {_} = useLingui() const t = useTheme() + const {muted} = useVideoVolumeState() // show countdown when: // 1. timeRemaining is a number - was seeing NaNs @@ -161,10 +160,10 @@ function VideoControls({ - {isMuted ? ( + {muted ? ( ) : ( diff --git a/src/view/com/util/post-embeds/VideoVolumeContext.tsx b/src/view/com/util/post-embeds/VideoVolumeContext.tsx new file mode 100644 index 00000000..cccb93ba --- /dev/null +++ b/src/view/com/util/post-embeds/VideoVolumeContext.tsx @@ -0,0 +1,32 @@ +import React from 'react' + +const Context = React.createContext( + {} as { + muted: boolean + setMuted: (muted: boolean) => void + }, +) + +export function Provider({children}: {children: React.ReactNode}) { + const [muted, setMuted] = React.useState(true) + + const value = React.useMemo( + () => ({ + muted, + setMuted, + }), + [muted, setMuted], + ) + + return {children} +} + +export function useVideoVolumeState() { + const context = React.useContext(Context) + if (!context) { + throw new Error( + 'useVideoVolumeState must be used within a VideoVolumeProvider', + ) + } + return context +} diff --git a/yarn.lock b/yarn.lock index 5fd07230..16cfb340 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4104,10 +4104,10 @@ resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== -"@haileyok/bluesky-video@0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@haileyok/bluesky-video/-/bluesky-video-0.1.2.tgz#53abb04c22885fcf8a1d8a7510d2cfbe7d45ff91" - integrity sha512-OPltVPNhjrm/+d4YYbaSsKLK7VQWC62ci8J05GO4I/PhWsYLWsAu79CGfZ1YTpfpIjYXyo0HjMmiig5X/hhOsQ== +"@haileyok/bluesky-video@0.1.4": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@haileyok/bluesky-video/-/bluesky-video-0.1.4.tgz#76acad0dffb9c80745bb752577be23cb566e4562" + integrity sha512-ggpk6E6U3giT+tmTc4GPraViA3ssnP32/Bty61UbZ3LiCQuc694LX+AOt01SfQ0B0fyd63J9DtT5rfaEJyjuzg== "@hapi/accept@^6.0.3": version "6.0.3" From 533382173c498a382c5192bb7829da7ac900d7e3 Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 13 Sep 2024 14:08:45 -0700 Subject: [PATCH 46/86] [Video] Don't require email verification on self-host (#5332) --- src/view/com/composer/videos/SelectVideoBtn.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/view/com/composer/videos/SelectVideoBtn.tsx b/src/view/com/composer/videos/SelectVideoBtn.tsx index da67d781..2f2b4c3e 100644 --- a/src/view/com/composer/videos/SelectVideoBtn.tsx +++ b/src/view/com/composer/videos/SelectVideoBtn.tsx @@ -13,6 +13,8 @@ import {useVideoLibraryPermission} from '#/lib/hooks/usePermissions' import {isNative} from '#/platform/detection' import {useModalControls} from '#/state/modals' import {useSession} from '#/state/session' +import {BSKY_SERVICE} from 'lib/constants' +import {getHostnameFromUrl} from 'lib/strings/url-helpers' import {atoms as a, useTheme} from '#/alf' import {Button} from '#/components/Button' import {VideoClip_Stroke2_Corner0_Rounded as VideoClipIcon} from '#/components/icons/VideoClip' @@ -38,7 +40,12 @@ export function SelectVideoBtn({onSelectVideo, disabled, setError}: Props) { return } - if (!currentAccount?.emailConfirmed) { + if ( + currentAccount && + !currentAccount.emailConfirmed && + getHostnameFromUrl(currentAccount.service) === + getHostnameFromUrl(BSKY_SERVICE) + ) { Keyboard.dismiss() control.open() } else { @@ -71,12 +78,12 @@ export function SelectVideoBtn({onSelectVideo, disabled, setError}: Props) { } } }, [ - onSelectVideo, requestVideoAccessIfNeeded, + currentAccount, + control, setError, _, - control, - currentAccount?.emailConfirmed, + onSelectVideo, ]) return ( From 88813f57c98041507eec708294272387cdc4a0f2 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 13 Sep 2024 16:21:45 -0500 Subject: [PATCH 47/86] Always display next button on login page (#5326) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Vinícius Souza <39967235+vinifsouza@users.noreply.github.com> Co-authored-by: Hailey --- src/screens/Login/ForgotPasswordForm.tsx | 3 +-- src/screens/Login/LoginForm.tsx | 31 +++++++----------------- 2 files changed, 10 insertions(+), 24 deletions(-) diff --git a/src/screens/Login/ForgotPasswordForm.tsx b/src/screens/Login/ForgotPasswordForm.tsx index ec30bab4..8588888b 100644 --- a/src/screens/Login/ForgotPasswordForm.tsx +++ b/src/screens/Login/ForgotPasswordForm.tsx @@ -144,8 +144,7 @@ export const ForgotPasswordForm = ({ variant="solid" color={'primary'} size="medium" - onPress={onPressNext} - disabled={!email}> + onPress={onPressNext}> Next diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx index 35b124b6..9a01c049 100644 --- a/src/screens/Login/LoginForm.tsx +++ b/src/screens/Login/LoginForm.tsx @@ -60,7 +60,6 @@ export const LoginForm = ({ const {track} = useAnalytics() const t = useTheme() const [isProcessing, setIsProcessing] = useState(false) - const [isReady, setIsReady] = useState(false) const [isAuthFactorTokenNeeded, setIsAuthFactorTokenNeeded] = useState(false) const identifierValueRef = useRef(initialHandle || '') @@ -83,12 +82,18 @@ export const LoginForm = ({ Keyboard.dismiss() LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) setError('') - setIsProcessing(true) const identifier = identifierValueRef.current.toLowerCase().trim() const password = passwordValueRef.current const authFactorToken = authFactorTokenValueRef.current + if (!identifier || !password) { + setError(_(msg`Invalid username or password`)) + return + } + + setIsProcessing(true) + try { // try to guess the handle if the user just gave their own username let fullIdent = identifier @@ -157,22 +162,6 @@ export const LoginForm = ({ } } - const checkIsReady = () => { - if ( - !!serviceDescription && - !!identifierValueRef.current && - !!passwordValueRef.current - ) { - if (!isReady) { - setIsReady(true) - } - } else { - if (isReady) { - setIsReady(false) - } - } - } - return ( Sign in}> @@ -204,7 +193,6 @@ export const LoginForm = ({ defaultValue={initialHandle || ''} onChangeText={v => { identifierValueRef.current = v - checkIsReady() }} onSubmitEditing={() => { passwordRef.current?.focus() @@ -233,7 +221,6 @@ export const LoginForm = ({ clearButtonMode="while-editing" onChangeText={v => { passwordValueRef.current = v - checkIsReady() }} onSubmitEditing={onPressNext} blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing @@ -325,7 +312,7 @@ export const LoginForm = ({ Connecting... - ) : isReady ? ( + ) : ( - ) : undefined} + )} ) From ce3893d8169cb63e982b57d18817c9155c2e874c Mon Sep 17 00:00:00 2001 From: dan Date: Fri, 13 Sep 2024 22:30:09 +0100 Subject: [PATCH 48/86] Apply Following settings to Lists (#5313) * Apply Following settings to Lists * Remove dead code --- src/components/StarterPack/Main/PostsList.tsx | 2 +- src/state/preferences/feed-tuners.tsx | 26 +------------------ src/state/queries/post-feed.ts | 2 -- 3 files changed, 2 insertions(+), 28 deletions(-) diff --git a/src/components/StarterPack/Main/PostsList.tsx b/src/components/StarterPack/Main/PostsList.tsx index c19c6bc6..0ff84ff4 100644 --- a/src/components/StarterPack/Main/PostsList.tsx +++ b/src/components/StarterPack/Main/PostsList.tsx @@ -18,7 +18,7 @@ interface ProfilesListProps { export const PostsList = React.forwardRef( function PostsListImpl({listUri, headerHeight, scrollElRef}, ref) { - const feed: FeedDescriptor = `list|${listUri}|as_following` + const feed: FeedDescriptor = `list|${listUri}` const {_} = useLingui() const onScrollToTop = useCallback(() => { diff --git a/src/state/preferences/feed-tuners.tsx b/src/state/preferences/feed-tuners.tsx index b6f14fae..3ed60e59 100644 --- a/src/state/preferences/feed-tuners.tsx +++ b/src/state/preferences/feed-tuners.tsx @@ -21,31 +21,7 @@ export function useFeedTuners(feedDesc: FeedDescriptor) { if (feedDesc.startsWith('feedgen')) { return [FeedTuner.preferredLangOnly(langPrefs.contentLanguages)] } - if (feedDesc.startsWith('list')) { - let feedTuners = [] - if (feedDesc.endsWith('|as_following')) { - // Same as Following tuners below, copypaste for now. - feedTuners.push(FeedTuner.removeOrphans) - if (preferences?.feedViewPrefs.hideReposts) { - feedTuners.push(FeedTuner.removeReposts) - } - if (preferences?.feedViewPrefs.hideReplies) { - feedTuners.push(FeedTuner.removeReplies) - } else { - feedTuners.push( - FeedTuner.followedRepliesOnly({ - userDid: currentAccount?.did || '', - }), - ) - } - if (preferences?.feedViewPrefs.hideQuotePosts) { - feedTuners.push(FeedTuner.removeQuotePosts) - } - feedTuners.push(FeedTuner.dedupThreads) - } - return feedTuners - } - if (feedDesc === 'following') { + if (feedDesc === 'following' || feedDesc.startsWith('list')) { const feedTuners = [FeedTuner.removeOrphans] if (preferences?.feedViewPrefs.hideReposts) { diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index ee3e2c14..7daf441a 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -51,7 +51,6 @@ type AuthorFilter = | 'posts_with_media' type FeedUri = string type ListUri = string -type ListFilter = 'as_following' // Applies current Following settings. Currently client-side. export type FeedDescriptor = | 'following' @@ -59,7 +58,6 @@ export type FeedDescriptor = | `feedgen|${FeedUri}` | `likes|${ActorDid}` | `list|${ListUri}` - | `list|${ListUri}|${ListFilter}` export interface FeedParams { mergeFeedEnabled?: boolean mergeFeedSources?: string[] From cac43127f0163c84a921afd806d91e1df10ea568 Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 13 Sep 2024 14:46:02 -0700 Subject: [PATCH 49/86] [Video] Bump video (#5333) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 1cff0d45..b970a54d 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/react-native-fontawesome": "^0.3.2", - "@haileyok/bluesky-video": "0.1.4", + "@haileyok/bluesky-video": "0.1.5", "@lingui/react": "^4.5.0", "@mattermost/react-native-paste-input": "^0.7.1", "@miblanchard/react-native-slider": "^2.3.1", diff --git a/yarn.lock b/yarn.lock index 16cfb340..33c16f3a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4104,10 +4104,10 @@ resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== -"@haileyok/bluesky-video@0.1.4": - version "0.1.4" - resolved "https://registry.yarnpkg.com/@haileyok/bluesky-video/-/bluesky-video-0.1.4.tgz#76acad0dffb9c80745bb752577be23cb566e4562" - integrity sha512-ggpk6E6U3giT+tmTc4GPraViA3ssnP32/Bty61UbZ3LiCQuc694LX+AOt01SfQ0B0fyd63J9DtT5rfaEJyjuzg== +"@haileyok/bluesky-video@0.1.5": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@haileyok/bluesky-video/-/bluesky-video-0.1.5.tgz#76b2adb89baa321fd881e7463bba3288f161ff06" + integrity sha512-nx0RWk1oghu/ObR2iPvlJDSBdtzh8UOvgawLF60leL/v+mM8SUrCJgba51SfosJKFvAX3/ABms/VOryFu0U/iw== "@hapi/accept@^6.0.3": version "6.0.3" From d76f9abdd718e24848a9b8f67486129aee421427 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 13 Sep 2024 16:48:28 -0500 Subject: [PATCH 50/86] "N" keyboard shortcut to open a new post modal (#5197) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Add hook on web app to open composer with 'N' keyboard shortcut * Extract, don't fire open composer if already open * Ignore interactive elements --------- Co-authored-by: João Gabriel Co-authored-by: Hailey --- src/App.web.tsx | 3 ++ .../{composer.tsx => composer/index.tsx} | 0 .../composer/useComposerKeyboardShortcut.tsx | 49 +++++++++++++++++++ 3 files changed, 52 insertions(+) rename src/state/shell/{composer.tsx => composer/index.tsx} (100%) create mode 100644 src/state/shell/composer/useComposerKeyboardShortcut.tsx diff --git a/src/App.web.tsx b/src/App.web.tsx index bef32082..6efe7cc0 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -35,6 +35,7 @@ import { } from '#/state/session' import {readLastActiveAccount} from '#/state/session/util' import {Provider as ShellStateProvider} from '#/state/shell' +import {useComposerKeyboardShortcut} from '#/state/shell/composer/useComposerKeyboardShortcut' 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' @@ -62,6 +63,8 @@ function InnerApp() { useIntentHandler() const hasCheckedReferrer = useStarterPackEntry() + useComposerKeyboardShortcut() + // init useEffect(() => { async function onLaunch(account?: SessionAccount) { diff --git a/src/state/shell/composer.tsx b/src/state/shell/composer/index.tsx similarity index 100% rename from src/state/shell/composer.tsx rename to src/state/shell/composer/index.tsx diff --git a/src/state/shell/composer/useComposerKeyboardShortcut.tsx b/src/state/shell/composer/useComposerKeyboardShortcut.tsx new file mode 100644 index 00000000..f4606218 --- /dev/null +++ b/src/state/shell/composer/useComposerKeyboardShortcut.tsx @@ -0,0 +1,49 @@ +import React from 'react' + +import {useComposerControls} from './' + +/** + * Based on {@link https://github.com/jaywcjlove/hotkeys-js/blob/b0038773f3b902574f22af747f3bb003a850f1da/src/index.js#L51C1-L64C2} + */ +function shouldIgnore(event: KeyboardEvent) { + const target: any = event.target || event.srcElement + if (!target) return false + const {tagName} = target + if (!tagName) return false + const isInput = + tagName === 'INPUT' && + ![ + 'checkbox', + 'radio', + 'range', + 'button', + 'file', + 'reset', + 'submit', + 'color', + ].includes(target.type) + // ignore: isContentEditable === 'true', and