From 99078bbff2282ad18594a4d5a4ed5fe207c394e4 Mon Sep 17 00:00:00 2001 From: Hailey Date: Wed, 12 Jun 2024 08:51:08 -0700 Subject: [PATCH 01/81] Don't show warning when sharing your own post in PWI opt-out mode (#4495) --- src/view/com/profile/ProfileMenu.tsx | 7 +++++-- src/view/com/util/forms/PostDropdownBtn.tsx | 5 ++++- src/view/com/util/post-ctrls/PostCtrls.tsx | 10 ++++++---- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/view/com/profile/ProfileMenu.tsx b/src/view/com/profile/ProfileMenu.tsx index 5d39e5f0f3..efc2497600 100644 --- a/src/view/com/profile/ProfileMenu.tsx +++ b/src/view/com/profile/ProfileMenu.tsx @@ -71,8 +71,11 @@ let ProfileMenu = ({ const loggedOutWarningPromptControl = Prompt.usePromptControl() const showLoggedOutWarning = React.useMemo(() => { - return !!profile.labels?.find(label => label.val === '!no-unauthenticated') - }, [profile.labels]) + return ( + profile.did !== currentAccount?.did && + !!profile.labels?.find(label => label.val === '!no-unauthenticated') + ) + }, [currentAccount, profile]) const invalidateProfileQuery = React.useCallback(() => { queryClient.invalidateQueries({ diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index b6873ff8ad..2486b73d58 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -196,6 +196,9 @@ let PostDropdownBtn = ({ ) }, [postAuthor]) + const showLoggedOutWarning = + postAuthor.did !== currentAccount?.did && hideInPWI + const onSharePost = React.useCallback(() => { const url = toShareUrl(href) shareUrl(url) @@ -296,7 +299,7 @@ let PostDropdownBtn = ({ testID="postDropdownShareBtn" label={isWeb ? _(msg`Copy link to post`) : _(msg`Share`)} onPress={() => { - if (hideInPWI) { + if (showLoggedOutWarning) { loggedOutWarningPromptControl.open() } else { onSharePost() diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index d42590e905..c389855e3d 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -27,7 +27,7 @@ import { usePostLikeMutationQueue, usePostRepostMutationQueue, } from '#/state/queries/post' -import {useRequireAuth} from '#/state/session' +import {useRequireAuth, useSession} from '#/state/session' import {useComposerControls} from '#/state/shell/composer' import {atoms as a, useTheme} from '#/alf' import {useDialogControl} from '#/components/Dialog' @@ -64,6 +64,7 @@ let PostCtrls = ({ const t = useTheme() const {_} = useLingui() const {openComposer} = useComposerControls() + const {currentAccount} = useSession() const [queueLike, queueUnlike] = usePostLikeMutationQueue(post, logContext) const [queueRepost, queueUnrepost] = usePostRepostMutationQueue( post, @@ -75,10 +76,11 @@ let PostCtrls = ({ const playHaptic = useHaptics() const shouldShowLoggedOutWarning = React.useMemo(() => { - return !!post.author.labels?.find( - label => label.val === '!no-unauthenticated', + return ( + post.author.did !== currentAccount?.did && + !!post.author.labels?.find(label => label.val === '!no-unauthenticated') ) - }, [post]) + }, [currentAccount, post]) const defaultCtrlColor = React.useMemo( () => ({ From a55f924639605709d436474073e75f9f39a84276 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 12 Jun 2024 16:52:00 -0500 Subject: [PATCH 02/81] Just use server count (#4499) * Just use server count * Fix count --- src/components/KnownFollowers.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/components/KnownFollowers.tsx b/src/components/KnownFollowers.tsx index b99fe3398e..2b8dcc866d 100644 --- a/src/components/KnownFollowers.tsx +++ b/src/components/KnownFollowers.tsx @@ -82,15 +82,6 @@ function KnownFollowersInner({ t.atoms.text_contrast_medium, ] - // list of users, minus blocks - const returnedCount = cachedKnownFollowers.followers.length - // db count, includes blocks - const fullCount = cachedKnownFollowers.count - // knownFollowers can return up to 5 users, but will exclude blocks - // therefore, if we have less 5 users, use whichever count is lower - const count = - returnedCount < 5 ? Math.min(fullCount, returnedCount) : fullCount - const slice = cachedKnownFollowers.followers.slice(0, 3).map(f => { const moderation = moderateProfile(f, moderationOpts) return { @@ -104,6 +95,7 @@ function KnownFollowersInner({ moderation, } }) + const count = cachedKnownFollowers.count - Math.min(slice.length, 2) return ( Date: Thu, 13 Jun 2024 04:23:37 +0200 Subject: [PATCH 03/81] Show social proof in hovercards (#4502) * Add social proof to hovercards * Close it more reliably --- src/components/KnownFollowers.tsx | 8 +++++++- src/components/ProfileHoverCard/index.web.tsx | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/components/KnownFollowers.tsx b/src/components/KnownFollowers.tsx index 2b8dcc866d..3d4d362ef2 100644 --- a/src/components/KnownFollowers.tsx +++ b/src/components/KnownFollowers.tsx @@ -8,7 +8,7 @@ import {makeProfileLink} from '#/lib/routes/links' import {sanitizeDisplayName} from 'lib/strings/display-names' import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme} from '#/alf' -import {Link} from '#/components/Link' +import {Link, LinkProps} from '#/components/Link' import {Text} from '#/components/Typography' const AVI_SIZE = 30 @@ -29,9 +29,11 @@ export function shouldShowKnownFollowers( export function KnownFollowers({ profile, moderationOpts, + onLinkPress, }: { profile: AppBskyActorDefs.ProfileViewDetailed moderationOpts: ModerationOpts + onLinkPress?: LinkProps['onPress'] }) { const cache = React.useRef>( new Map(), @@ -56,6 +58,7 @@ export function KnownFollowers({ profile={profile} cachedKnownFollowers={cachedKnownFollowers} moderationOpts={moderationOpts} + onLinkPress={onLinkPress} /> ) } @@ -67,10 +70,12 @@ function KnownFollowersInner({ profile, moderationOpts, cachedKnownFollowers, + onLinkPress, }: { profile: AppBskyActorDefs.ProfileViewDetailed moderationOpts: ModerationOpts cachedKnownFollowers: AppBskyActorDefs.KnownFollowers + onLinkPress?: LinkProps['onPress'] }) { const t = useTheme() const {_} = useLingui() @@ -102,6 +107,7 @@ function KnownFollowersInner({ label={_( msg`Press to view followers of this account that you also follow`, )} + onPress={onLinkPress} to={makeProfileLink(profile, 'known-followers')} style={[ a.flex_1, diff --git a/src/components/ProfileHoverCard/index.web.tsx b/src/components/ProfileHoverCard/index.web.tsx index 024867b1a3..1fd74fd195 100644 --- a/src/components/ProfileHoverCard/index.web.tsx +++ b/src/components/ProfileHoverCard/index.web.tsx @@ -22,6 +22,10 @@ import {useFollowMethods} from '#/components/hooks/useFollowMethods' import {useRichText} from '#/components/hooks/useRichText' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' +import { + KnownFollowers, + shouldShowKnownFollowers, +} from '#/components/KnownFollowers' import {InlineLinkText, Link} from '#/components/Link' import {Loader} from '#/components/Loader' import {Portal} from '#/components/Portal' @@ -473,6 +477,17 @@ function Inner({ /> ) : undefined} + + {!isMe && + shouldShowKnownFollowers(profile.viewer?.knownFollowers) && ( + + + + )} )} From 247af5aee99ba5020b8bee9ccfa0f02bb8c4084a Mon Sep 17 00:00:00 2001 From: Mary <148872143+mary-ext@users.noreply.github.com> Date: Thu, 13 Jun 2024 09:37:49 +0700 Subject: [PATCH 04/81] Prevent rich-formatting paste (#4327) * fix: prevent rich-formatting paste * fix: return true instead of preventDefault --- .../com/composer/text-input/TextInput.web.tsx | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index 7f8dc2ed5e..a91524974e 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -150,16 +150,27 @@ export const TextInput = React.forwardRef(function TextInputImpl( attributes: { class: modeClass, }, - handlePaste: (_, event) => { - const items = event.clipboardData?.items + handlePaste: (view, event) => { + const clipboardData = event.clipboardData - if (items === undefined) { - return + if (clipboardData) { + if (clipboardData.types.includes('text/html')) { + // Rich-text formatting is pasted, try retrieving plain text + const text = clipboardData.getData('text/plain') + + // `pasteText` will invoke this handler again, but `clipboardData` will be null. + view.pasteText(text) + + // Return `true` to prevent ProseMirror's default paste behavior. + return true + } else { + // Otherwise, try retrieving images from the clipboard + + getImageFromUri(clipboardData.items, (uri: string) => { + textInputWebEmitter.emit('photo-pasted', uri) + }) + } } - - getImageFromUri(items, (uri: string) => { - textInputWebEmitter.emit('photo-pasted', uri) - }) }, handleKeyDown: (_, event) => { if ((event.metaKey || event.ctrlKey) && event.code === 'Enter') { From d989128e5b086fc60aea01ba991039c4e66165c6 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 12 Jun 2024 21:44:06 -0500 Subject: [PATCH 05/81] Set profile hover prefetch stale time to 30s (#4417) * Set prefetch stale time to 30s * Run prefetch on mouseOver * Only prefetch once on mousemove --- src/state/queries/profile.ts | 1 + src/view/com/util/PostMeta.tsx | 75 +++++++++++++++++++--------------- 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts index 7cc9f69116..6f7f2de792 100644 --- a/src/state/queries/profile.ts +++ b/src/state/queries/profile.ts @@ -94,6 +94,7 @@ export function usePrefetchProfileQuery() { const prefetchProfileQuery = useCallback( async (did: string) => { await queryClient.prefetchQuery({ + staleTime: STALE.SECONDS.THIRTY, queryKey: RQKEY(did), queryFn: async () => { const res = await agent.getProfile({actor: did || ''}) diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index b6fe6d374d..df45174b9a 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -11,6 +11,7 @@ import {sanitizeHandle} from 'lib/strings/handles' import {niceDate} from 'lib/strings/time' import {TypographyVariant} from 'lib/ThemeContext' import {isAndroid, isWeb} from 'platform/detection' +import {atoms as a} from '#/alf' import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {TextLinkOnWebOnly} from './Link' import {Text} from './text/Text' @@ -39,9 +40,13 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => { const prefetchProfileQuery = usePrefetchProfileQuery() const profileLink = makeProfileLink(opts.author) - const onPointerEnter = isWeb - ? () => prefetchProfileQuery(opts.author.did) - : undefined + const prefetchedProfile = React.useRef(false) + const onPointerMove = React.useCallback(() => { + if (!prefetchedProfile.current) { + prefetchedProfile.current = true + prefetchProfileQuery(opts.author.did) + } + }, [opts.author.did, prefetchProfileQuery]) const queryClient = useQueryClient() const onOpenAuthor = opts.onOpenAuthor @@ -66,37 +71,39 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => { )} - - - {sanitizeDisplayName( - displayName, - opts.moderation?.ui('displayName'), - )} - - } - href={profileLink} - onBeforePress={onBeforePressAuthor} - onPointerEnter={onPointerEnter} - /> - - + + + + {sanitizeDisplayName( + displayName, + opts.moderation?.ui('displayName'), + )} + + } + href={profileLink} + onBeforePress={onBeforePressAuthor} + /> + + + {!isAndroid && ( Date: Wed, 12 Jun 2024 21:53:32 -0500 Subject: [PATCH 06/81] Fix profile hover card blocked state (#4480) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix: mini profile on hover allows following a blocker/blocked user (#4423) (#4440) * Tweaks --------- Co-authored-by: Michał Gołda --- src/components/ProfileHoverCard/index.web.tsx | 79 +++++++++++++------ .../moderation/ProfileHeaderAlerts.tsx | 13 ++- 2 files changed, 65 insertions(+), 27 deletions(-) diff --git a/src/components/ProfileHoverCard/index.web.tsx b/src/components/ProfileHoverCard/index.web.tsx index 1fd74fd195..e17977af43 100644 --- a/src/components/ProfileHoverCard/index.web.tsx +++ b/src/components/ProfileHoverCard/index.web.tsx @@ -5,6 +5,7 @@ import {flip, offset, shift, size, useFloating} from '@floating-ui/react-dom' import {msg, plural} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {getModerationCauseKey} from '#/lib/moderation' import {makeProfileLink} from '#/lib/routes/links' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' @@ -31,6 +32,7 @@ import {Loader} from '#/components/Loader' import {Portal} from '#/components/Portal' import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' +import {ProfileLabel} from '../moderation/ProfileHeaderAlerts' import {ProfileHoverCardProps} from './types' const floatingMiddlewares = [ @@ -374,7 +376,10 @@ function Inner({ profile: profileShadow, logContext: 'ProfileHoverCard', }) - const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy + const isBlockedUser = + profile.viewer?.blocking || + profile.viewer?.blockedBy || + profile.viewer?.blockingByList const following = formatCount(profile.followsCount || 0) const followers = formatCount(profile.followersCount || 0) const pluralizedFollowers = plural(profile.followersCount || 0, { @@ -405,29 +410,41 @@ function Inner({ /> - {!isMe && ( - - )} + {!isMe && + (isBlockedUser ? ( + + {_(msg`View profile`)} + + ) : ( + + ))} @@ -443,7 +460,19 @@ function Inner({ - {!blockHide && ( + {isBlockedUser && ( + + {moderation.ui('profileView').alerts.map(cause => ( + + ))} + + )} + + {!isBlockedUser && ( <> - + {!disableDetailsDialog && ( + + )} ) } From 7faa1d9131fc28e1c0acd4b4c7b7c2bbeabc2281 Mon Sep 17 00:00:00 2001 From: Minseo Lee Date: Thu, 13 Jun 2024 11:58:25 +0900 Subject: [PATCH 07/81] KnownFollowers localization (#4494) * Update KnownFollowers.tsx * Update KnownFollowers.tsx * Update KnownFollowers.tsx --- src/components/KnownFollowers.tsx | 52 +++++++++++++++++-------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/src/components/KnownFollowers.tsx b/src/components/KnownFollowers.tsx index 3d4d362ef2..a8bdb763d7 100644 --- a/src/components/KnownFollowers.tsx +++ b/src/components/KnownFollowers.tsx @@ -1,7 +1,7 @@ import React from 'react' import {View} from 'react-native' import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' -import {msg, plural, Trans} from '@lingui/macro' +import {msg, Plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {makeProfileLink} from '#/lib/routes/links' @@ -164,31 +164,37 @@ function KnownFollowersInner({ }, ]} numberOfLines={2}> - Followed by{' '} {count > 2 ? ( - <> - {slice.slice(0, 2).map(({profile: prof}, i) => ( - - {prof.displayName} - {i === 0 && ', '} - - ))} - {', '} - {plural(count - 2, { - one: 'and # other', - other: 'and # others', - })} - - ) : count === 2 ? ( - slice.map(({profile: prof}, i) => ( - - {prof.displayName} {i === 0 ? _(msg`and`) + ' ' : ''} + + Followed by{' '} + + {slice[0].profile.displayName} - )) + ,{' '} + + {slice[1].profile.displayName} + + , and{' '} + + + ) : count === 2 ? ( + + Followed by{' '} + + {slice[0].profile.displayName} + {' '} + and{' '} + + {slice[1].profile.displayName} + + ) : ( - - {slice[0].profile.displayName} - + + Followed by{' '} + + {slice[0].profile.displayName} + + )} From 498e46ae4e698e5a75dec9972209fe2bb8a2a603 Mon Sep 17 00:00:00 2001 From: dan Date: Thu, 13 Jun 2024 11:03:31 +0200 Subject: [PATCH 08/81] Hide bio and social proof for blocked users (#4504) --- src/screens/Profile/Header/ProfileHeaderStandard.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx index f8a87a68e4..4ad84ac633 100644 --- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx +++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx @@ -75,6 +75,10 @@ let ProfileHeaderStandard = ({ const [_queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) const unblockPromptControl = Prompt.usePromptControl() const requireAuth = useRequireAuth() + const isBlockedUser = + profile.viewer?.blocking || + profile.viewer?.blockedBy || + profile.viewer?.blockingByList const onPressEditProfile = React.useCallback(() => { track('ProfileHeader:EditProfileButtonClicked') @@ -257,7 +261,7 @@ let ProfileHeaderStandard = ({ - {!isPlaceholderProfile && ( + {!isPlaceholderProfile && !isBlockedUser && ( <> {descriptionRT && !moderation.ui('profileView').blur ? ( @@ -274,6 +278,7 @@ let ProfileHeaderStandard = ({ ) : undefined} {!isMe && + !isBlockedUser && shouldShowKnownFollowers(profile.viewer?.knownFollowers) && ( Date: Thu, 13 Jun 2024 15:58:56 +0100 Subject: [PATCH 09/81] Calculate correct keyboard offset in composer (#4500) * calculate correct keyboard offset * give viewHeight a default value * much simpler approach --- src/view/com/composer/Composer.tsx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 9bb704012e..80bce5351c 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -415,12 +415,14 @@ export const ComposePost = observer(function ComposePost({ bottomBarAnimatedStyle, } = useAnimatedBorders() + const keyboardVerticalOffset = useKeyboardVerticalOffset() + return ( + keyboardVerticalOffset={keyboardVerticalOffset} + style={a.flex_1}> @@ -741,6 +743,19 @@ function useAnimatedBorders() { } } +function useKeyboardVerticalOffset() { + const {top} = useSafeAreaInsets() + + // Android etc + if (!isIOS) return 0 + + // iPhone SE + if (top === 20) return 40 + + // all other iPhones + return top + 10 +} + const styles = StyleSheet.create({ topbarInner: { flexDirection: 'row', From d9066a6beb4a84ab42d638810d25c002601678a4 Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 13 Jun 2024 14:31:19 -0700 Subject: [PATCH 10/81] add `document.referrer` to statsig custom (#4509) * add referrer to statsig custom dont include referrer if hostname is bsky.app save add `document.referrer` to statsig custom * add a hostname field * account for ssr * account for ssr --- src/lib/statsig/statsig.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx index 166d468a1b..d57f1c0aa6 100644 --- a/src/lib/statsig/statsig.tsx +++ b/src/lib/statsig/statsig.tsx @@ -26,6 +26,8 @@ type StatsigUser = { bundleDate: number refSrc: string refUrl: string + referrer: string + referrerHostname: string appLanguage: string contentLanguages: string[] } @@ -33,12 +35,22 @@ type StatsigUser = { let refSrc = '' let refUrl = '' +let referrer = '' +let referrerHostname = '' if (isWeb && typeof window !== 'undefined') { const params = new URLSearchParams(window.location.search) refSrc = params.get('ref_src') ?? '' refUrl = decodeURIComponent(params.get('ref_url') ?? '') } +if (isWeb && typeof document !== 'undefined' && document != null) { + const url = new URL(document.referrer) + if (url.hostname !== 'bsky.app') { + referrer = document.referrer + referrerHostname = url.hostname + } +} + export type {LogEvents} function createStatsigOptions(prefetchUsers: StatsigUser[]) { @@ -198,6 +210,8 @@ function toStatsigUser(did: string | undefined): StatsigUser { custom: { refSrc, refUrl, + referrer, + referrerHostname, platform: Platform.OS as 'ios' | 'android' | 'web', bundleIdentifier: BUNDLE_IDENTIFIER, bundleDate: BUNDLE_DATE, From bdeac28d74abd54b0373663cbd57b7858888280f Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 13 Jun 2024 23:06:22 +0100 Subject: [PATCH 11/81] Try/catch URL parsing of referrer (#4511) --- src/lib/statsig/statsig.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx index d57f1c0aa6..f6aed999f9 100644 --- a/src/lib/statsig/statsig.tsx +++ b/src/lib/statsig/statsig.tsx @@ -43,12 +43,19 @@ if (isWeb && typeof window !== 'undefined') { refUrl = decodeURIComponent(params.get('ref_url') ?? '') } -if (isWeb && typeof document !== 'undefined' && document != null) { - const url = new URL(document.referrer) - if (url.hostname !== 'bsky.app') { - referrer = document.referrer - referrerHostname = url.hostname - } +if ( + isWeb && + typeof document !== 'undefined' && + document != null && + document.referrer +) { + try { + const url = new URL(document.referrer) + if (url.hostname !== 'bsky.app') { + referrer = document.referrer + referrerHostname = url.hostname + } + } catch {} } export type {LogEvents} From 4c0f0378808410513558696a12a68bbd3c1d9a75 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 14 Jun 2024 11:56:29 -0500 Subject: [PATCH 12/81] Reuse overfetching for popular feeds, add in existing filtering (#4501) --- src/state/queries/feed.ts | 100 +++++++++++++++++++++++++++++++++++-- src/view/screens/Feeds.tsx | 46 +++-------------- 2 files changed, 101 insertions(+), 45 deletions(-) diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts index b599ac1a0f..fed23f5b12 100644 --- a/src/state/queries/feed.ts +++ b/src/state/queries/feed.ts @@ -1,3 +1,4 @@ +import {useCallback, useEffect, useMemo, useRef} from 'react' import { AppBskyActorDefs, AppBskyFeedDefs, @@ -171,28 +172,117 @@ export function useFeedSourceInfoQuery({uri}: {uri: string}) { }) } -export const useGetPopularFeedsQueryKey = ['getPopularFeeds'] +// HACK +// the protocol doesn't yet tell us which feeds are personalized +// this list is used to filter out feed recommendations from logged out users +// for the ones we know need it +// -prf +export const KNOWN_AUTHED_ONLY_FEEDS = [ + 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/with-friends', // popular with friends, by bsky.app + 'at://did:plc:tenurhgjptubkk5zf5qhi3og/app.bsky.feed.generator/mutuals', // mutuals, by skyfeed + 'at://did:plc:tenurhgjptubkk5zf5qhi3og/app.bsky.feed.generator/only-posts', // only posts, by skyfeed + 'at://did:plc:wzsilnxf24ehtmmc3gssy5bu/app.bsky.feed.generator/mentions', // mentions, by flicknow + 'at://did:plc:q6gjnaw2blty4crticxkmujt/app.bsky.feed.generator/bangers', // my bangers, by jaz + 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/mutuals', // mutuals, by bluesky + 'at://did:plc:q6gjnaw2blty4crticxkmujt/app.bsky.feed.generator/my-followers', // followers, by jaz + 'at://did:plc:vpkhqolt662uhesyj6nxm7ys/app.bsky.feed.generator/followpics', // the gram, by why +] -export function useGetPopularFeedsQuery() { +type GetPopularFeedsOptions = {limit?: number} + +export function createGetPopularFeedsQueryKey(...args: any[]) { + return ['getPopularFeeds', ...args] +} + +export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) { + const {hasSession} = useSession() const agent = useAgent() - return useInfiniteQuery< + const limit = options?.limit || 10 + const {data: preferences} = usePreferencesQuery() + + // Make sure this doesn't invalidate unless really needed. + const selectArgs = useMemo( + () => ({ + hasSession, + savedFeeds: preferences?.savedFeeds || [], + }), + [hasSession, preferences?.savedFeeds], + ) + const lastPageCountRef = useRef(0) + + const query = useInfiniteQuery< AppBskyUnspeccedGetPopularFeedGenerators.OutputSchema, Error, InfiniteData, QueryKey, string | undefined >({ - queryKey: useGetPopularFeedsQueryKey, + queryKey: createGetPopularFeedsQueryKey(options), queryFn: async ({pageParam}) => { const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({ - limit: 10, + limit, cursor: pageParam, }) return res.data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, + select: useCallback( + ( + data: InfiniteData, + ) => { + const {savedFeeds, hasSession: hasSessionInner} = selectArgs + data?.pages.map(page => { + page.feeds = page.feeds.filter(feed => { + if ( + !hasSessionInner && + KNOWN_AUTHED_ONLY_FEEDS.includes(feed.uri) + ) { + return false + } + const alreadySaved = Boolean( + savedFeeds?.find(f => { + return f.value === feed.uri + }), + ) + return !alreadySaved + }) + + return page + }) + + return data + }, + [selectArgs /* Don't change. Everything needs to go into selectArgs. */], + ), }) + + useEffect(() => { + const {isFetching, hasNextPage, data} = query + if (isFetching || !hasNextPage) { + return + } + + // avoid double-fires of fetchNextPage() + if ( + lastPageCountRef.current !== 0 && + lastPageCountRef.current === data?.pages?.length + ) { + return + } + + // fetch next page if we haven't gotten a full page of content + let count = 0 + for (const page of data?.pages || []) { + count += page.feeds.length + } + if (count < limit && (data?.pages.length || 0) < 6) { + query.fetchNextPage() + lastPageCountRef.current = data?.pages?.length || 0 + } + }, [query, limit]) + + return query } export function useSearchPopularFeedsMutation() { diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx index 76ff4268fd..612559455f 100644 --- a/src/view/screens/Feeds.tsx +++ b/src/view/screens/Feeds.tsx @@ -104,22 +104,6 @@ type FlatlistSlice = key: string } -// HACK -// the protocol doesn't yet tell us which feeds are personalized -// this list is used to filter out feed recommendations from logged out users -// for the ones we know need it -// -prf -const KNOWN_AUTHED_ONLY_FEEDS = [ - 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/with-friends', // popular with friends, by bsky.app - 'at://did:plc:tenurhgjptubkk5zf5qhi3og/app.bsky.feed.generator/mutuals', // mutuals, by skyfeed - 'at://did:plc:tenurhgjptubkk5zf5qhi3og/app.bsky.feed.generator/only-posts', // only posts, by skyfeed - 'at://did:plc:wzsilnxf24ehtmmc3gssy5bu/app.bsky.feed.generator/mentions', // mentions, by flicknow - 'at://did:plc:q6gjnaw2blty4crticxkmujt/app.bsky.feed.generator/bangers', // my bangers, by jaz - 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/mutuals', // mutuals, by bluesky - 'at://did:plc:q6gjnaw2blty4crticxkmujt/app.bsky.feed.generator/my-followers', // followers, by jaz - 'at://did:plc:vpkhqolt662uhesyj6nxm7ys/app.bsky.feed.generator/followpics', // the gram, by why -] - export function FeedsScreen(_props: Props) { const pal = usePalette('default') const {openComposer} = useComposerControls() @@ -327,10 +311,7 @@ export function FeedsScreen(_props: Props) { type: 'popularFeedsLoading', }) } else { - if ( - !popularFeeds?.pages || - popularFeeds?.pages[0]?.feeds?.length === 0 - ) { + if (!popularFeeds?.pages) { slices.push({ key: 'popularFeedsNoResults', type: 'popularFeedsNoResults', @@ -338,26 +319,11 @@ export function FeedsScreen(_props: Props) { } else { for (const page of popularFeeds.pages || []) { slices = slices.concat( - page.feeds - .filter(feed => { - if ( - !hasSession && - KNOWN_AUTHED_ONLY_FEEDS.includes(feed.uri) - ) { - return false - } - const alreadySaved = Boolean( - preferences?.savedFeeds?.find(f => { - return f.value === feed.uri - }), - ) - return !alreadySaved - }) - .map(feed => ({ - key: `popularFeed:${feed.uri}`, - type: 'popularFeed', - feedUri: feed.uri, - })), + page.feeds.map(feed => ({ + key: `popularFeed:${feed.uri}`, + type: 'popularFeed', + feedUri: feed.uri, + })), ) } From fe3f872d493d3b80941bf496f1d5eb4c6d29fd6a Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 14 Jun 2024 11:56:43 -0500 Subject: [PATCH 13/81] Add known followers to shadow cache (#4517) --- src/state/cache/profile-shadow.ts | 2 ++ src/state/queries/known-followers.ts | 32 ++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/state/cache/profile-shadow.ts b/src/state/cache/profile-shadow.ts index 0a618ab3bb..dc907664e2 100644 --- a/src/state/cache/profile-shadow.ts +++ b/src/state/cache/profile-shadow.ts @@ -5,6 +5,7 @@ import EventEmitter from 'eventemitter3' import {batchedUpdates} from '#/lib/batchedUpdates' import {findAllProfilesInQueryData as findAllProfilesInActorSearchQueryData} from '../queries/actor-search' +import {findAllProfilesInQueryData as findAllProfilesInKnownFollowersQueryData} from '../queries/known-followers' import {findAllProfilesInQueryData as findAllProfilesInListMembersQueryData} from '../queries/list-members' import {findAllProfilesInQueryData as findAllProfilesInListConvosQueryData} from '../queries/messages/list-converations' import {findAllProfilesInQueryData as findAllProfilesInMyBlockedAccountsQueryData} from '../queries/my-blocked-accounts' @@ -111,4 +112,5 @@ function* findProfilesInCache( yield* findAllProfilesInListConvosQueryData(queryClient, did) yield* findAllProfilesInFeedsQueryData(queryClient, did) yield* findAllProfilesInPostThreadQueryData(queryClient, did) + yield* findAllProfilesInKnownFollowersQueryData(queryClient, did) } diff --git a/src/state/queries/known-followers.ts b/src/state/queries/known-followers.ts index adcbf4b502..fedd9b40f0 100644 --- a/src/state/queries/known-followers.ts +++ b/src/state/queries/known-followers.ts @@ -1,5 +1,10 @@ -import {AppBskyGraphGetKnownFollowers} from '@atproto/api' -import {InfiniteData, QueryKey, useInfiniteQuery} from '@tanstack/react-query' +import {AppBskyActorDefs, AppBskyGraphGetKnownFollowers} from '@atproto/api' +import { + InfiniteData, + QueryClient, + QueryKey, + useInfiniteQuery, +} from '@tanstack/react-query' import {useAgent} from '#/state/session' @@ -32,3 +37,26 @@ export function useProfileKnownFollowersQuery(did: string | undefined) { enabled: !!did, }) } + +export function* findAllProfilesInQueryData( + queryClient: QueryClient, + did: string, +): Generator { + const queryDatas = queryClient.getQueriesData< + InfiniteData + >({ + queryKey: [RQKEY_ROOT], + }) + for (const [_queryKey, queryData] of queryDatas) { + if (!queryData?.pages) { + continue + } + for (const page of queryData?.pages) { + for (const follow of page.followers) { + if (follow.did === did) { + yield follow + } + } + } + } +} From 641a36c21dd8adf9d57353c41dfbf7ec5a48c8cd Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 14 Jun 2024 09:59:02 -0700 Subject: [PATCH 14/81] version bump (#4519) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5f9a898fc9..e08aa9d29c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bsky.app", - "version": "1.86.0", + "version": "1.87.0", "private": true, "engines": { "node": ">=18" From f8c58a68a9a78689f9f636f3d739d9574a18ff7b Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 14 Jun 2024 11:59:53 -0500 Subject: [PATCH 15/81] Fix count again (#4516) --- src/components/KnownFollowers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/KnownFollowers.tsx b/src/components/KnownFollowers.tsx index a8bdb763d7..63f61ce856 100644 --- a/src/components/KnownFollowers.tsx +++ b/src/components/KnownFollowers.tsx @@ -100,7 +100,7 @@ function KnownFollowersInner({ moderation, } }) - const count = cachedKnownFollowers.count - Math.min(slice.length, 2) + const count = cachedKnownFollowers.count return ( Date: Fri, 14 Jun 2024 19:01:31 +0200 Subject: [PATCH 16/81] Resolve patch-package warnings (#4520) --- .../{expo-haptics+12.8.1.patch => expo-haptics+13.0.1.patch} | 4 ++-- ...{expo-updates+0.25.11.patch => expo-updates+0.25.14.patch} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename patches/{expo-haptics+12.8.1.patch => expo-haptics+13.0.1.patch} (96%) rename patches/{expo-updates+0.25.11.patch => expo-updates+0.25.14.patch} (96%) diff --git a/patches/expo-haptics+12.8.1.patch b/patches/expo-haptics+13.0.1.patch similarity index 96% rename from patches/expo-haptics+12.8.1.patch rename to patches/expo-haptics+13.0.1.patch index a95b56f3be..9c7b9a6663 100644 --- a/patches/expo-haptics+12.8.1.patch +++ b/patches/expo-haptics+13.0.1.patch @@ -1,9 +1,9 @@ diff --git a/node_modules/expo-haptics/android/src/main/java/expo/modules/haptics/HapticsModule.kt b/node_modules/expo-haptics/android/src/main/java/expo/modules/haptics/HapticsModule.kt -index 26c52af..b949a4c 100644 +index 1520465..6ea988a 100644 --- a/node_modules/expo-haptics/android/src/main/java/expo/modules/haptics/HapticsModule.kt +++ b/node_modules/expo-haptics/android/src/main/java/expo/modules/haptics/HapticsModule.kt @@ -42,7 +42,7 @@ class HapticsModule : Module() { - + private fun vibrate(type: HapticsVibrationType) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - vibrator.vibrate(VibrationEffect.createWaveform(type.timings, type.amplitudes, -1)) diff --git a/patches/expo-updates+0.25.11.patch b/patches/expo-updates+0.25.14.patch similarity index 96% rename from patches/expo-updates+0.25.11.patch rename to patches/expo-updates+0.25.14.patch index 5f9eceef48..6fc4fc5fcf 100644 --- a/patches/expo-updates+0.25.11.patch +++ b/patches/expo-updates+0.25.14.patch @@ -1,11 +1,11 @@ diff --git a/node_modules/expo-updates/ios/EXUpdates/Update/ExpoUpdatesUpdate.swift b/node_modules/expo-updates/ios/EXUpdates/Update/ExpoUpdatesUpdate.swift -index b85291e..07a5d3c 100644 +index b85291e..546709d 100644 --- a/node_modules/expo-updates/ios/EXUpdates/Update/ExpoUpdatesUpdate.swift +++ b/node_modules/expo-updates/ios/EXUpdates/Update/ExpoUpdatesUpdate.swift @@ -78,13 +78,20 @@ public final class ExpoUpdatesUpdate: Update { status = UpdateStatus.StatusPending } - + + // Instead of relying on various hacks to get the correct format for the specific + // platform on the backend, we can just add this little patch.. + let dateFormatter = DateFormatter() From 36e976fe5c35f66dee51d15b1767da90e8f7a817 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 14 Jun 2024 12:32:57 -0500 Subject: [PATCH 17/81] Redo explore page (#4491) * Redo explore page, wip * Remove circle icons * Load more styling * Lower limit * Some styling tweaks * Abstract * Add tab, query, factor out * Revert unneeded change * Revert unneeded change v2 * Update copy * Load more styling * Header styles * The thin blue line * Make sure it's hairline * Update query keys * Border * Expand avis * Very load more copy --- .../arrowBottom_stroke2_corner0_rounded.svg | 1 + src/components/icons/Arrow.tsx | 4 + src/state/queries/feed.ts | 34 +- src/state/queries/suggested-follows.ts | 13 +- src/view/screens/Search/Explore.tsx | 556 ++++++++++++++++++ src/view/screens/Search/Search.tsx | 141 ++--- 6 files changed, 656 insertions(+), 93 deletions(-) create mode 100644 assets/icons/arrowBottom_stroke2_corner0_rounded.svg create mode 100644 src/view/screens/Search/Explore.tsx diff --git a/assets/icons/arrowBottom_stroke2_corner0_rounded.svg b/assets/icons/arrowBottom_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..5f4a11e09a --- /dev/null +++ b/assets/icons/arrowBottom_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/src/components/icons/Arrow.tsx b/src/components/icons/Arrow.tsx index eb753e5493..d6fb635e96 100644 --- a/src/components/icons/Arrow.tsx +++ b/src/components/icons/Arrow.tsx @@ -7,3 +7,7 @@ export const ArrowTopRight_Stroke2_Corner0_Rounded = createSinglePathSVG({ export const ArrowLeft_Stroke2_Corner0_Rounded = createSinglePathSVG({ path: 'M3 12a1 1 0 0 1 .293-.707l6-6a1 1 0 0 1 1.414 1.414L6.414 11H20a1 1 0 1 1 0 2H6.414l4.293 4.293a1 1 0 0 1-1.414 1.414l-6-6A1 1 0 0 1 3 12Z', }) + +export const ArrowBottom_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 21a1 1 0 0 1-.707-.293l-6-6a1 1 0 1 1 1.414-1.414L11 17.586V4a1 1 0 1 1 2 0v13.586l4.293-4.293a1 1 0 0 1 1.414 1.414l-6 6A1 1 0 0 1 12 21Z', +}) diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts index fed23f5b12..2981b41b45 100644 --- a/src/state/queries/feed.ts +++ b/src/state/queries/feed.ts @@ -190,8 +190,10 @@ export const KNOWN_AUTHED_ONLY_FEEDS = [ type GetPopularFeedsOptions = {limit?: number} -export function createGetPopularFeedsQueryKey(...args: any[]) { - return ['getPopularFeeds', ...args] +export function createGetPopularFeedsQueryKey( + options?: GetPopularFeedsOptions, +) { + return ['getPopularFeeds', options] } export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) { @@ -299,6 +301,34 @@ export function useSearchPopularFeedsMutation() { }) } +const popularFeedsSearchQueryKeyRoot = 'popularFeedsSearch' +export const createPopularFeedsSearchQueryKey = (query: string) => [ + popularFeedsSearchQueryKeyRoot, + query, +] + +export function usePopularFeedsSearch({ + query, + enabled, +}: { + query: string + enabled?: boolean +}) { + const agent = useAgent() + return useQuery({ + enabled, + queryKey: createPopularFeedsSearchQueryKey(query), + queryFn: async () => { + const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({ + limit: 10, + query: query, + }) + + return res.data.feeds + }, + }) +} + export type SavedFeedSourceInfo = FeedSourceInfo & { savedFeed: AppBskyActorDefs.SavedFeed } diff --git a/src/state/queries/suggested-follows.ts b/src/state/queries/suggested-follows.ts index 59b8f7ed55..40251d43d7 100644 --- a/src/state/queries/suggested-follows.ts +++ b/src/state/queries/suggested-follows.ts @@ -23,7 +23,10 @@ import {useAgent, useSession} from '#/state/session' import {useModerationOpts} from '../preferences/moderation-opts' const suggestedFollowsQueryKeyRoot = 'suggested-follows' -const suggestedFollowsQueryKey = [suggestedFollowsQueryKeyRoot] +const suggestedFollowsQueryKey = (options?: SuggestedFollowsOptions) => [ + suggestedFollowsQueryKeyRoot, + options, +] const suggestedFollowsByActorQueryKeyRoot = 'suggested-follows-by-actor' const suggestedFollowsByActorQueryKey = (did: string) => [ @@ -31,7 +34,9 @@ const suggestedFollowsByActorQueryKey = (did: string) => [ did, ] -export function useSuggestedFollowsQuery() { +type SuggestedFollowsOptions = {limit?: number} + +export function useSuggestedFollowsQuery(options?: SuggestedFollowsOptions) { const {currentAccount} = useSession() const agent = useAgent() const moderationOpts = useModerationOpts() @@ -46,12 +51,12 @@ export function useSuggestedFollowsQuery() { >({ enabled: !!moderationOpts && !!preferences, staleTime: STALE.HOURS.ONE, - queryKey: suggestedFollowsQueryKey, + queryKey: suggestedFollowsQueryKey(options), queryFn: async ({pageParam}) => { const contentLangs = getContentLanguages().join(',') const res = await agent.app.bsky.actor.getSuggestions( { - limit: 25, + limit: options?.limit || 25, cursor: pageParam, }, { diff --git a/src/view/screens/Search/Explore.tsx b/src/view/screens/Search/Explore.tsx new file mode 100644 index 0000000000..f6e998838a --- /dev/null +++ b/src/view/screens/Search/Explore.tsx @@ -0,0 +1,556 @@ +import React from 'react' +import {View} from 'react-native' +import { + AppBskyActorDefs, + AppBskyFeedDefs, + moderateProfile, + ModerationDecision, + ModerationOpts, +} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {logger} from '#/logger' +import {isWeb} from '#/platform/detection' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useGetPopularFeedsQuery} from '#/state/queries/feed' +import {usePreferencesQuery} from '#/state/queries/preferences' +import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows' +import {useSession} from '#/state/session' +import {cleanError} from 'lib/strings/errors' +import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard' +import {List} from '#/view/com/util/List' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' +import { + FeedFeedLoadingPlaceholder, + ProfileCardFeedLoadingPlaceholder, +} from 'view/com/util/LoadingPlaceholder' +import {atoms as a, useTheme, ViewStyleProp} from '#/alf' +import {Button} from '#/components/Button' +import {ArrowBottom_Stroke2_Corner0_Rounded as ArrowBottom} from '#/components/icons/Arrow' +import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' +import {Props as SVGIconProps} from '#/components/icons/common' +import {ListSparkle_Stroke2_Corner0_Rounded as ListSparkle} from '#/components/icons/ListSparkle' +import {UserCircle_Stroke2_Corner0_Rounded as Person} from '#/components/icons/UserCircle' +import {Loader} from '#/components/Loader' +import {Text} from '#/components/Typography' + +function SuggestedItemsHeader({ + title, + description, + style, + icon: Icon, +}: { + title: string + description: string + icon: React.ComponentType +} & ViewStyleProp) { + const t = useTheme() + + return ( + + + + + {title} + + + {description} + + + + ) +} + +type LoadMoreItems = + | { + type: 'profile' + key: string + avatar: string + moderation: ModerationDecision + } + | { + type: 'feed' + key: string + avatar: string + moderation: undefined + } + +function LoadMore({ + item, + moderationOpts, +}: { + item: ExploreScreenItems & {type: 'loadMore'} + moderationOpts?: ModerationOpts +}) { + const t = useTheme() + const {_} = useLingui() + const items = React.useMemo(() => { + return item.items + .map(_item => { + if (_item.type === 'profile') { + return { + type: 'profile', + key: _item.profile.did, + avatar: _item.profile.avatar, + moderation: moderateProfile(_item.profile, moderationOpts!), + } + } else if (_item.type === 'feed') { + return { + type: 'feed', + key: _item.feed.uri, + avatar: _item.feed.avatar, + moderation: undefined, + } + } + return undefined + }) + .filter(Boolean) as LoadMoreItems[] + }, [item.items, moderationOpts]) + const type = items[0].type + + return ( + + + + ) +} + +type ExploreScreenItems = + | { + type: 'header' + key: string + title: string + description: string + style?: ViewStyleProp['style'] + icon: React.ComponentType + } + | { + type: 'profile' + key: string + profile: AppBskyActorDefs.ProfileViewBasic + } + | { + type: 'feed' + key: string + feed: AppBskyFeedDefs.GeneratorView + } + | { + type: 'loadMore' + key: string + isLoadingMore: boolean + onLoadMore: () => void + items: ExploreScreenItems[] + } + | { + type: 'profilePlaceholder' + key: string + } + | { + type: 'feedPlaceholder' + key: string + } + | { + type: 'error' + key: string + message: string + error: string + } + +export function Explore() { + const {_} = useLingui() + const t = useTheme() + const {hasSession} = useSession() + const {data: preferences, error: preferencesError} = usePreferencesQuery() + const moderationOpts = useModerationOpts() + const { + data: profiles, + hasNextPage: hasNextProfilesPage, + isLoading: isLoadingProfiles, + isFetchingNextPage: isFetchingNextProfilesPage, + error: profilesError, + fetchNextPage: fetchNextProfilesPage, + } = useSuggestedFollowsQuery({limit: 3}) + const { + data: feeds, + hasNextPage: hasNextFeedsPage, + isLoading: isLoadingFeeds, + isFetchingNextPage: isFetchingNextFeedsPage, + error: feedsError, + fetchNextPage: fetchNextFeedsPage, + } = useGetPopularFeedsQuery({limit: 3}) + + const isLoadingMoreProfiles = isFetchingNextProfilesPage && !isLoadingProfiles + const onLoadMoreProfiles = React.useCallback(async () => { + if (isFetchingNextProfilesPage || !hasNextProfilesPage || profilesError) + return + try { + await fetchNextProfilesPage() + } catch (err) { + logger.error('Failed to load more suggested follows', {message: err}) + } + }, [ + isFetchingNextProfilesPage, + hasNextProfilesPage, + profilesError, + fetchNextProfilesPage, + ]) + + const isLoadingMoreFeeds = isFetchingNextFeedsPage && !isLoadingFeeds + const onLoadMoreFeeds = React.useCallback(async () => { + if (isFetchingNextFeedsPage || !hasNextFeedsPage || feedsError) return + try { + await fetchNextFeedsPage() + } catch (err) { + logger.error('Failed to load more suggested follows', {message: err}) + } + }, [ + isFetchingNextFeedsPage, + hasNextFeedsPage, + feedsError, + fetchNextFeedsPage, + ]) + + const items = React.useMemo(() => { + const i: ExploreScreenItems[] = [ + { + type: 'header', + key: 'suggested-follows-header', + title: _(msg`Suggested accounts`), + description: _( + msg`Follow more accounts to get connected to your interests and build your network.`, + ), + icon: Person, + }, + ] + + if (profiles) { + // Currently the responses contain duplicate items. + // Needs to be fixed on backend, but let's dedupe to be safe. + let seen = new Set() + for (const page of profiles.pages) { + for (const actor of page.actors) { + if (!seen.has(actor.did)) { + seen.add(actor.did) + i.push({ + type: 'profile', + key: actor.did, + profile: actor, + }) + } + } + } + + i.push({ + type: 'loadMore', + key: 'loadMoreProfiles', + isLoadingMore: isLoadingMoreProfiles, + onLoadMore: onLoadMoreProfiles, + items: i.filter(item => item.type === 'profile').slice(-3), + }) + } else { + if (profilesError) { + i.push({ + type: 'error', + key: 'profilesError', + message: _(msg`Failed to load suggested follows`), + error: cleanError(profilesError), + }) + } else { + i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'}) + } + } + + i.push({ + type: 'header', + key: 'suggested-feeds-header', + title: _(msg`Discover new feeds`), + description: _( + msg`Custom feeds built by the community bring you new experiences and help you find the content you love.`, + ), + style: [a.pt_5xl], + icon: ListSparkle, + }) + + if (feeds && preferences) { + // Currently the responses contain duplicate items. + // Needs to be fixed on backend, but let's dedupe to be safe. + let seen = new Set() + for (const page of feeds.pages) { + for (const feed of page.feeds) { + if (!seen.has(feed.uri)) { + seen.add(feed.uri) + i.push({ + type: 'feed', + key: feed.uri, + feed, + }) + } + } + } + + if (feedsError) { + i.push({ + type: 'error', + key: 'feedsError', + message: _(msg`Failed to load suggested feeds`), + error: cleanError(feedsError), + }) + } else if (preferencesError) { + i.push({ + type: 'error', + key: 'preferencesError', + message: _(msg`Failed to load feeds preferences`), + error: cleanError(preferencesError), + }) + } else { + i.push({ + type: 'loadMore', + key: 'loadMoreFeeds', + isLoadingMore: isLoadingMoreFeeds, + onLoadMore: onLoadMoreFeeds, + items: i.filter(item => item.type === 'feed').slice(-3), + }) + } + } else { + if (feedsError) { + i.push({ + type: 'error', + key: 'feedsError', + message: _(msg`Failed to load suggested feeds`), + error: cleanError(feedsError), + }) + } else if (preferencesError) { + i.push({ + type: 'error', + key: 'preferencesError', + message: _(msg`Failed to load feeds preferences`), + error: cleanError(preferencesError), + }) + } else { + i.push({type: 'feedPlaceholder', key: 'feedPlaceholder'}) + } + } + + return i + }, [ + _, + profiles, + feeds, + preferences, + onLoadMoreFeeds, + onLoadMoreProfiles, + isLoadingMoreProfiles, + isLoadingMoreFeeds, + profilesError, + feedsError, + preferencesError, + ]) + + const renderItem = React.useCallback( + ({item}: {item: ExploreScreenItems}) => { + switch (item.type) { + case 'header': { + return ( + + ) + } + case 'profile': { + return ( + + + + ) + } + case 'feed': { + return ( + + + + ) + } + case 'loadMore': { + return + } + case 'profilePlaceholder': { + return + } + case 'feedPlaceholder': { + return + } + case 'error': { + return ( + + + + + + {item.message} + + + {item.error} + + + + + ) + } + } + }, + [t, hasSession, moderationOpts], + ) + + return ( + item.key} + // @ts-ignore web only -prf + desktopFixedHeight + contentContainerStyle={{paddingBottom: 200}} + keyboardShouldPersistTaps="handled" + keyboardDismissMode="on-drag" + /> + ) +} diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx index b6daf84b38..f1b0301d00 100644 --- a/src/view/screens/Search/Search.tsx +++ b/src/view/screens/Search/Search.tsx @@ -29,15 +29,14 @@ import {MagnifyingGlassIcon} from '#/lib/icons' import {makeProfileLink} from '#/lib/routes/links' import {NavigationProp} from '#/lib/routes/types' import {augmentSearchQuery} from '#/lib/strings/helpers' -import {s} from '#/lib/styles' import {logger} from '#/logger' import {isIOS, isNative, isWeb} from '#/platform/detection' import {listenSoftReset} from '#/state/events' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' import {useActorSearch} from '#/state/queries/actor-search' +import {usePopularFeedsSearch} from '#/state/queries/feed' import {useSearchPostsQuery} from '#/state/queries/search-posts' -import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows' import {useSession} from '#/state/session' import {useSetDrawerOpen} from '#/state/shell' import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell' @@ -56,8 +55,9 @@ import {Link} from '#/view/com/util/Link' import {List} from '#/view/com/util/List' import {Text} from '#/view/com/util/text/Text' import {CenteredView, ScrollView} from '#/view/com/util/Views' +import {Explore} from '#/view/screens/Search/Explore' import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search' -import {ProfileCardFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder' +import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' import {atoms as a} from '#/alf' import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu' @@ -122,70 +122,6 @@ function EmptyState({message, error}: {message: string; error?: string}) { ) } -function useSuggestedFollows(): [ - AppBskyActorDefs.ProfileViewBasic[], - () => void, -] { - const { - data: suggestions, - hasNextPage, - isFetchingNextPage, - isError, - fetchNextPage, - } = useSuggestedFollowsQuery() - - const onEndReached = React.useCallback(async () => { - if (isFetchingNextPage || !hasNextPage || isError) return - try { - await fetchNextPage() - } catch (err) { - logger.error('Failed to load more suggested follows', {message: err}) - } - }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) - - const items: AppBskyActorDefs.ProfileViewBasic[] = [] - if (suggestions) { - // Currently the responses contain duplicate items. - // Needs to be fixed on backend, but let's dedupe to be safe. - let seen = new Set() - for (const page of suggestions.pages) { - for (const actor of page.actors) { - if (!seen.has(actor.did)) { - seen.add(actor.did) - items.push(actor) - } - } - } - } - return [items, onEndReached] -} - -let SearchScreenSuggestedFollows = (_props: {}): React.ReactNode => { - const pal = usePalette('default') - const [suggestions, onEndReached] = useSuggestedFollows() - - return suggestions.length ? ( - } - keyExtractor={item => item.did} - // @ts-ignore web only -prf - desktopFixedHeight - contentContainerStyle={{paddingBottom: 200}} - keyboardShouldPersistTaps="handled" - keyboardDismissMode="on-drag" - onEndReached={onEndReached} - onEndReachedThreshold={2} - /> - ) : ( - - - - - ) -} -SearchScreenSuggestedFollows = React.memo(SearchScreenSuggestedFollows) - type SearchResultSlice = | { type: 'post' @@ -342,6 +278,50 @@ let SearchScreenUserResults = ({ } SearchScreenUserResults = React.memo(SearchScreenUserResults) +let SearchScreenFeedsResults = ({ + query, + active, +}: { + query: string + active: boolean +}): React.ReactNode => { + const {_} = useLingui() + const {hasSession} = useSession() + + const {data: results, isFetched} = usePopularFeedsSearch({ + query, + enabled: active, + }) + + return isFetched && results ? ( + <> + {results.length ? ( + ( + + )} + keyExtractor={item => item.did} + // @ts-ignore web only -prf + desktopFixedHeight + contentContainerStyle={{paddingBottom: 100}} + /> + ) : ( + + )} + + ) : ( + + ) +} +SearchScreenFeedsResults = React.memo(SearchScreenFeedsResults) + let SearchScreenInner = ({query}: {query?: string}): React.ReactNode => { const pal = usePalette('default') const setMinimalShellMode = useSetMinimalShellMode() @@ -389,6 +369,12 @@ let SearchScreenInner = ({query}: {query?: string}): React.ReactNode => { ), }, + { + title: _(msg`Feeds`), + component: ( + + ), + }, ] }, [_, query, activeTab]) @@ -408,26 +394,7 @@ let SearchScreenInner = ({query}: {query?: string}): React.ReactNode => { ))} ) : hasSession ? ( - - - - Suggested Follows - - - - - + ) : ( Date: Sat, 15 Jun 2024 02:39:08 +0900 Subject: [PATCH 18/81] Fix kawaii logo (#4505) --- src/view/com/home/HomeHeaderLayout.web.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/view/com/home/HomeHeaderLayout.web.tsx b/src/view/com/home/HomeHeaderLayout.web.tsx index 77bdba51fd..28f29ec787 100644 --- a/src/view/com/home/HomeHeaderLayout.web.tsx +++ b/src/view/com/home/HomeHeaderLayout.web.tsx @@ -57,6 +57,7 @@ function HomeHeaderLayoutDesktopAndTablet({ t.atoms.bg, t.atoms.border_contrast_low, styles.bar, + kawaii && {paddingTop: 22, paddingBottom: 16}, ]}> From 5751014117ff87a0b5188841b468095f00305ed5 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 14 Jun 2024 14:24:04 -0500 Subject: [PATCH 19/81] Feed source card (#4512) * Pass event through click handlers * Add FeedCard, use in Feeds screen * Tweak space * Don't contrain rt height * Tweak space * Fix type errors, don't pass event to fns that don't expect it * Show unresolved RT prior to facet resolution --- src/components/FeedCard.tsx | 198 ++++++++++++++++++ src/components/Prompt.tsx | 17 +- src/components/dms/LeaveConvoPrompt.tsx | 2 +- .../Profile/Header/ProfileHeaderLabeler.tsx | 2 +- src/view/com/util/post-embeds/GifEmbed.tsx | 2 +- src/view/screens/Feeds.tsx | 22 +- 6 files changed, 222 insertions(+), 21 deletions(-) create mode 100644 src/components/FeedCard.tsx diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx new file mode 100644 index 0000000000..2745ed7c9f --- /dev/null +++ b/src/components/FeedCard.tsx @@ -0,0 +1,198 @@ +import React from 'react' +import {GestureResponderEvent, View} from 'react-native' +import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api' +import {msg, plural, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {logger} from '#/logger' +import { + useAddSavedFeedsMutation, + usePreferencesQuery, + useRemoveFeedMutation, +} from '#/state/queries/preferences' +import {sanitizeHandle} from 'lib/strings/handles' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import * as Toast from 'view/com/util/Toast' +import {useTheme} from '#/alf' +import {atoms as a} from '#/alf' +import {Button, ButtonIcon} from '#/components/Button' +import {useRichText} from '#/components/hooks/useRichText' +import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' +import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' +import {Link as InternalLink} from '#/components/Link' +import {Loader} from '#/components/Loader' +import * as Prompt from '#/components/Prompt' +import {RichText} from '#/components/RichText' +import {Text} from '#/components/Typography' + +export function Default({feed}: {feed: AppBskyFeedDefs.GeneratorView}) { + return ( + + +
+ + + +
+ + +
+ + ) +} + +export function Link({ + children, + feed, +}: { + children: React.ReactElement + feed: AppBskyFeedDefs.GeneratorView +}) { + const href = React.useMemo(() => { + const urip = new AtUri(feed.uri) + const handleOrDid = feed.creator.handle || feed.creator.did + return `/profile/${handleOrDid}/feed/${urip.rkey}` + }, [feed]) + return {children} +} + +export function Outer({children}: {children: React.ReactNode}) { + return {children} +} + +export function Header({children}: {children: React.ReactNode}) { + return {children} +} + +export function Avatar({src}: {src: string | undefined}) { + return +} + +export function TitleAndByline({ + title, + creator, +}: { + title: string + creator: AppBskyActorDefs.ProfileViewBasic +}) { + const t = useTheme() + + return ( + + + {title} + + + Feed by {sanitizeHandle(creator.handle, '@')} + + + ) +} + +export function Description({description}: {description?: string}) { + const [rt, isResolving] = useRichText(description || '') + if (!description) return null + return isResolving ? ( + + ) : ( + + ) +} + +export function Likes({count}: {count: number}) { + const t = useTheme() + return ( + + {plural(count || 0, { + one: 'Liked by # user', + other: 'Liked by # users', + })} + + ) +} + +export function Action({uri, pin}: {uri: string; pin?: boolean}) { + const {_} = useLingui() + const {data: preferences} = usePreferencesQuery() + const {isPending: isAddSavedFeedPending, mutateAsync: saveFeeds} = + useAddSavedFeedsMutation() + const {isPending: isRemovePending, mutateAsync: removeFeed} = + useRemoveFeedMutation() + const savedFeedConfig = React.useMemo(() => { + return preferences?.savedFeeds?.find( + feed => feed.type === 'feed' && feed.value === uri, + ) + }, [preferences?.savedFeeds, uri]) + const removePromptControl = Prompt.usePromptControl() + const isPending = isAddSavedFeedPending || isRemovePending + + const toggleSave = React.useCallback( + async (e: GestureResponderEvent) => { + e.preventDefault() + e.stopPropagation() + + try { + if (savedFeedConfig) { + await removeFeed(savedFeedConfig) + } else { + await saveFeeds([ + { + type: 'feed', + value: uri, + pinned: pin || false, + }, + ]) + } + Toast.show(_(msg`Feeds updated!`)) + } catch (e: any) { + logger.error(e, {context: `FeedCard: failed to update feeds`, pin}) + Toast.show(_(msg`Failed to update feeds`)) + } + }, + [_, pin, saveFeeds, removeFeed, uri, savedFeedConfig], + ) + + const onPrompRemoveFeed = React.useCallback( + async (e: GestureResponderEvent) => { + e.preventDefault() + e.stopPropagation() + + removePromptControl.open() + }, + [removePromptControl], + ) + + return ( + <> + + + + + ) +} diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx index d05cab5ab6..315ad0dfda 100644 --- a/src/components/Prompt.tsx +++ b/src/components/Prompt.tsx @@ -1,10 +1,10 @@ import React from 'react' -import {View} from 'react-native' +import {GestureResponderEvent, View} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {atoms as a, useBreakpoints, useTheme} from '#/alf' -import {Button, ButtonColor, ButtonText} from '#/components/Button' +import {Button, ButtonColor, ButtonProps, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {Text} from '#/components/Typography' @@ -136,7 +136,7 @@ export function Action({ * Note: The dialog will close automatically when the action is pressed, you * should NOT close the dialog as a side effect of this method. */ - onPress: () => void + onPress: ButtonProps['onPress'] color?: ButtonColor /** * Optional i18n string. If undefined, it will default to "Confirm". @@ -147,9 +147,12 @@ export function Action({ const {_} = useLingui() const {gtMobile} = useBreakpoints() const {close} = Dialog.useDialogContext() - const handleOnPress = React.useCallback(() => { - close(onPress) - }, [close, onPress]) + const handleOnPress = React.useCallback( + (e: GestureResponderEvent) => { + close(() => onPress?.(e)) + }, + [close, onPress], + ) return ( + + + + + + + Say hello! + + + + {profileName} joined Bluesky{' '} + {timeAgo(createdAt, {format: 'long'})} ago + + + + + +
+ ) +} diff --git a/src/components/icons/Newskie.tsx b/src/components/icons/Newskie.tsx new file mode 100644 index 0000000000..ddbb33201e --- /dev/null +++ b/src/components/icons/Newskie.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Newskie = createSinglePathSVG({ + path: 'M11.183 8.561c0 .544.348.984.892.984.545 0 .893-.44.893-.985V6.985c0-.544-.348-.985-.893-.985-.543 0-.892.44-.892.985v1.576Zm5.94 7.481c0 .539-.438.942-.976.942H8.004c-.538 0-.975-.411-.975-.95 0-2.782 2.264-5.021 5.046-5.021 2.783 0 5.047 2.247 5.047 5.03Zm-.43-4.584a.983.983 0 0 1 0-1.393l1.114-1.114a.985.985 0 0 1 1.393 1.393l-1.114 1.114a.985.985 0 0 1-1.393 0Zm2.897 3.741h1.575c.544 0 .985.349.985.892 0 .544-.44.892-.985.892h-1.67a.872.872 0 0 1-.89-.887c0-.543.44-.897.985-.897Zm-14.045.893c0-.544-.44-.892-.985-.892H2.985c-.544 0-.985.349-.985.892 0 .544.44.892.985.892H4.56c.545 0 .985-.349.985-.892Zm1.913-6.027a.985.985 0 0 1-1.393 1.393L4.95 10.344A.985.985 0 0 1 6.344 8.95l1.114 1.114Z', +}) diff --git a/src/screens/Profile/Header/Handle.tsx b/src/screens/Profile/Header/Handle.tsx index 9ab24fbbed..4f438a2868 100644 --- a/src/screens/Profile/Header/Handle.tsx +++ b/src/screens/Profile/Header/Handle.tsx @@ -5,7 +5,9 @@ import {Trans} from '@lingui/macro' import {Shadow} from '#/state/cache/types' import {isInvalidHandle} from 'lib/strings/handles' +import {isAndroid} from 'platform/detection' import {atoms as a, useTheme, web} from '#/alf' +import {NewskieDialog} from '#/components/NewskieDialog' import {Text} from '#/components/Typography' export function ProfileHeaderHandle({ @@ -17,7 +19,10 @@ export function ProfileHeaderHandle({ const invalidHandle = isInvalidHandle(profile.handle) const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy return ( - + + {profile.viewer?.followedBy && !blockHide ? ( From 35e54e24a0b08ce0f2e3389aeb4fb0f29778170e Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 18 Jun 2024 13:37:14 -0500 Subject: [PATCH 36/81] Explore fixes (#4540) * Use safe check, check for next page, handle varied lengths * Fix border width * Move safe check * Add font_heavy and use it on the explore page headers --------- Co-authored-by: Paul Frazee --- src/alf/atoms.ts | 3 +++ src/alf/tokens.ts | 1 + src/view/screens/Search/Explore.tsx | 31 ++++++++++++++++++----------- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts index 1ccb0460c4..1dc2dfa7b0 100644 --- a/src/alf/atoms.ts +++ b/src/alf/atoms.ts @@ -267,6 +267,9 @@ export const atoms = { font_bold: { fontWeight: tokens.fontWeight.bold, }, + font_heavy: { + fontWeight: tokens.fontWeight.heavy, + }, italic: { fontStyle: 'italic', }, diff --git a/src/alf/tokens.ts b/src/alf/tokens.ts index 1bddd95d43..675844e296 100644 --- a/src/alf/tokens.ts +++ b/src/alf/tokens.ts @@ -118,6 +118,7 @@ export const fontWeight = { normal: '400', semibold: '500', bold: '600', + heavy: '700', } as const export const gradients = { diff --git a/src/view/screens/Search/Explore.tsx b/src/view/screens/Search/Explore.tsx index c7f5f939fe..dd93bf8130 100644 --- a/src/view/screens/Search/Explore.tsx +++ b/src/view/screens/Search/Explore.tsx @@ -64,7 +64,7 @@ function SuggestedItemsHeader({ fill={t.palette.primary_500} style={{marginLeft: -2}} /> - {title} + {title} {description} @@ -119,6 +119,9 @@ function LoadMore({ }) .filter(Boolean) as LoadMoreItems[] }, [item.items, moderationOpts]) + + if (items.length === 0) return null + const type = items[0].type return ( @@ -142,20 +145,20 @@ function LoadMore({ a.relative, { height: 32, - width: 32 + 15 * 3, + width: 32 + 15 * items.length, }, ]}> item.type === 'profile').slice(-3), - }) + if (hasNextProfilesPage) { + i.push({ + type: 'loadMore', + key: 'loadMoreProfiles', + isLoadingMore: isLoadingMoreProfiles, + onLoadMore: onLoadMoreProfiles, + items: i.filter(item => item.type === 'profile').slice(-3), + }) + } } else { if (profilesError) { i.push({ @@ -412,7 +417,7 @@ export function Explore() { message: _(msg`Failed to load feeds preferences`), error: cleanError(preferencesError), }) - } else { + } else if (hasNextFeedsPage) { i.push({ type: 'loadMore', key: 'loadMoreFeeds', @@ -454,6 +459,8 @@ export function Explore() { profilesError, feedsError, preferencesError, + hasNextProfilesPage, + hasNextFeedsPage, ]) const renderItem = React.useCallback( From 5f5d845053e13169f89fc70a3f858b0a9e5ed4fd Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 18 Jun 2024 19:48:34 +0100 Subject: [PATCH 37/81] Server-side thread mutes (#4518) * update atproto/api * move thread mutes to server side * rm log * move muted threads provider to inside did key * use map instead of object --- package.json | 2 +- src/App.native.tsx | 76 ++++++++++----------- src/App.web.tsx | 72 +++++++++---------- src/lib/statsig/events.ts | 2 + src/state/cache/thread-mutes.tsx | 44 ++++++++++++ src/state/muted-threads.tsx | 62 ----------------- src/state/queries/notifications/feed.ts | 3 - src/state/queries/notifications/unread.tsx | 5 +- src/state/queries/notifications/util.ts | 50 -------------- src/state/queries/post.ts | 70 +++++++++++++++++++ src/view/com/util/forms/PostDropdownBtn.tsx | 45 ++++++------ src/view/com/util/post-ctrls/PostCtrls.tsx | 4 +- yarn.lock | 8 +-- 13 files changed, 223 insertions(+), 220 deletions(-) create mode 100644 src/state/cache/thread-mutes.tsx delete mode 100644 src/state/muted-threads.tsx diff --git a/package.json b/package.json index 29e198c9cc..4178369035 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web" }, "dependencies": { - "@atproto/api": "^0.12.18", + "@atproto/api": "^0.12.19", "@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/App.native.tsx b/src/App.native.tsx index 322e944a47..18461fdd05 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -14,40 +14,40 @@ import * as SplashScreen from 'expo-splash-screen' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useIntentHandler} from '#/lib/hooks/useIntentHandler' +import {QueryProvider} from '#/lib/react-query' import { initialize, Provider as StatsigProvider, tryFetchGates, } from '#/lib/statsig/statsig' +import {s} from '#/lib/styles' +import {ThemeProvider} from '#/lib/ThemeContext' import {logger} from '#/logger' +import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' +import {Provider as DialogStateProvider} from '#/state/dialogs' +import {Provider as InvitesStateProvider} from '#/state/invites' +import {Provider as LightboxStateProvider} from '#/state/lightbox' import {MessagesProvider} from '#/state/messages' +import {Provider as ModalStateProvider} from '#/state/modals' import {init as initPersistedState} from '#/state/persisted' +import {Provider as PrefsStateProvider} from '#/state/preferences' import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs' import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts' -import {readLastActiveAccount} from '#/state/session/util' -import {useIntentHandler} from 'lib/hooks/useIntentHandler' -import {QueryProvider} from 'lib/react-query' -import {s} from 'lib/styles' -import {ThemeProvider} from 'lib/ThemeContext' -import {Provider as DialogStateProvider} from 'state/dialogs' -import {Provider as InvitesStateProvider} from 'state/invites' -import {Provider as LightboxStateProvider} from 'state/lightbox' -import {Provider as ModalStateProvider} from 'state/modals' -import {Provider as MutedThreadsProvider} from 'state/muted-threads' -import {Provider as PrefsStateProvider} from 'state/preferences' -import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' +import {Provider as UnreadNotifsProvider} from '#/state/queries/notifications/unread' import { Provider as SessionProvider, SessionAccount, useSession, useSessionApi, -} from 'state/session' -import {Provider as ShellStateProvider} from 'state/shell' -import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out' -import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed' -import {TestCtrls} from 'view/com/testing/TestCtrls' -import * as Toast from 'view/com/util/Toast' -import {Shell} from 'view/shell' +} from '#/state/session' +import {readLastActiveAccount} from '#/state/session/util' +import {Provider as ShellStateProvider} from '#/state/shell' +import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out' +import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' +import {TestCtrls} from '#/view/com/testing/TestCtrls' +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 {Provider as PortalProvider} from '#/components/Portal' @@ -112,10 +112,12 @@ function InnerApp() { - - - - + + + + + + @@ -154,21 +156,19 @@ function App() { - - - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index 5c4dc4e637..6af3c7d6fb 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -8,35 +8,35 @@ import {SafeAreaProvider} from 'react-native-safe-area-context' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useIntentHandler} from '#/lib/hooks/useIntentHandler' +import {QueryProvider} from '#/lib/react-query' import {Provider as StatsigProvider} from '#/lib/statsig/statsig' +import {ThemeProvider} from '#/lib/ThemeContext' import {logger} from '#/logger' +import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' +import {Provider as DialogStateProvider} from '#/state/dialogs' +import {Provider as InvitesStateProvider} from '#/state/invites' +import {Provider as LightboxStateProvider} from '#/state/lightbox' import {MessagesProvider} from '#/state/messages' +import {Provider as ModalStateProvider} from '#/state/modals' import {init as initPersistedState} from '#/state/persisted' +import {Provider as PrefsStateProvider} from '#/state/preferences' import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs' import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts' -import {readLastActiveAccount} from '#/state/session/util' -import {useIntentHandler} from 'lib/hooks/useIntentHandler' -import {QueryProvider} from 'lib/react-query' -import {ThemeProvider} from 'lib/ThemeContext' -import {Provider as DialogStateProvider} from 'state/dialogs' -import {Provider as InvitesStateProvider} from 'state/invites' -import {Provider as LightboxStateProvider} from 'state/lightbox' -import {Provider as ModalStateProvider} from 'state/modals' -import {Provider as MutedThreadsProvider} from 'state/muted-threads' -import {Provider as PrefsStateProvider} from 'state/preferences' -import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' +import {Provider as UnreadNotifsProvider} from '#/state/queries/notifications/unread' import { Provider as SessionProvider, SessionAccount, useSession, useSessionApi, -} from 'state/session' -import {Provider as ShellStateProvider} from 'state/shell' -import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out' -import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed' -import * as Toast from 'view/com/util/Toast' -import {ToastContainer} from 'view/com/util/Toast.web' -import {Shell} from 'view/shell/index' +} from '#/state/session' +import {readLastActiveAccount} from '#/state/session/util' +import {Provider as ShellStateProvider} from '#/state/shell' +import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out' +import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' +import * as Toast from '#/view/com/util/Toast' +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 {Provider as PortalProvider} from '#/components/Portal' @@ -96,9 +96,11 @@ function InnerApp() { - - - + + + + + @@ -136,21 +138,19 @@ function App() { - - - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index 9939f60c9c..0d77ec8a36 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -103,6 +103,8 @@ export type LogEvents = { 'post:unrepost': { logContext: 'FeedItem' | 'PostThreadItem' | 'Post' } + 'post:mute': {} + 'post:unmute': {} 'profile:follow': { didBecomeMutual: boolean | undefined followeeClout: number | undefined diff --git a/src/state/cache/thread-mutes.tsx b/src/state/cache/thread-mutes.tsx new file mode 100644 index 0000000000..b58bd430f5 --- /dev/null +++ b/src/state/cache/thread-mutes.tsx @@ -0,0 +1,44 @@ +import React from 'react' + +type StateContext = Map +type SetStateContext = (uri: string, value: boolean) => void + +const stateContext = React.createContext(new Map()) +const setStateContext = React.createContext( + (_: string) => false, +) + +export function Provider({children}: React.PropsWithChildren<{}>) { + const [state, setState] = React.useState(() => new Map()) + + const setThreadMute = React.useCallback( + (uri: string, value: boolean) => { + setState(prev => { + const next = new Map(prev) + next.set(uri, value) + return next + }) + }, + [setState], + ) + return ( + + + {children} + + + ) +} + +export function useMutedThreads() { + return React.useContext(stateContext) +} + +export function useIsThreadMuted(uri: string, defaultValue = false) { + const state = React.useContext(stateContext) + return state.get(uri) ?? defaultValue +} + +export function useSetThreadMute() { + return React.useContext(setStateContext) +} diff --git a/src/state/muted-threads.tsx b/src/state/muted-threads.tsx deleted file mode 100644 index 84a717eb79..0000000000 --- a/src/state/muted-threads.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react' -import * as persisted from '#/state/persisted' -import {track} from '#/lib/analytics/analytics' - -type StateContext = persisted.Schema['mutedThreads'] -type ToggleContext = (uri: string) => boolean - -const stateContext = React.createContext( - persisted.defaults.mutedThreads, -) -const toggleContext = React.createContext((_: string) => false) - -export function Provider({children}: React.PropsWithChildren<{}>) { - const [state, setState] = React.useState(persisted.get('mutedThreads')) - - const toggleThreadMute = React.useCallback( - (uri: string) => { - let muted = false - setState((arr: string[]) => { - if (arr.includes(uri)) { - arr = arr.filter(v => v !== uri) - muted = false - track('Post:ThreadUnmute') - } else { - arr = arr.concat([uri]) - muted = true - track('Post:ThreadMute') - } - persisted.write('mutedThreads', arr) - return arr - }) - return muted - }, - [setState], - ) - - React.useEffect(() => { - return persisted.onUpdate(() => { - setState(persisted.get('mutedThreads')) - }) - }, [setState]) - - return ( - - - {children} - - - ) -} - -export function useMutedThreads() { - return React.useContext(stateContext) -} - -export function useToggleThreadMute() { - return React.useContext(toggleContext) -} - -export function isThreadMuted(uri: string) { - return persisted.get('mutedThreads').includes(uri) -} diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts index d9f019af38..0607f07a10 100644 --- a/src/state/queries/notifications/feed.ts +++ b/src/state/queries/notifications/feed.ts @@ -26,7 +26,6 @@ import { useQueryClient, } from '@tanstack/react-query' -import {useMutedThreads} from '#/state/muted-threads' import {useAgent} from '#/state/session' import {useModerationOpts} from '../../preferences/moderation-opts' import {STALE} from '..' @@ -54,7 +53,6 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) { const agent = useAgent() const queryClient = useQueryClient() const moderationOpts = useModerationOpts() - const threadMutes = useMutedThreads() const unreads = useUnreadNotificationsApi() const enabled = opts?.enabled !== false const lastPageCountRef = useRef(0) @@ -82,7 +80,6 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) { cursor: pageParam, queryClient, moderationOpts, - threadMutes, fetchAdditionalData: true, }) ).page diff --git a/src/state/queries/notifications/unread.tsx b/src/state/queries/notifications/unread.tsx index ffb8d03bc7..7bb325ea98 100644 --- a/src/state/queries/notifications/unread.tsx +++ b/src/state/queries/notifications/unread.tsx @@ -9,7 +9,6 @@ import EventEmitter from 'eventemitter3' import BroadcastChannel from '#/lib/broadcast' import {logger} from '#/logger' -import {useMutedThreads} from '#/state/muted-threads' import {useAgent, useSession} from '#/state/session' import {resetBadgeCount} from 'lib/notifications/notifications' import {useModerationOpts} from '../../preferences/moderation-opts' @@ -48,7 +47,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const agent = useAgent() const queryClient = useQueryClient() const moderationOpts = useModerationOpts() - const threadMutes = useMutedThreads() const [numUnread, setNumUnread] = React.useState('') @@ -147,7 +145,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) { limit: 40, queryClient, moderationOpts, - threadMutes, // only fetch subjects when the page is going to be used // in the notifications query, otherwise skip it @@ -192,7 +189,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { } }, } - }, [setNumUnread, queryClient, moderationOpts, threadMutes, agent]) + }, [setNumUnread, queryClient, moderationOpts, agent]) checkUnreadRef.current = api.checkUnread return ( diff --git a/src/state/queries/notifications/util.ts b/src/state/queries/notifications/util.ts index 4662493533..8ed1c0390c 100644 --- a/src/state/queries/notifications/util.ts +++ b/src/state/queries/notifications/util.ts @@ -1,5 +1,4 @@ import { - AppBskyEmbedRecord, AppBskyFeedDefs, AppBskyFeedLike, AppBskyFeedPost, @@ -28,7 +27,6 @@ export async function fetchPage({ limit, queryClient, moderationOpts, - threadMutes, fetchAdditionalData, }: { agent: BskyAgent @@ -36,7 +34,6 @@ export async function fetchPage({ limit: number queryClient: QueryClient moderationOpts: ModerationOpts | undefined - threadMutes: string[] fetchAdditionalData: boolean }): Promise<{page: FeedPage; indexedAt: string | undefined}> { const res = await agent.listNotifications({ @@ -67,11 +64,6 @@ export async function fetchPage({ } } - // apply thread muting - notifsGrouped = notifsGrouped.filter( - notif => !isThreadMuted(notif, threadMutes), - ) - let seenAt = res.data.seenAt ? new Date(res.data.seenAt) : new Date() if (Number.isNaN(seenAt.getTime())) { seenAt = new Date() @@ -207,45 +199,3 @@ function getSubjectUri( return notif.reasonSubject } } - -export function isThreadMuted(notif: FeedNotification, threadMutes: string[]) { - // If there's a subject we want to use that. This will always work on the notifications tab - if (notif.subject) { - const record = notif.subject.record as AppBskyFeedPost.Record - // Check for a quote record - if ( - (record.reply && threadMutes.includes(record.reply.root.uri)) || - (notif.subject.uri && threadMutes.includes(notif.subject.uri)) - ) { - return true - } else if ( - AppBskyEmbedRecord.isMain(record.embed) && - threadMutes.includes(record.embed.record.uri) - ) { - return true - } - } else { - // Otherwise we just do the best that we can - const record = notif.notification.record - if (AppBskyFeedPost.isRecord(record)) { - if (record.reply && threadMutes.includes(record.reply.root.uri)) { - // We can always filter replies - return true - } else if ( - AppBskyEmbedRecord.isMain(record.embed) && - threadMutes.includes(record.embed.record.uri) - ) { - // We can also filter quotes if the quoted post is the root - return true - } - } else if ( - AppBskyFeedRepost.isRecord(record) && - threadMutes.includes(record.subject.uri) - ) { - // Finally we can filter reposts, again if the post is the root - return true - } - } - - return false -} diff --git a/src/state/queries/post.ts b/src/state/queries/post.ts index 794f48eb1b..8e77bf6b92 100644 --- a/src/state/queries/post.ts +++ b/src/state/queries/post.ts @@ -8,6 +8,7 @@ import {logEvent, LogEvents, toClout} from '#/lib/statsig/statsig' import {updatePostShadow} from '#/state/cache/post-shadow' import {Shadow} from '#/state/cache/types' import {useAgent, useSession} from '#/state/session' +import {useIsThreadMuted, useSetThreadMute} from '../cache/thread-mutes' import {findProfileQueryData} from './profile' const RQKEY_ROOT = 'post' @@ -291,3 +292,72 @@ export function usePostDeleteMutation() { }, }) } + +export function useThreadMuteMutationQueue( + post: Shadow, + rootUri: string, +) { + const threadMuteMutation = useThreadMuteMutation() + const threadUnmuteMutation = useThreadUnmuteMutation() + const isThreadMuted = useIsThreadMuted(rootUri, post.viewer?.threadMuted) + const setThreadMute = useSetThreadMute() + + const queueToggle = useToggleMutationQueue({ + initialState: isThreadMuted, + runMutation: async (_prev, shouldLike) => { + if (shouldLike) { + await threadMuteMutation.mutateAsync({ + uri: rootUri, + }) + return true + } else { + await threadUnmuteMutation.mutateAsync({ + uri: rootUri, + }) + return false + } + }, + onSuccess(finalIsMuted) { + // finalize + setThreadMute(rootUri, finalIsMuted) + }, + }) + + const queueMuteThread = useCallback(() => { + // optimistically update + setThreadMute(rootUri, true) + return queueToggle(true) + }, [setThreadMute, rootUri, queueToggle]) + + const queueUnmuteThread = useCallback(() => { + // optimistically update + setThreadMute(rootUri, false) + return queueToggle(false) + }, [rootUri, setThreadMute, queueToggle]) + + return [isThreadMuted, queueMuteThread, queueUnmuteThread] as const +} + +function useThreadMuteMutation() { + const agent = useAgent() + return useMutation< + {}, + Error, + {uri: string} // the root post's uri + >({ + mutationFn: ({uri}) => { + logEvent('post:mute', {}) + return agent.api.app.bsky.graph.muteThread({root: uri}) + }, + }) +} + +function useThreadUnmuteMutation() { + const agent = useAgent() + return useMutation<{}, Error, {uri: string}>({ + mutationFn: ({uri}) => { + logEvent('post:unmute', {}) + return agent.api.app.bsky.graph.unmuteThread({root: uri}) + }, + }) +} diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index 2486b73d58..45e00e58c7 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -7,7 +7,7 @@ import { } from 'react-native' import * as Clipboard from 'expo-clipboard' import { - AppBskyActorDefs, + AppBskyFeedDefs, AppBskyFeedPost, AtUri, RichText as RichTextAPI, @@ -22,12 +22,15 @@ import {richTextToString} from '#/lib/strings/rich-text-helpers' import {getTranslatorLink} from '#/locale/helpers' import {logger} from '#/logger' import {isWeb} from '#/platform/detection' +import {Shadow} from '#/state/cache/post-shadow' import {useFeedFeedbackContext} from '#/state/feed-feedback' -import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads' import {useLanguagePrefs} from '#/state/preferences' import {useHiddenPosts, useHiddenPostsApi} from '#/state/preferences' import {useOpenLink} from '#/state/preferences/in-app-browser' -import {usePostDeleteMutation} from '#/state/queries/post' +import { + usePostDeleteMutation, + useThreadMuteMutationQueue, +} from '#/state/queries/post' import {useSession} from '#/state/session' import {getCurrentRoute} from 'lib/routes/helpers' import {shareUrl} from 'lib/sharing' @@ -62,9 +65,7 @@ import * as Toast from '../Toast' let PostDropdownBtn = ({ testID, - postAuthor, - postCid, - postUri, + post, postFeedContext, record, richText, @@ -74,9 +75,7 @@ let PostDropdownBtn = ({ timestamp, }: { testID: string - postAuthor: AppBskyActorDefs.ProfileViewBasic - postCid: string - postUri: string + post: Shadow postFeedContext: string | undefined record: AppBskyFeedPost.Record richText: RichTextAPI @@ -92,8 +91,6 @@ let PostDropdownBtn = ({ const {_} = useLingui() const defaultCtrlColor = theme.palette.default.postCtrl const langPrefs = useLanguagePrefs() - const mutedThreads = useMutedThreads() - const toggleThreadMute = useToggleThreadMute() const postDeleteMutation = usePostDeleteMutation() const hiddenPosts = useHiddenPosts() const {hidePost} = useHiddenPostsApi() @@ -107,9 +104,15 @@ let PostDropdownBtn = ({ const loggedOutWarningPromptControl = useDialogControl() const embedPostControl = useDialogControl() const sendViaChatControl = useDialogControl() + const postUri = post.uri + const postCid = post.cid + const postAuthor = post.author const rootUri = record.reply?.root?.uri || postUri - const isThreadMuted = mutedThreads.includes(rootUri) + const [isThreadMuted, muteThread, unmuteThread] = useThreadMuteMutationQueue( + post, + rootUri, + ) const isPostHidden = hiddenPosts && hiddenPosts.includes(postUri) const isAuthor = postAuthor.did === currentAccount?.did @@ -162,18 +165,22 @@ let PostDropdownBtn = ({ const onToggleThreadMute = React.useCallback(() => { try { - const muted = toggleThreadMute(rootUri) - if (muted) { + if (isThreadMuted) { + unmuteThread() + Toast.show(_(msg`You will now receive notifications for this thread`)) + } else { + muteThread() Toast.show( _(msg`You will no longer receive notifications for this thread`), ) - } else { - Toast.show(_(msg`You will now receive notifications for this thread`)) } - } catch (e) { - logger.error('Failed to toggle thread mute', {message: e}) + } catch (e: any) { + if (e?.name !== 'AbortError') { + logger.error('Failed to toggle thread mute', {message: e}) + Toast.show(_(msg`Failed to toggle thread mute, please try again`)) + } } - }, [rootUri, toggleThreadMute, _]) + }, [isThreadMuted, unmuteThread, _, muteThread]) const onCopyPostText = React.useCallback(() => { const str = richTextToString(richText, true) diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index c389855e3d..c0e743db48 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -319,9 +319,7 @@ let PostCtrls = ({ Date: Tue, 18 Jun 2024 11:48:49 -0700 Subject: [PATCH 38/81] Fix: only apply self-thread load-more behavior on the outer edge of the reply tree (#4559) --- src/state/queries/post-thread.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/state/queries/post-thread.ts b/src/state/queries/post-thread.ts index a8b1160fb4..f7e5e2ecba 100644 --- a/src/state/queries/post-thread.ts +++ b/src/state/queries/post-thread.ts @@ -31,6 +31,7 @@ import { getEmbeddedPost, } from './util' +const REPLY_TREE_DEPTH = 10 const RQKEY_ROOT = 'post-thread' export const RQKEY = (uri: string) => [RQKEY_ROOT, uri] type ThreadViewNode = AppBskyFeedGetPostThread.OutputSchema['thread'] @@ -90,7 +91,10 @@ export function usePostThreadQuery(uri: string | undefined) { gcTime: 0, queryKey: RQKEY(uri || ''), async queryFn() { - const res = await agent.getPostThread({uri: uri!, depth: 10}) + const res = await agent.getPostThread({ + uri: uri!, + depth: REPLY_TREE_DEPTH, + }) if (res.success) { const thread = responseToThreadNodes(res.data.thread) annotateSelfThread(thread) @@ -287,7 +291,12 @@ function annotateSelfThread(thread: ThreadNode) { selfThreadNode.ctx.isSelfThread = true } const last = selfThreadNodes[selfThreadNodes.length - 1] - if (last && last.post.replyCount && !last.replies?.length) { + if ( + last && + last.ctx.depth === REPLY_TREE_DEPTH && // at the edge of the tree depth + last.post.replyCount && // has replies + !last.replies?.length // replies were not hydrated + ) { last.ctx.hasMoreSelfThread = true } } From 983d85384b9e736193e6c89107df5ced447a056a Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 18 Jun 2024 13:50:07 -0500 Subject: [PATCH 39/81] Force callers of `getTimeAgo` to pass in the value for "now" (#4560) * Remove icky hook for now * Force callers of getTimeAgo to pass in the 'now' value * Update usage in Newskie dialog --- src/components/NewskieDialog.tsx | 7 ++++--- src/lib/hooks/useTimeAgo.ts | 17 ++++------------- src/view/com/util/TimeElapsed.tsx | 6 ++++-- src/view/screens/Log.tsx | 4 +++- 4 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/components/NewskieDialog.tsx b/src/components/NewskieDialog.tsx index fcdae0daa5..789a42d5f9 100644 --- a/src/components/NewskieDialog.tsx +++ b/src/components/NewskieDialog.tsx @@ -30,12 +30,13 @@ export function NewskieDialog({ const moderation = moderateProfile(profile, moderationOpts) return sanitizeDisplayName(name, moderation.ui('displayName')) }, [moderationOpts, profile]) + const [now] = React.useState(Date.now()) const timeAgo = useGetTimeAgo() const createdAt = profile.createdAt as string | undefined const daysOld = React.useMemo(() => { if (!createdAt) return Infinity - return differenceInSeconds(new Date(), new Date(createdAt)) / 86400 - }, [createdAt]) + return differenceInSeconds(now, new Date(createdAt)) / 86400 + }, [createdAt, now]) if (!createdAt || daysOld > 7) return null @@ -70,7 +71,7 @@ export function NewskieDialog({ {profileName} joined Bluesky{' '} - {timeAgo(createdAt, {format: 'long'})} ago + {timeAgo(createdAt, now, {format: 'long'})} ago diff --git a/src/lib/hooks/useTimeAgo.ts b/src/lib/hooks/useTimeAgo.ts index 5f0782f96f..efcb4754bb 100644 --- a/src/lib/hooks/useTimeAgo.ts +++ b/src/lib/hooks/useTimeAgo.ts @@ -1,4 +1,4 @@ -import {useCallback, useMemo} from 'react' +import {useCallback} from 'react' import {msg, plural} from '@lingui/macro' import {I18nContext, useLingui} from '@lingui/react' import {differenceInSeconds} from 'date-fns' @@ -12,25 +12,16 @@ export function useGetTimeAgo() { const {_} = useLingui() return useCallback( ( - date: number | string | Date, + earlier: number | string | Date, + later: number | string | Date, options?: Omit, ) => { - return dateDiff(date, Date.now(), {lingui: _, format: options?.format}) + return dateDiff(earlier, later, {lingui: _, format: options?.format}) }, [_], ) } -export function useTimeAgo( - date: number | string | Date, - options?: Omit, -): string { - const timeAgo = useGetTimeAgo() - return useMemo(() => { - return timeAgo(date, {...options}) - }, [date, options, timeAgo]) -} - const NOW = 5 const MINUTE = 60 const HOUR = MINUTE * 60 diff --git a/src/view/com/util/TimeElapsed.tsx b/src/view/com/util/TimeElapsed.tsx index d939b3163d..a495851826 100644 --- a/src/view/com/util/TimeElapsed.tsx +++ b/src/view/com/util/TimeElapsed.tsx @@ -15,12 +15,14 @@ export function TimeElapsed({ const ago = useGetTimeAgo() const format = timeToString ?? ago const tick = useTickEveryMinute() - const [timeElapsed, setTimeAgo] = React.useState(() => format(timestamp)) + const [timeElapsed, setTimeAgo] = React.useState(() => + format(timestamp, tick), + ) const [prevTick, setPrevTick] = React.useState(tick) if (prevTick !== tick) { setPrevTick(tick) - setTimeAgo(format(timestamp)) + setTimeAgo(format(timestamp, tick)) } return children({timeElapsed}) diff --git a/src/view/screens/Log.tsx b/src/view/screens/Log.tsx index e10aa83ab7..e6040b77e2 100644 --- a/src/view/screens/Log.tsx +++ b/src/view/screens/Log.tsx @@ -7,6 +7,7 @@ import {useFocusEffect} from '@react-navigation/native' import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo' import {getEntries} from '#/logger/logDump' +import {useTickEveryMinute} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell' import {usePalette} from 'lib/hooks/usePalette' import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types' @@ -24,6 +25,7 @@ export function LogScreen({}: NativeStackScreenProps< const setMinimalShellMode = useSetMinimalShellMode() const [expanded, setExpanded] = React.useState([]) const timeAgo = useGetTimeAgo() + const tick = useTickEveryMinute() useFocusEffect( React.useCallback(() => { @@ -72,7 +74,7 @@ export function LogScreen({}: NativeStackScreenProps< /> ) : undefined} - {timeAgo(entry.timestamp)} + {timeAgo(entry.timestamp, tick)} {expanded.includes(entry.id) ? ( From 4165a02b2d712ba20b9fdbf435d4cb00c03e5e52 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 18 Jun 2024 13:52:44 -0500 Subject: [PATCH 40/81] Prevent unecessary calls (#4561) (cherry picked from commit ecb48797675c5be24508bf47141e930c64dac14e) --- src/components/NewskieDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/NewskieDialog.tsx b/src/components/NewskieDialog.tsx index 789a42d5f9..281430e31f 100644 --- a/src/components/NewskieDialog.tsx +++ b/src/components/NewskieDialog.tsx @@ -30,7 +30,7 @@ export function NewskieDialog({ const moderation = moderateProfile(profile, moderationOpts) return sanitizeDisplayName(name, moderation.ui('displayName')) }, [moderationOpts, profile]) - const [now] = React.useState(Date.now()) + const [now] = React.useState(() => Date.now()) const timeAgo = useGetTimeAgo() const createdAt = profile.createdAt as string | undefined const daysOld = React.useMemo(() => { From d6ce16d15ae79c4fef943cd48dfa0cdb072e9596 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 18 Jun 2024 12:07:56 -0700 Subject: [PATCH 41/81] Implement thread locking (#4545) * Add the ability to edit threadgates * Fix bottom border on mobile * Refresh thread after threadgate edit --- src/lib/analytics/types.ts | 2 + src/lib/api/index.ts | 17 +- src/state/modals/index.tsx | 3 +- src/state/queries/post-thread.ts | 2 +- src/state/queries/threadgate.ts | 33 +++ src/view/com/modals/Threadgate.tsx | 11 +- src/view/com/post-thread/PostThreadItem.tsx | 25 +- src/view/com/threadgate/WhoCanReply.tsx | 240 ++++++++++++-------- 8 files changed, 222 insertions(+), 111 deletions(-) diff --git a/src/lib/analytics/types.ts b/src/lib/analytics/types.ts index cdf535dec2..720495ea1d 100644 --- a/src/lib/analytics/types.ts +++ b/src/lib/analytics/types.ts @@ -32,6 +32,8 @@ export type TrackPropertiesMap = { 'Post:ThreadMute': {} // CAN BE SERVER 'Post:ThreadUnmute': {} // CAN BE SERVER 'Post:Reply': {} // CAN BE SERVER + 'Post:EditThreadgateOpened': {} + 'Post:ThreadgateEdited': {} // PROFILE events 'Profile:Follow': { username: string diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index dfaae2e018..5b1c998cb8 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -270,7 +270,7 @@ export async function post(agent: BskyAgent, opts: PostOpts) { return res } -async function createThreadgate( +export async function createThreadgate( agent: BskyAgent, postUri: string, threadgate: ThreadgateSetting[], @@ -296,10 +296,17 @@ async function createThreadgate( } const postUrip = new AtUri(postUri) - await agent.api.app.bsky.feed.threadgate.create( - {repo: agent.session!.did, rkey: postUrip.rkey}, - {post: postUri, createdAt: new Date().toISOString(), allow}, - ) + await agent.api.com.atproto.repo.putRecord({ + repo: agent.session!.did, + collection: 'app.bsky.feed.threadgate', + rkey: postUrip.rkey, + record: { + $type: 'app.bsky.feed.threadgate', + post: postUri, + allow, + createdAt: new Date().toISOString(), + }, + }) } // helpers diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index ced14335bd..685b10bd85 100644 --- a/src/state/modals/index.tsx +++ b/src/state/modals/index.tsx @@ -70,7 +70,8 @@ export interface SelfLabelModal { export interface ThreadgateModal { name: 'threadgate' settings: ThreadgateSetting[] - onChange: (settings: ThreadgateSetting[]) => void + onChange?: (settings: ThreadgateSetting[]) => void + onConfirm?: (settings: ThreadgateSetting[]) => void } export interface ChangeHandleModal { diff --git a/src/state/queries/post-thread.ts b/src/state/queries/post-thread.ts index f7e5e2ecba..db85e8a177 100644 --- a/src/state/queries/post-thread.ts +++ b/src/state/queries/post-thread.ts @@ -32,7 +32,7 @@ import { } from './util' const REPLY_TREE_DEPTH = 10 -const RQKEY_ROOT = 'post-thread' +export const RQKEY_ROOT = 'post-thread' export const RQKEY = (uri: string) => [RQKEY_ROOT, uri] type ThreadViewNode = AppBskyFeedGetPostThread.OutputSchema['thread'] diff --git a/src/state/queries/threadgate.ts b/src/state/queries/threadgate.ts index 4891175825..67c6f8c084 100644 --- a/src/state/queries/threadgate.ts +++ b/src/state/queries/threadgate.ts @@ -1,5 +1,38 @@ +import {AppBskyFeedDefs, AppBskyFeedThreadgate} from '@atproto/api' + export type ThreadgateSetting = | {type: 'nobody'} | {type: 'mention'} | {type: 'following'} | {type: 'list'; list: string} + +export function threadgateViewToSettings( + threadgate: AppBskyFeedDefs.ThreadgateView | undefined, +): ThreadgateSetting[] { + const record = + threadgate && + AppBskyFeedThreadgate.isRecord(threadgate.record) && + AppBskyFeedThreadgate.validateRecord(threadgate.record).success + ? threadgate.record + : null + if (!record) { + return [] + } + if (!record.allow?.length) { + return [{type: 'nobody'}] + } + return record.allow + .map(allow => { + if (allow.$type === 'app.bsky.feed.threadgate#mentionRule') { + return {type: 'mention'} + } + if (allow.$type === 'app.bsky.feed.threadgate#followingRule') { + return {type: 'following'} + } + if (allow.$type === 'app.bsky.feed.threadgate#listRule') { + return {type: 'list', list: allow.list} + } + return undefined + }) + .filter(Boolean) as ThreadgateSetting[] +} diff --git a/src/view/com/modals/Threadgate.tsx b/src/view/com/modals/Threadgate.tsx index a2e9f391c0..4a9a9e2ab5 100644 --- a/src/view/com/modals/Threadgate.tsx +++ b/src/view/com/modals/Threadgate.tsx @@ -26,9 +26,11 @@ export const snapPoints = ['60%'] export function Component({ settings, onChange, + onConfirm, }: { settings: ThreadgateSetting[] - onChange: (settings: ThreadgateSetting[]) => void + onChange?: (settings: ThreadgateSetting[]) => void + onConfirm?: (settings: ThreadgateSetting[]) => void }) { const pal = usePalette('default') const {closeModal} = useModalControls() @@ -38,12 +40,12 @@ export function Component({ const onPressEverybody = () => { setSelected([]) - onChange([]) + onChange?.([]) } const onPressNobody = () => { setSelected([{type: 'nobody'}]) - onChange([{type: 'nobody'}]) + onChange?.([{type: 'nobody'}]) } const onPressAudience = (setting: ThreadgateSetting) => { @@ -57,7 +59,7 @@ export function Component({ newSelected.splice(i, 1) } setSelected(newSelected) - onChange(newSelected) + onChange?.(newSelected) } return ( @@ -124,6 +126,7 @@ export function Component({ testID="confirmBtn" onPress={() => { closeModal() + onConfirm?.(selected) }} style={styles.btn} accessibilityRole="button" diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 5ee60e4eab..6d03029d7f 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -25,7 +25,7 @@ import {sanitizeHandle} from 'lib/strings/handles' import {countLines} from 'lib/strings/helpers' import {niceDate} from 'lib/strings/time' import {s} from 'lib/styles' -import {isWeb} from 'platform/detection' +import {isNative, isWeb} from 'platform/detection' import {useSession} from 'state/session' import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn' import {atoms as a} from '#/alf' @@ -189,6 +189,7 @@ let PostThreadItemLoaded = ({ const itemTitle = _(msg`Post by ${post.author.handle}`) const authorHref = makeProfileLink(post.author) const authorTitle = post.author.handle + const isThreadAuthor = getThreadAuthor(post, record) === currentAccount?.did const likesHref = React.useMemo(() => { const urip = new AtUri(post.uri) return makeProfileLink(post.author, 'post', urip.rkey, 'liked-by') @@ -395,7 +396,11 @@ let PostThreadItemLoaded = ({ - + ) } else { @@ -578,7 +583,9 @@ let PostThreadItemLoaded = ({ post={post} style={{ marginTop: 4, + borderBottomWidth: 1, }} + isThreadAuthor={isThreadAuthor} /> ) @@ -681,6 +688,20 @@ function ExpandedPostDetails({ ) } +function getThreadAuthor( + post: AppBskyFeedDefs.PostView, + record: AppBskyFeedPost.Record, +): string { + if (!record.reply) { + return post.author.did + } + try { + return new AtUri(record.reply.root.uri).host + } catch { + return '' + } +} + const styles = StyleSheet.create({ outer: { borderTopWidth: hairlineWidth, diff --git a/src/view/com/threadgate/WhoCanReply.tsx b/src/view/com/threadgate/WhoCanReply.tsx index c1e36d481d..3ffbaa7ae9 100644 --- a/src/view/com/threadgate/WhoCanReply.tsx +++ b/src/view/com/threadgate/WhoCanReply.tsx @@ -1,128 +1,172 @@ import React from 'react' -import {StyleProp, View, ViewStyle} from 'react-native' -import { - AppBskyFeedDefs, - AppBskyFeedThreadgate, - AppBskyGraphDefs, - AtUri, -} from '@atproto/api' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {Trans} from '@lingui/macro' +import {Keyboard, StyleProp, View, ViewStyle} from 'react-native' +import {AppBskyFeedDefs, AppBskyGraphDefs, AtUri} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useQueryClient} from '@tanstack/react-query' +import {useAnalytics} from '#/lib/analytics/analytics' +import {createThreadgate} from '#/lib/api' import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle' import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {makeListLink, makeProfileLink} from '#/lib/routes/links' import {colors} from '#/lib/styles' +import {logger} from '#/logger' +import {isNative} from '#/platform/detection' +import {useModalControls} from '#/state/modals' +import {RQKEY_ROOT as POST_THREAD_RQKEY_ROOT} from '#/state/queries/post-thread' +import { + ThreadgateSetting, + threadgateViewToSettings, +} from '#/state/queries/threadgate' +import {useAgent} from '#/state/session' +import * as Toast from 'view/com/util/Toast' +import {Button} from '#/components/Button' import {TextLink} from '../util/Link' import {Text} from '../util/text/Text' export function WhoCanReply({ post, + isThreadAuthor, style, }: { post: AppBskyFeedDefs.PostView + isThreadAuthor: boolean style?: StyleProp }) { + const {track} = useAnalytics() + const {_} = useLingui() const pal = usePalette('default') - const {isMobile} = useWebMediaQueries() + const agent = useAgent() + const queryClient = useQueryClient() + const {openModal} = useModalControls() const containerStyles = useColorSchemeStyle( { - borderColor: pal.colors.unreadNotifBorder, backgroundColor: pal.colors.unreadNotifBg, }, { - borderColor: pal.colors.unreadNotifBorder, backgroundColor: pal.colors.unreadNotifBg, }, ) - const iconStyles = useColorSchemeStyle( - { - backgroundColor: colors.blue3, - }, - { - backgroundColor: colors.blue3, - }, - ) const textStyles = useColorSchemeStyle( - {color: colors.gray7}, + {color: colors.blue5}, {color: colors.blue1}, ) - const record = React.useMemo( - () => - post.threadgate && - AppBskyFeedThreadgate.isRecord(post.threadgate.record) && - AppBskyFeedThreadgate.validateRecord(post.threadgate.record).success - ? post.threadgate.record - : null, + const hoverStyles = useColorSchemeStyle( + { + backgroundColor: colors.white, + }, + { + backgroundColor: pal.colors.background, + }, + ) + const settings = React.useMemo( + () => threadgateViewToSettings(post.threadgate), [post], ) - if (record) { - return ( - - - - - - - {!record.allow?.length ? ( - Replies to this thread are disabled - ) : ( - - Only{' '} - {record.allow.map((rule, i) => ( - <> - - - - ))}{' '} - can reply. - - )} - - - - ) + const isRootPost = !('reply' in post.record) + + const onPressEdit = () => { + track('Post:EditThreadgateOpened') + if (isNative && Keyboard.isVisible()) { + Keyboard.dismiss() + } + openModal({ + name: 'threadgate', + settings, + async onConfirm(newSettings: ThreadgateSetting[]) { + try { + if (newSettings.length) { + await createThreadgate(agent, post.uri, newSettings) + } else { + await agent.api.com.atproto.repo.deleteRecord({ + repo: agent.session!.did, + collection: 'app.bsky.feed.threadgate', + rkey: new AtUri(post.uri).rkey, + }) + } + Toast.show('Thread settings updated') + queryClient.invalidateQueries({ + queryKey: [POST_THREAD_RQKEY_ROOT], + }) + track('Post:ThreadgateEdited') + } catch (err) { + Toast.show( + 'There was an issue. Please check your internet connection and try again.', + ) + logger.error('Failed to edit threadgate', {message: err}) + } + }, + }) } - return null + + if (!isRootPost) { + return null + } + if (!settings.length && !isThreadAuthor) { + return null + } + + return ( + + + + {!settings.length ? ( + Everybody can reply. + ) : settings[0].type === 'nobody' ? ( + Replies to this thread are disabled. + ) : ( + + Only{' '} + {settings.map((rule, i) => ( + <> + + + + ))}{' '} + can reply. + + )} + + + {isThreadAuthor && ( + + + + )} + + ) } function Rule({ @@ -130,15 +174,15 @@ function Rule({ post, lists, }: { - rule: any + rule: ThreadgateSetting post: AppBskyFeedDefs.PostView lists: AppBskyGraphDefs.ListViewBasic[] | undefined }) { const pal = usePalette('default') - if (AppBskyFeedThreadgate.isMentionRule(rule)) { + if (rule.type === 'mention') { return mentioned users } - if (AppBskyFeedThreadgate.isFollowingRule(rule)) { + if (rule.type === 'following') { return ( users followed by{' '} @@ -151,7 +195,7 @@ function Rule({ ) } - if (AppBskyFeedThreadgate.isListRule(rule)) { + if (rule.type === 'list') { const list = lists?.find(l => l.uri === rule.list) if (list) { const listUrip = new AtUri(list.uri) From 502bcad7017d72fb23c40a268b1e220f892db7da Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 18 Jun 2024 14:09:40 -0500 Subject: [PATCH 42/81] Disable newskie dialog tap in hover card web (#4562) --- src/components/NewskieDialog.tsx | 3 +++ src/components/ProfileHoverCard/index.web.tsx | 2 +- src/screens/Profile/Header/Handle.tsx | 6 ++++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/NewskieDialog.tsx b/src/components/NewskieDialog.tsx index 281430e31f..0354bfc432 100644 --- a/src/components/NewskieDialog.tsx +++ b/src/components/NewskieDialog.tsx @@ -18,8 +18,10 @@ import {Text} from '#/components/Typography' export function NewskieDialog({ profile, + disabled, }: { profile: AppBskyActorDefs.ProfileViewDetailed + disabled?: boolean }) { const {_} = useLingui() const moderationOpts = useModerationOpts() @@ -43,6 +45,7 @@ export function NewskieDialog({ return ( diff --git a/src/view/com/util/post-ctrls/RepostButton.web.tsx b/src/view/com/util/post-ctrls/RepostButton.web.tsx index 0898981419..17ab736ced 100644 --- a/src/view/com/util/post-ctrls/RepostButton.web.tsx +++ b/src/view/com/util/post-ctrls/RepostButton.web.tsx @@ -12,6 +12,7 @@ import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repos import * as Menu from '#/components/Menu' import {Text} from '#/components/Typography' import {EventStopper} from '../EventStopper' +import {formatCount} from '../numeric/format' interface Props { isReposted: boolean @@ -115,20 +116,22 @@ const RepostInner = ({ color: {color: string} repostCount?: number big?: boolean -}) => ( - - - {typeof repostCount !== 'undefined' && repostCount > 0 ? ( - - {repostCount} - - ) : undefined} - -) +}) => { + return ( + + + {typeof repostCount !== 'undefined' && repostCount > 0 ? ( + + {formatCount(repostCount)} + + ) : undefined} + + ) +} diff --git a/yarn.lock b/yarn.lock index 700ddfe072..a0fd8749a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3897,18 +3897,18 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2" integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q== -"@formatjs/ecma402-abstract@1.18.0": - version "1.18.0" - resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.0.tgz#e2120e7101020140661b58430a7ff4262705a2f2" - integrity sha512-PEVLoa3zBevWSCZzPIM/lvPCi8P5l4G+NXQMc/CjEiaCWgyHieUoo0nM7Bs0n/NbuQ6JpXEolivQ9pKSBHaDlA== +"@formatjs/ecma402-abstract@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.0.0.tgz#39197ab90b1c78b7342b129a56a7acdb8f512e17" + integrity sha512-rRqXOqdFmk7RYvj4khklyqzcfQl9vEL/usogncBHRZfZBDOwMGuSRNFl02fu5KGHXdbinju+YXyuR+Nk8xlr/g== dependencies: - "@formatjs/intl-localematcher" "0.5.2" + "@formatjs/intl-localematcher" "0.5.4" tslib "^2.4.0" -"@formatjs/intl-enumerator@1.4.3": - version "1.4.3" - resolved "https://registry.yarnpkg.com/@formatjs/intl-enumerator/-/intl-enumerator-1.4.3.tgz#8d278c273485d7c6219916509fbd51ce3142064d" - integrity sha512-0NpTmAQnDokPoB5aVtXvOdtrUq/uEuPPhBUAr57TYYDjI5MwfFXt8F6JCm6s6CPI0inL8+nxPLjjqH0qyNnP4Q== +"@formatjs/intl-enumerator@1.4.7": + version "1.4.7" + resolved "https://registry.yarnpkg.com/@formatjs/intl-enumerator/-/intl-enumerator-1.4.7.tgz#6ab697f3f8f18cf0cc6a6b028cb9c40db6001f3d" + integrity sha512-03RHnFqfpB4H/jwCwlzC+wkTDk2Fi24JmVIY2PVGvTUpikN2bSr9+8oTXfOC+y7B7VxjCArUnqWXVoctkmy85w== dependencies: tslib "^2.4.0" @@ -3919,30 +3919,39 @@ dependencies: tslib "^2.4.0" -"@formatjs/intl-locale@^3.4.3": - version "3.4.3" - resolved "https://registry.yarnpkg.com/@formatjs/intl-locale/-/intl-locale-3.4.3.tgz#fdd2a3978b03aa76965abbca86526bb1d02973b6" - integrity sha512-g/35yMikkkRmLYmqE4W74gvZyKa768oC9OmUFzfLmH3CVYF3v2kvAZI0WsxWLbxYj8TT7wBDeLIL3aIlRw4Osw== +"@formatjs/intl-locale@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@formatjs/intl-locale/-/intl-locale-4.0.0.tgz#c111a33078413eba2011e82140466261eb1d67cd" + integrity sha512-+4dbMEGsp1bvB3JB3UHH6YTjMnFTifnfdaHp4ROrCCu50NedA69RBsDCG3eivcZkbj57X9ehGhMWjLxlP+gyVw== dependencies: - "@formatjs/ecma402-abstract" "1.18.0" - "@formatjs/intl-enumerator" "1.4.3" + "@formatjs/ecma402-abstract" "2.0.0" + "@formatjs/intl-enumerator" "1.4.7" "@formatjs/intl-getcanonicallocales" "2.3.0" tslib "^2.4.0" -"@formatjs/intl-localematcher@0.5.2": - version "0.5.2" - resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.2.tgz#5fcf029fd218905575e5080fa33facdcb623d532" - integrity sha512-txaaE2fiBMagLrR4jYhxzFO6wEdEG4TPMqrzBAcbr4HFUYzH/YC+lg6OIzKCHm8WgDdyQevxbAAV1OgcXctuGw== +"@formatjs/intl-localematcher@0.5.4": + version "0.5.4" + resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz#caa71f2e40d93e37d58be35cfffe57865f2b366f" + integrity sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g== dependencies: tslib "^2.4.0" -"@formatjs/intl-pluralrules@^5.2.10": - version "5.2.10" - resolved "https://registry.yarnpkg.com/@formatjs/intl-pluralrules/-/intl-pluralrules-5.2.10.tgz#379fc06133625df0cae715c1d902001974ff3279" - integrity sha512-wfJypePrbOByaZVPP1moLXHgS9LeAvi9coP95XZX7ySVrwdDGPnxz9Pw+o7J1o8AjLxjiqGrvAi74key5zzIjQ== +"@formatjs/intl-numberformat@^8.10.3": + version "8.10.3" + resolved "https://registry.yarnpkg.com/@formatjs/intl-numberformat/-/intl-numberformat-8.10.3.tgz#abc97cc6a7b7f1b20da9f07a976b5589c1192ab8" + integrity sha512-lH3liLMeIjZ19Zxt8RRPnBcpPweS1YNSXRURDiFfvFmRlDZUOd8+GlcVyECcPZPkIoSH/p4lfGrnaUzepxJ92g== dependencies: - "@formatjs/ecma402-abstract" "1.18.0" - "@formatjs/intl-localematcher" "0.5.2" + "@formatjs/ecma402-abstract" "2.0.0" + "@formatjs/intl-localematcher" "0.5.4" + tslib "^2.4.0" + +"@formatjs/intl-pluralrules@^5.2.14": + version "5.2.14" + resolved "https://registry.yarnpkg.com/@formatjs/intl-pluralrules/-/intl-pluralrules-5.2.14.tgz#7477bd2aa9bfde9e543d839707eff5460eb08026" + integrity sha512-l6Ev7aOGXJSh5EPDEqzsbyufdCCKXZk993QXRQebLsB0TXRhIyF4alqjdMEatLwIigK/Mka8kiVIOLeFP5Cj9Q== + dependencies: + "@formatjs/ecma402-abstract" "2.0.0" + "@formatjs/intl-localematcher" "0.5.4" tslib "^2.4.0" "@fortawesome/fontawesome-common-types@6.4.2": From 0f931933a7b7b72505fbab6e1f25860f7a84e59c Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 19 Jun 2024 22:32:44 +0100 Subject: [PATCH 56/81] Option for large alt badges (#4571) * add pref for large alt badge * add to settings * do the large badge bit * Tweak wording --------- Co-authored-by: Dan Abramov --- src/state/persisted/schema.ts | 2 + src/state/preferences/alt-text-required.tsx | 1 + src/state/preferences/hidden-posts.tsx | 1 + src/state/preferences/index.tsx | 25 ++++++----- src/state/preferences/large-alt-badge.tsx | 49 +++++++++++++++++++++ src/view/com/util/images/Gallery.tsx | 22 +++++---- src/view/com/util/post-embeds/GifEmbed.tsx | 6 ++- src/view/com/util/post-embeds/index.tsx | 15 +++---- src/view/screens/AccessibilitySettings.tsx | 35 ++++++++++----- 9 files changed, 113 insertions(+), 43 deletions(-) create mode 100644 src/state/preferences/large-alt-badge.tsx diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts index 9d5b17d354..c942828f2a 100644 --- a/src/state/persisted/schema.ts +++ b/src/state/persisted/schema.ts @@ -60,6 +60,7 @@ export const schema = z.object({ appLanguage: z.string(), }), requireAltTextEnabled: z.boolean(), // should move to server + largeAltBadgeEnabled: z.boolean().optional(), externalEmbeds: z .object({ giphy: z.enum(externalEmbedOptions).optional(), @@ -112,6 +113,7 @@ export const defaults: Schema = { appLanguage: deviceLocales[0] || 'en', }, requireAltTextEnabled: false, + largeAltBadgeEnabled: false, externalEmbeds: {}, mutedThreads: [], invites: { diff --git a/src/state/preferences/alt-text-required.tsx b/src/state/preferences/alt-text-required.tsx index 81de9e0060..642e790fbc 100644 --- a/src/state/preferences/alt-text-required.tsx +++ b/src/state/preferences/alt-text-required.tsx @@ -1,4 +1,5 @@ import React from 'react' + import * as persisted from '#/state/persisted' type StateContext = persisted.Schema['requireAltTextEnabled'] diff --git a/src/state/preferences/hidden-posts.tsx b/src/state/preferences/hidden-posts.tsx index 11119ce758..2c6a373e15 100644 --- a/src/state/preferences/hidden-posts.tsx +++ b/src/state/preferences/hidden-posts.tsx @@ -1,4 +1,5 @@ import React from 'react' + import * as persisted from '#/state/persisted' type SetStateCb = ( diff --git a/src/state/preferences/index.tsx b/src/state/preferences/index.tsx index 70c8efc805..e1a35f193c 100644 --- a/src/state/preferences/index.tsx +++ b/src/state/preferences/index.tsx @@ -8,6 +8,7 @@ import {Provider as HiddenPostsProvider} from './hidden-posts' import {Provider as InAppBrowserProvider} from './in-app-browser' import {Provider as KawaiiProvider} from './kawaii' import {Provider as LanguagesProvider} from './languages' +import {Provider as LargeAltBadgeProvider} from './large-alt-badge' export { useRequireAltTextEnabled, @@ -27,17 +28,19 @@ export function Provider({children}: React.PropsWithChildren<{}>) { return ( - - - - - - {children} - - - - - + + + + + + + {children} + + + + + + ) diff --git a/src/state/preferences/large-alt-badge.tsx b/src/state/preferences/large-alt-badge.tsx new file mode 100644 index 0000000000..b3d597c5cb --- /dev/null +++ b/src/state/preferences/large-alt-badge.tsx @@ -0,0 +1,49 @@ +import React from 'react' + +import * as persisted from '#/state/persisted' + +type StateContext = persisted.Schema['largeAltBadgeEnabled'] +type SetContext = (v: persisted.Schema['largeAltBadgeEnabled']) => void + +const stateContext = React.createContext( + persisted.defaults.largeAltBadgeEnabled, +) +const setContext = React.createContext( + (_: persisted.Schema['largeAltBadgeEnabled']) => {}, +) + +export function Provider({children}: React.PropsWithChildren<{}>) { + const [state, setState] = React.useState( + persisted.get('largeAltBadgeEnabled'), + ) + + const setStateWrapped = React.useCallback( + (largeAltBadgeEnabled: persisted.Schema['largeAltBadgeEnabled']) => { + setState(largeAltBadgeEnabled) + persisted.write('largeAltBadgeEnabled', largeAltBadgeEnabled) + }, + [setState], + ) + + React.useEffect(() => { + return persisted.onUpdate(() => { + setState(persisted.get('largeAltBadgeEnabled')) + }) + }, [setStateWrapped]) + + return ( + + + {children} + + + ) +} + +export function useLargeAltBadgeEnabled() { + return React.useContext(stateContext) +} + +export function useSetLargeAltBadgeEnabled() { + return React.useContext(setContext) +} diff --git a/src/view/com/util/images/Gallery.tsx b/src/view/com/util/images/Gallery.tsx index 8d23d258f5..9bbb2ac100 100644 --- a/src/view/com/util/images/Gallery.tsx +++ b/src/view/com/util/images/Gallery.tsx @@ -5,7 +5,9 @@ import {AppBskyEmbedImages} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {isWeb} from 'platform/detection' +import {isWeb} from '#/platform/detection' +import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' +import {atoms as a} from '#/alf' type EventFunction = (index: number) => void @@ -27,20 +29,21 @@ export const GalleryItem: FC = ({ onLongPress, }) => { const {_} = useLingui() + const largeAltBadge = useLargeAltBadgeEnabled() const image = images[index] return ( - + onPress(index) : undefined} onPressIn={onPressIn ? () => onPressIn(index) : undefined} onLongPress={onLongPress ? () => onLongPress(index) : undefined} - style={styles.fullWidth} + style={a.flex_1} accessibilityRole="button" accessibilityLabel={image.alt || _(msg`Image`)} accessibilityHint=""> = ({ {image.alt === '' ? null : ( - + ALT @@ -59,13 +64,6 @@ export const GalleryItem: FC = ({ } const styles = StyleSheet.create({ - fullWidth: { - flex: 1, - }, - image: { - flex: 1, - borderRadius: 4, - }, altContainer: { backgroundColor: 'rgba(0, 0, 0, 0.75)', borderRadius: 6, diff --git a/src/view/com/util/post-embeds/GifEmbed.tsx b/src/view/com/util/post-embeds/GifEmbed.tsx index f2e2a8b0e9..1558b75c62 100644 --- a/src/view/com/util/post-embeds/GifEmbed.tsx +++ b/src/view/com/util/post-embeds/GifEmbed.tsx @@ -8,6 +8,7 @@ import {useLingui} from '@lingui/react' import {HITSLOP_20} from '#/lib/constants' import {parseAltFromGIFDescription} from '#/lib/gif-alt-text' import {isWeb} from '#/platform/detection' +import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' import {EmbedPlayerParams} from 'lib/strings/embed-player' import {useAutoplayDisabled} from 'state/preferences' import {atoms as a, useTheme} from '#/alf' @@ -157,6 +158,7 @@ export function GifEmbed({ function AltText({text}: {text: string}) { const control = Prompt.usePromptControl() + const largeAltBadge = useLargeAltBadgeEnabled() const {_} = useLingui() return ( @@ -169,7 +171,9 @@ function AltText({text}: {text: string}) { hitSlop={HITSLOP_20} onPress={control.open} style={styles.altContainer}> - + ALT diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index a13fffc370..be34a2869e 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -21,6 +21,7 @@ import { import {ImagesLightbox, useLightboxControls} from '#/state/lightbox' import {usePalette} from 'lib/hooks/usePalette' import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' +import {atoms as a} from '#/alf' import {ContentHider} from '../../../../components/moderation/ContentHider' import {AutoSizedImage} from '../images/AutoSizedImage' import {ImageLayoutGrid} from '../images/ImageLayoutGrid' @@ -28,6 +29,7 @@ import {ExternalLinkEmbed} from './ExternalLinkEmbed' import {ListEmbed} from './ListEmbed' import {MaybeQuoteEmbed} from './QuoteEmbed' import hairlineWidth = StyleSheet.hairlineWidth +import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' type Embed = | AppBskyEmbedRecord.View @@ -51,6 +53,7 @@ export function PostEmbeds({ }) { const pal = usePalette('default') const {openLightbox} = useLightboxControls() + const largeAltBadge = useLargeAltBadgeEnabled() // quote post with media // = @@ -130,10 +133,12 @@ export function PostEmbeds({ dimensionsHint={aspectRatio} onPress={() => _openLightbox(0)} onPressIn={() => onPressIn(0)} - style={[styles.singleImage]}> + style={a.rounded_sm}> {alt === '' ? null : ( - + ALT @@ -151,9 +156,6 @@ export function PostEmbeds({ images={embed.images} onPress={_openLightbox} onPressIn={onPressIn} - style={ - embed.images.length === 1 ? [styles.singleImage] : undefined - } /> @@ -179,9 +181,6 @@ const styles = StyleSheet.create({ imagesContainer: { marginTop: 8, }, - singleImage: { - borderRadius: 8, - }, altContainer: { backgroundColor: 'rgba(0, 0, 0, 0.75)', borderRadius: 6, diff --git a/src/view/screens/AccessibilitySettings.tsx b/src/view/screens/AccessibilitySettings.tsx index ac0d985f10..9ac9793367 100644 --- a/src/view/screens/AccessibilitySettings.tsx +++ b/src/view/screens/AccessibilitySettings.tsx @@ -4,13 +4,12 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useFocusEffect} from '@react-navigation/native' +import {useAnalytics} from '#/lib/analytics/analytics' +import {usePalette} from '#/lib/hooks/usePalette' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' +import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' +import {s} from '#/lib/styles' import {isNative} from '#/platform/detection' -import {useSetMinimalShellMode} from '#/state/shell' -import {useAnalytics} from 'lib/analytics/analytics' -import {usePalette} from 'lib/hooks/usePalette' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types' -import {s} from 'lib/styles' import { useAutoplayDisabled, useHapticsDisabled, @@ -18,11 +17,16 @@ import { useSetAutoplayDisabled, useSetHapticsDisabled, useSetRequireAltTextEnabled, -} from 'state/preferences' -import {ToggleButton} from 'view/com/util/forms/ToggleButton' -import {SimpleViewHeader} from '../com/util/SimpleViewHeader' -import {Text} from '../com/util/text/Text' -import {ScrollView} from '../com/util/Views' +} from '#/state/preferences' +import { + useLargeAltBadgeEnabled, + useSetLargeAltBadgeEnabled, +} from '#/state/preferences/large-alt-badge' +import {useSetMinimalShellMode} from '#/state/shell' +import {ToggleButton} from '#/view/com/util/forms/ToggleButton' +import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' +import {Text} from '#/view/com/util/text/Text' +import {ScrollView} from '#/view/com/util/Views' type Props = NativeStackScreenProps< CommonNavigatorParams, @@ -41,6 +45,8 @@ export function AccessibilitySettingsScreen({}: Props) { const setAutoplayDisabled = useSetAutoplayDisabled() const hapticsDisabled = useHapticsDisabled() const setHapticsDisabled = useSetHapticsDisabled() + const largeAltBadgeEnabled = useLargeAltBadgeEnabled() + const setLargeAltBadgeEnabled = useSetLargeAltBadgeEnabled() useFocusEffect( React.useCallback(() => { @@ -84,6 +90,13 @@ export function AccessibilitySettingsScreen({}: Props) { isSelected={requireAltTextEnabled} onPress={() => setRequireAltTextEnabled(!requireAltTextEnabled)} /> + setLargeAltBadgeEnabled(!largeAltBadgeEnabled)} + /> Media From 73fc0094dd5e7056d2612965f81648b9ffe423d0 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 19 Jun 2024 22:45:08 +0100 Subject: [PATCH 57/81] Update HomeHeaderLayoutMobile.tsx (#4572) --- src/view/com/home/HomeHeaderLayoutMobile.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/view/com/home/HomeHeaderLayoutMobile.tsx b/src/view/com/home/HomeHeaderLayoutMobile.tsx index 895baa9a4d..8cf0452cec 100644 --- a/src/view/com/home/HomeHeaderLayoutMobile.tsx +++ b/src/view/com/home/HomeHeaderLayoutMobile.tsx @@ -120,6 +120,7 @@ const styles = StyleSheet.create({ paddingHorizontal: 16, paddingVertical: 5, width: '100%', + minHeight: 46, }, title: { fontSize: 21, From 5c31859f7bb753b07752e2b8faf2248561c46c54 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 19 Jun 2024 23:42:12 +0100 Subject: [PATCH 58/81] fix for autofill covering border (#4573) --- src/components/forms/TextField.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx index 73a660ea6c..f7a827b493 100644 --- a/src/components/forms/TextField.tsx +++ b/src/components/forms/TextField.tsx @@ -196,6 +196,13 @@ export function createInput(Component: typeof TextInput) { textAlignVertical: rest.multiline ? 'top' : undefined, minHeight: rest.multiline ? 80 : undefined, }, + // fix for autofill styles covering border + web({ + paddingTop: 12, + paddingBottom: 12, + marginTop: 2, + marginBottom: 2, + }), android({ paddingBottom: 16, }), From 89d99a87019aced9008f33e37e55bd98ec8a7084 Mon Sep 17 00:00:00 2001 From: Hailey Date: Wed, 19 Jun 2024 16:20:43 -0700 Subject: [PATCH 59/81] use 1000x1000 for image height in avatar cropper (#4453) --- src/screens/Onboarding/StepProfile/index.tsx | 4 ++-- src/view/com/util/UserAvatar.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/screens/Onboarding/StepProfile/index.tsx b/src/screens/Onboarding/StepProfile/index.tsx index 3556bba7a0..5304aa5031 100644 --- a/src/screens/Onboarding/StepProfile/index.tsx +++ b/src/screens/Onboarding/StepProfile/index.tsx @@ -181,8 +181,8 @@ export function StepProfile() { image = await openCropper({ mediaType: 'photo', cropperCircleOverlay: true, - height: image.height, - width: image.width, + height: 1000, + width: 1000, path: image.path, }) } diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index c212ea4c02..e9aa625806 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -303,8 +303,8 @@ let EditableUserAvatar = ({ const croppedImage = await openCropper({ mediaType: 'photo', cropperCircleOverlay: true, - height: item.height, - width: item.width, + height: 1000, + width: 1000, path: item.path, }) From 7d8fca56dc25b31b592692ad385105be42285c72 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 19 Jun 2024 18:47:43 -0500 Subject: [PATCH 60/81] Convert button to use forwardRef (#4576) --- src/components/Button.tsx | 667 ++++++++++++++++++++------------------ 1 file changed, 343 insertions(+), 324 deletions(-) diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 982f422134..deac450eea 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -88,336 +88,355 @@ export function useButtonContext() { return React.useContext(Context) } -export function Button({ - children, - variant, - color, - size, - shape = 'default', - label, - disabled = false, - style, - hoverStyle: hoverStyleProp, - ...rest -}: ButtonProps) { - const t = useTheme() - const [state, setState] = React.useState({ - pressed: false, - hovered: false, - focused: false, - }) - - const onPressIn = React.useCallback(() => { - setState(s => ({ - ...s, - pressed: true, - })) - }, [setState]) - const onPressOut = React.useCallback(() => { - setState(s => ({ - ...s, - pressed: false, - })) - }, [setState]) - const onHoverIn = React.useCallback(() => { - setState(s => ({ - ...s, - hovered: true, - })) - }, [setState]) - const onHoverOut = React.useCallback(() => { - setState(s => ({ - ...s, - hovered: false, - })) - }, [setState]) - const onFocus = React.useCallback(() => { - setState(s => ({ - ...s, - focused: true, - })) - }, [setState]) - const onBlur = React.useCallback(() => { - setState(s => ({ - ...s, - focused: false, - })) - }, [setState]) - - const {baseStyles, hoverStyles} = React.useMemo(() => { - const baseStyles: ViewStyle[] = [] - const hoverStyles: ViewStyle[] = [] - const light = t.name === 'light' - - if (color === 'primary') { - if (variant === 'solid') { - if (!disabled) { - baseStyles.push({ - backgroundColor: t.palette.primary_500, - }) - hoverStyles.push({ - backgroundColor: t.palette.primary_600, - }) - } else { - baseStyles.push({ - backgroundColor: t.palette.primary_700, - }) - } - } else if (variant === 'outline') { - baseStyles.push(a.border, t.atoms.bg, { - borderWidth: 1, - }) - - if (!disabled) { - baseStyles.push(a.border, { - borderColor: t.palette.primary_500, - }) - hoverStyles.push(a.border, { - backgroundColor: light - ? t.palette.primary_50 - : t.palette.primary_950, - }) - } else { - baseStyles.push(a.border, { - borderColor: light ? t.palette.primary_200 : t.palette.primary_900, - }) - } - } else if (variant === 'ghost') { - if (!disabled) { - baseStyles.push(t.atoms.bg) - hoverStyles.push({ - backgroundColor: light - ? t.palette.primary_100 - : t.palette.primary_900, - }) - } - } - } else if (color === 'secondary') { - if (variant === 'solid') { - if (!disabled) { - baseStyles.push({ - backgroundColor: t.palette.contrast_25, - }) - hoverStyles.push({ - backgroundColor: t.palette.contrast_50, - }) - } else { - baseStyles.push({ - backgroundColor: t.palette.contrast_100, - }) - } - } else if (variant === 'outline') { - baseStyles.push(a.border, t.atoms.bg, { - borderWidth: 1, - }) - - if (!disabled) { - baseStyles.push(a.border, { - borderColor: t.palette.contrast_300, - }) - hoverStyles.push(t.atoms.bg_contrast_50) - } else { - baseStyles.push(a.border, { - borderColor: t.palette.contrast_200, - }) - } - } else if (variant === 'ghost') { - if (!disabled) { - baseStyles.push(t.atoms.bg) - hoverStyles.push({ - backgroundColor: t.palette.contrast_100, - }) - } - } - } else if (color === 'negative') { - if (variant === 'solid') { - if (!disabled) { - baseStyles.push({ - backgroundColor: t.palette.negative_500, - }) - hoverStyles.push({ - backgroundColor: t.palette.negative_600, - }) - } else { - baseStyles.push({ - backgroundColor: t.palette.negative_700, - }) - } - } else if (variant === 'outline') { - baseStyles.push(a.border, t.atoms.bg, { - borderWidth: 1, - }) - - if (!disabled) { - baseStyles.push(a.border, { - borderColor: t.palette.negative_500, - }) - hoverStyles.push(a.border, { - backgroundColor: light - ? t.palette.negative_50 - : t.palette.negative_975, - }) - } else { - baseStyles.push(a.border, { - borderColor: light - ? t.palette.negative_200 - : t.palette.negative_900, - }) - } - } else if (variant === 'ghost') { - if (!disabled) { - baseStyles.push(t.atoms.bg) - hoverStyles.push({ - backgroundColor: light - ? t.palette.negative_100 - : t.palette.negative_975, - }) - } - } - } - - if (shape === 'default') { - if (size === 'large') { - baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md) - } else if (size === 'medium') { - baseStyles.push({paddingVertical: 12}, a.px_2xl, a.rounded_sm, a.gap_md) - } else if (size === 'small') { - baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm) - } else if (size === 'xsmall') { - baseStyles.push({paddingVertical: 6}, a.px_sm, a.rounded_sm, a.gap_sm) - } else if (size === 'tiny') { - baseStyles.push({paddingVertical: 4}, a.px_sm, a.rounded_xs, a.gap_xs) - } - } else if (shape === 'round' || shape === 'square') { - if (size === 'large') { - if (shape === 'round') { - baseStyles.push({height: 54, width: 54}) - } else { - baseStyles.push({height: 50, width: 50}) - } - } else if (size === 'small') { - baseStyles.push({height: 34, width: 34}) - } else if (size === 'xsmall') { - baseStyles.push({height: 28, width: 28}) - } else if (size === 'tiny') { - baseStyles.push({height: 20, width: 20}) - } - - if (shape === 'round') { - baseStyles.push(a.rounded_full) - } else if (shape === 'square') { - if (size === 'tiny') { - baseStyles.push(a.rounded_xs) - } else { - baseStyles.push(a.rounded_sm) - } - } - } - - return { - baseStyles, - hoverStyles, - } - }, [t, variant, color, size, shape, disabled]) - - const {gradientColors, gradientHoverColors, gradientLocations} = - React.useMemo(() => { - const colors: string[] = [] - const hoverColors: string[] = [] - const locations: number[] = [] - const gradient = { - primary: tokens.gradients.sky, - secondary: tokens.gradients.sky, - negative: tokens.gradients.sky, - gradient_sky: tokens.gradients.sky, - gradient_midnight: tokens.gradients.midnight, - gradient_sunrise: tokens.gradients.sunrise, - gradient_sunset: tokens.gradients.sunset, - gradient_nordic: tokens.gradients.nordic, - gradient_bonfire: tokens.gradients.bonfire, - }[color || 'primary'] - - if (variant === 'gradient') { - colors.push(...gradient.values.map(([_, color]) => color)) - hoverColors.push(...gradient.values.map(_ => gradient.hover_value)) - locations.push(...gradient.values.map(([location, _]) => location)) - } - - return { - gradientColors: colors, - gradientHoverColors: hoverColors, - gradientLocations: locations, - } - }, [variant, color]) - - const context = React.useMemo( - () => ({ - ...state, +export const Button = React.forwardRef( + ( + { + children, variant, color, size, - disabled: disabled || false, - }), - [state, variant, color, size, disabled], - ) + shape = 'default', + label, + disabled = false, + style, + hoverStyle: hoverStyleProp, + ...rest + }, + ref, + ) => { + const t = useTheme() + const [state, setState] = React.useState({ + pressed: false, + hovered: false, + focused: false, + }) - const flattenedBaseStyles = flatten(baseStyles) + const onPressIn = React.useCallback(() => { + setState(s => ({ + ...s, + pressed: true, + })) + }, [setState]) + const onPressOut = React.useCallback(() => { + setState(s => ({ + ...s, + pressed: false, + })) + }, [setState]) + const onHoverIn = React.useCallback(() => { + setState(s => ({ + ...s, + hovered: true, + })) + }, [setState]) + const onHoverOut = React.useCallback(() => { + setState(s => ({ + ...s, + hovered: false, + })) + }, [setState]) + const onFocus = React.useCallback(() => { + setState(s => ({ + ...s, + focused: true, + })) + }, [setState]) + const onBlur = React.useCallback(() => { + setState(s => ({ + ...s, + focused: false, + })) + }, [setState]) - return ( - { + const baseStyles: ViewStyle[] = [] + const hoverStyles: ViewStyle[] = [] + const light = t.name === 'light' + + if (color === 'primary') { + if (variant === 'solid') { + if (!disabled) { + baseStyles.push({ + backgroundColor: t.palette.primary_500, + }) + hoverStyles.push({ + backgroundColor: t.palette.primary_600, + }) + } else { + baseStyles.push({ + backgroundColor: t.palette.primary_700, + }) + } + } else if (variant === 'outline') { + baseStyles.push(a.border, t.atoms.bg, { + borderWidth: 1, + }) + + if (!disabled) { + baseStyles.push(a.border, { + borderColor: t.palette.primary_500, + }) + hoverStyles.push(a.border, { + backgroundColor: light + ? t.palette.primary_50 + : t.palette.primary_950, + }) + } else { + baseStyles.push(a.border, { + borderColor: light + ? t.palette.primary_200 + : t.palette.primary_900, + }) + } + } else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push(t.atoms.bg) + hoverStyles.push({ + backgroundColor: light + ? t.palette.primary_100 + : t.palette.primary_900, + }) + } + } + } else if (color === 'secondary') { + if (variant === 'solid') { + if (!disabled) { + baseStyles.push({ + backgroundColor: t.palette.contrast_25, + }) + hoverStyles.push({ + backgroundColor: t.palette.contrast_50, + }) + } else { + baseStyles.push({ + backgroundColor: t.palette.contrast_100, + }) + } + } else if (variant === 'outline') { + baseStyles.push(a.border, t.atoms.bg, { + borderWidth: 1, + }) + + if (!disabled) { + baseStyles.push(a.border, { + borderColor: t.palette.contrast_300, + }) + hoverStyles.push(t.atoms.bg_contrast_50) + } else { + baseStyles.push(a.border, { + borderColor: t.palette.contrast_200, + }) + } + } else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push(t.atoms.bg) + hoverStyles.push({ + backgroundColor: t.palette.contrast_100, + }) + } + } + } else if (color === 'negative') { + if (variant === 'solid') { + if (!disabled) { + baseStyles.push({ + backgroundColor: t.palette.negative_500, + }) + hoverStyles.push({ + backgroundColor: t.palette.negative_600, + }) + } else { + baseStyles.push({ + backgroundColor: t.palette.negative_700, + }) + } + } else if (variant === 'outline') { + baseStyles.push(a.border, t.atoms.bg, { + borderWidth: 1, + }) + + if (!disabled) { + baseStyles.push(a.border, { + borderColor: t.palette.negative_500, + }) + hoverStyles.push(a.border, { + backgroundColor: light + ? t.palette.negative_50 + : t.palette.negative_975, + }) + } else { + baseStyles.push(a.border, { + borderColor: light + ? t.palette.negative_200 + : t.palette.negative_900, + }) + } + } else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push(t.atoms.bg) + hoverStyles.push({ + backgroundColor: light + ? t.palette.negative_100 + : t.palette.negative_975, + }) + } + } + } + + if (shape === 'default') { + if (size === 'large') { + baseStyles.push( + {paddingVertical: 15}, + a.px_2xl, + a.rounded_sm, + a.gap_md, + ) + } else if (size === 'medium') { + baseStyles.push( + {paddingVertical: 12}, + a.px_2xl, + a.rounded_sm, + a.gap_md, + ) + } else if (size === 'small') { + baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm) + } else if (size === 'xsmall') { + baseStyles.push({paddingVertical: 6}, a.px_sm, a.rounded_sm, a.gap_sm) + } else if (size === 'tiny') { + baseStyles.push({paddingVertical: 4}, a.px_sm, a.rounded_xs, a.gap_xs) + } + } else if (shape === 'round' || shape === 'square') { + if (size === 'large') { + if (shape === 'round') { + baseStyles.push({height: 54, width: 54}) + } else { + baseStyles.push({height: 50, width: 50}) + } + } else if (size === 'small') { + baseStyles.push({height: 34, width: 34}) + } else if (size === 'xsmall') { + baseStyles.push({height: 28, width: 28}) + } else if (size === 'tiny') { + baseStyles.push({height: 20, width: 20}) + } + + if (shape === 'round') { + baseStyles.push(a.rounded_full) + } else if (shape === 'square') { + if (size === 'tiny') { + baseStyles.push(a.rounded_xs) + } else { + baseStyles.push(a.rounded_sm) + } + } + } + + return { + baseStyles, + hoverStyles, + } + }, [t, variant, color, size, shape, disabled]) + + const {gradientColors, gradientHoverColors, gradientLocations} = + React.useMemo(() => { + const colors: string[] = [] + const hoverColors: string[] = [] + const locations: number[] = [] + const gradient = { + primary: tokens.gradients.sky, + secondary: tokens.gradients.sky, + negative: tokens.gradients.sky, + gradient_sky: tokens.gradients.sky, + gradient_midnight: tokens.gradients.midnight, + gradient_sunrise: tokens.gradients.sunrise, + gradient_sunset: tokens.gradients.sunset, + gradient_nordic: tokens.gradients.nordic, + gradient_bonfire: tokens.gradients.bonfire, + }[color || 'primary'] + + if (variant === 'gradient') { + colors.push(...gradient.values.map(([_, color]) => color)) + hoverColors.push(...gradient.values.map(_ => gradient.hover_value)) + locations.push(...gradient.values.map(([location, _]) => location)) + } + + return { + gradientColors: colors, + gradientHoverColors: hoverColors, + gradientLocations: locations, + } + }, [variant, color]) + + const context = React.useMemo( + () => ({ + ...state, + variant, + color, + size, disabled: disabled || false, - }} - style={[ - a.flex_row, - a.align_center, - a.justify_center, - flattenedBaseStyles, - flatten(style), - ...(state.hovered || state.pressed - ? [hoverStyles, flatten(hoverStyleProp)] - : []), - ]} - onPressIn={onPressIn} - onPressOut={onPressOut} - onHoverIn={onHoverIn} - onHoverOut={onHoverOut} - onFocus={onFocus} - onBlur={onBlur}> - {variant === 'gradient' && ( - - - - )} - - {typeof children === 'function' ? children(context) : children} - - - ) -} + }), + [state, variant, color, size, disabled], + ) + + const flattenedBaseStyles = flatten(baseStyles) + + return ( + + {variant === 'gradient' && ( + + + + )} + + {typeof children === 'function' ? children(context) : children} + + + ) + }, +) +Button.displayName = 'Button' export function useSharedButtonTextStyles() { const t = useTheme() From 75aec19230609f8ae689cd1ec0b2697c5233bdeb Mon Sep 17 00:00:00 2001 From: dan Date: Thu, 20 Jun 2024 04:19:37 +0300 Subject: [PATCH 61/81] Fix threadgate read after write (#4577) * Fix threadgate read-after-write problem * Fix React key (drive-by) --- src/view/com/threadgate/WhoCanReply.tsx | 43 ++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/src/view/com/threadgate/WhoCanReply.tsx b/src/view/com/threadgate/WhoCanReply.tsx index 3ffbaa7ae9..7e3528d928 100644 --- a/src/view/com/threadgate/WhoCanReply.tsx +++ b/src/view/com/threadgate/WhoCanReply.tsx @@ -1,12 +1,19 @@ import React from 'react' import {Keyboard, StyleProp, View, ViewStyle} from 'react-native' -import {AppBskyFeedDefs, AppBskyGraphDefs, AtUri} from '@atproto/api' +import { + AppBskyFeedDefs, + AppBskyFeedGetPostThread, + AppBskyGraphDefs, + AtUri, + BskyAgent, +} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' import {useAnalytics} from '#/lib/analytics/analytics' import {createThreadgate} from '#/lib/api' +import {until} from '#/lib/async/until' import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle' import {usePalette} from '#/lib/hooks/usePalette' import {makeListLink, makeProfileLink} from '#/lib/routes/links' @@ -85,6 +92,18 @@ export function WhoCanReply({ rkey: new AtUri(post.uri).rkey, }) } + await whenAppViewReady(agent, post.uri, res => { + const thread = res.data.thread + if (AppBskyFeedDefs.isThreadViewPost(thread)) { + const fetchedSettings = threadgateViewToSettings( + thread.post.threadgate, + ) + return ( + JSON.stringify(fetchedSettings) === JSON.stringify(newSettings) + ) + } + return false + }) Toast.show('Thread settings updated') queryClient.invalidateQueries({ queryKey: [POST_THREAD_RQKEY_ROOT], @@ -133,15 +152,14 @@ export function WhoCanReply({ Only{' '} {settings.map((rule, i) => ( - <> + - + ))}{' '} can reply. @@ -227,3 +245,20 @@ function Separator({i, length}: {i: number; length: number}) { } return <>, } + +async function whenAppViewReady( + agent: BskyAgent, + uri: string, + fn: (res: AppBskyFeedGetPostThread.Response) => boolean, +) { + await until( + 5, // 5 tries + 1e3, // 1s delay between tries + fn, + () => + agent.app.bsky.feed.getPostThread({ + uri, + depth: 0, + }), + ) +} From 80197556f176723b619349dc060d1b7001472d47 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 19 Jun 2024 18:39:45 -0700 Subject: [PATCH 62/81] Rework "Who can reply" to blend more nicely into the UI (#4578) * Rework WhoCanReply controls in threads to blend more nicely * Fix layout * Fix post control hitslops * Move dialog content to separate component --------- Co-authored-by: Dan Abramov --- src/lib/constants.ts | 1 + src/view/com/post-thread/PostThreadItem.tsx | 38 +- src/view/com/threadgate/WhoCanReply.tsx | 447 +++++++++++------- src/view/com/util/post-ctrls/PostCtrls.tsx | 10 +- src/view/com/util/post-ctrls/RepostButton.tsx | 4 +- 5 files changed, 314 insertions(+), 186 deletions(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 05d1591f56..e0b8998007 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -84,6 +84,7 @@ export const createHitslop = (size: number): Insets => ({ export const HITSLOP_10 = createHitslop(10) export const HITSLOP_20 = createHitslop(20) export const HITSLOP_30 = createHitslop(30) +export const POST_CTRL_HITSLOP = {top: 5, bottom: 10, left: 10, right: 10} export const BACK_HITSLOP = HITSLOP_30 export const MAX_POST_LINES = 25 diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 6d03029d7f..92b529db78 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -25,7 +25,7 @@ import {sanitizeHandle} from 'lib/strings/handles' import {countLines} from 'lib/strings/helpers' import {niceDate} from 'lib/strings/time' import {s} from 'lib/styles' -import {isNative, isWeb} from 'platform/detection' +import {isWeb} from 'platform/detection' import {useSession} from 'state/session' import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn' import {atoms as a} from '#/alf' @@ -35,7 +35,7 @@ import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe' import {PostAlerts} from '../../../components/moderation/PostAlerts' import {PostHider} from '../../../components/moderation/PostHider' import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers' -import {WhoCanReply} from '../threadgate/WhoCanReply' +import {WhoCanReplyBlock, WhoCanReplyInline} from '../threadgate/WhoCanReply' import {ErrorMessage} from '../util/error/ErrorMessage' import {Link, TextLink} from '../util/Link' import {formatCount} from '../util/numeric/format' @@ -340,6 +340,7 @@ let PostThreadItemLoaded = ({ @@ -396,11 +397,6 @@ let PostThreadItemLoaded = ({
- ) } else { @@ -579,14 +575,7 @@ let PostThreadItemLoaded = ({ ) : undefined} - + ) } @@ -654,10 +643,12 @@ function PostOuterWrapper({ function ExpandedPostDetails({ post, + isThreadAuthor, needsTranslation, translatorUrl, }: { post: AppBskyFeedDefs.PostView + isThreadAuthor: boolean needsTranslation: boolean translatorUrl: string }) { @@ -670,14 +661,23 @@ function ExpandedPostDetails({ }, [openLink, translatorUrl]) return ( - - {niceDate(post.indexedAt)} + + {niceDate(post.indexedAt)} + {needsTranslation && ( <> - · + · Translate diff --git a/src/view/com/threadgate/WhoCanReply.tsx b/src/view/com/threadgate/WhoCanReply.tsx index 7e3528d928..3f9970f5fb 100644 --- a/src/view/com/threadgate/WhoCanReply.tsx +++ b/src/view/com/threadgate/WhoCanReply.tsx @@ -11,13 +11,10 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' -import {useAnalytics} from '#/lib/analytics/analytics' import {createThreadgate} from '#/lib/api' import {until} from '#/lib/async/until' -import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle' -import {usePalette} from '#/lib/hooks/usePalette' +import {HITSLOP_10} from '#/lib/constants' import {makeListLink, makeProfileLink} from '#/lib/routes/links' -import {colors} from '#/lib/styles' import {logger} from '#/logger' import {isNative} from '#/platform/detection' import {useModalControls} from '#/state/modals' @@ -28,45 +25,301 @@ import { } from '#/state/queries/threadgate' import {useAgent} from '#/state/session' import * as Toast from 'view/com/util/Toast' +import {atoms as a, useTheme} from '#/alf' import {Button} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {useDialogControl} from '#/components/Dialog' +import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign' +import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe' +import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group' +import {Text} from '#/components/Typography' import {TextLink} from '../util/Link' -import {Text} from '../util/text/Text' -export function WhoCanReply({ - post, - isThreadAuthor, - style, -}: { +interface WhoCanReplyProps { post: AppBskyFeedDefs.PostView isThreadAuthor: boolean style?: StyleProp -}) { - const {track} = useAnalytics() +} + +export function WhoCanReplyInline({ + post, + isThreadAuthor, + style, +}: WhoCanReplyProps) { const {_} = useLingui() - const pal = usePalette('default') + const t = useTheme() + const infoDialogControl = useDialogControl() + const {settings, isRootPost, onPressEdit} = useWhoCanReply(post) + + if (!isRootPost) { + return null + } + if (!settings.length && !isThreadAuthor) { + return null + } + + const isEverybody = settings.length === 0 + const isNobody = !!settings.find(gate => gate.type === 'nobody') + const description = isEverybody + ? _(msg`Everybody can reply`) + : isNobody + ? _(msg`Replies disabled`) + : _(msg`Some people can reply`) + + return ( + <> + + + + ) +} + +export function WhoCanReplyBlock({ + post, + isThreadAuthor, + style, +}: WhoCanReplyProps) { + const {_} = useLingui() + const t = useTheme() + const infoDialogControl = useDialogControl() + const {settings, isRootPost, onPressEdit} = useWhoCanReply(post) + + if (!isRootPost) { + return null + } + if (!settings.length && !isThreadAuthor) { + return null + } + + const isEverybody = settings.length === 0 + const isNobody = !!settings.find(gate => gate.type === 'nobody') + const description = isEverybody + ? _(msg`Everybody can reply`) + : isNobody + ? _(msg`Replies on this thread are disabled`) + : _(msg`Some people can reply`) + + return ( + <> + + + + ) +} + +function Icon({ + color, + width, + settings, +}: { + color: string + width?: number + settings: ThreadgateSetting[] +}) { + const isEverybody = settings.length === 0 + const isNobody = !!settings.find(gate => gate.type === 'nobody') + const IconComponent = isEverybody ? Earth : isNobody ? CircleBanSign : Group + return +} + +function InfoDialog({ + control, + post, + settings, +}: { + control: Dialog.DialogControlProps + post: AppBskyFeedDefs.PostView + settings: ThreadgateSetting[] +}) { + return ( + + + + + ) +} + +function InfoDialogInner({ + post, + settings, +}: { + post: AppBskyFeedDefs.PostView + settings: ThreadgateSetting[] +}) { + const {_} = useLingui() + return ( + + + + Who can reply? + + + + + ) +} + +function Rules({ + post, + settings, +}: { + post: AppBskyFeedDefs.PostView + settings: ThreadgateSetting[] +}) { + const t = useTheme() + return ( + + {!settings.length ? ( + Everybody can reply + ) : settings[0].type === 'nobody' ? ( + Replies to this thread are disabled + ) : ( + + Only{' '} + {settings.map((rule, i) => ( + <> + + + + ))}{' '} + can reply + + )} + + ) +} + +function Rule({ + rule, + post, + lists, +}: { + rule: ThreadgateSetting + post: AppBskyFeedDefs.PostView + lists: AppBskyGraphDefs.ListViewBasic[] | undefined +}) { + const t = useTheme() + if (rule.type === 'mention') { + return mentioned users + } + if (rule.type === 'following') { + return ( + + users followed by{' '} + + + ) + } + if (rule.type === 'list') { + const list = lists?.find(l => l.uri === rule.list) + if (list) { + const listUrip = new AtUri(list.uri) + return ( + + {' '} + members + + ) + } + } +} + +function Separator({i, length}: {i: number; length: number}) { + if (length < 2 || i === length - 1) { + return null + } + if (i === length - 2) { + return ( + <> + {length > 2 ? ',' : ''} and{' '} + + ) + } + return <>, +} + +function useWhoCanReply(post: AppBskyFeedDefs.PostView) { const agent = useAgent() const queryClient = useQueryClient() const {openModal} = useModalControls() - const containerStyles = useColorSchemeStyle( - { - backgroundColor: pal.colors.unreadNotifBg, - }, - { - backgroundColor: pal.colors.unreadNotifBg, - }, - ) - const textStyles = useColorSchemeStyle( - {color: colors.blue5}, - {color: colors.blue1}, - ) - const hoverStyles = useColorSchemeStyle( - { - backgroundColor: colors.white, - }, - { - backgroundColor: pal.colors.background, - }, - ) + const settings = React.useMemo( () => threadgateViewToSettings(post.threadgate), [post], @@ -74,7 +327,6 @@ export function WhoCanReply({ const isRootPost = !('reply' in post.record) const onPressEdit = () => { - track('Post:EditThreadgateOpened') if (isNative && Keyboard.isVisible()) { Keyboard.dismiss() } @@ -108,7 +360,6 @@ export function WhoCanReply({ queryClient.invalidateQueries({ queryKey: [POST_THREAD_RQKEY_ROOT], }) - track('Post:ThreadgateEdited') } catch (err) { Toast.show( 'There was an issue. Please check your internet connection and try again.', @@ -119,131 +370,7 @@ export function WhoCanReply({ }) } - if (!isRootPost) { - return null - } - if (!settings.length && !isThreadAuthor) { - return null - } - - return ( - - - - {!settings.length ? ( - Everybody can reply. - ) : settings[0].type === 'nobody' ? ( - Replies to this thread are disabled. - ) : ( - - Only{' '} - {settings.map((rule, i) => ( - - - - - ))}{' '} - can reply. - - )} - - - {isThreadAuthor && ( - - - - )} - - ) -} - -function Rule({ - rule, - post, - lists, -}: { - rule: ThreadgateSetting - post: AppBskyFeedDefs.PostView - lists: AppBskyGraphDefs.ListViewBasic[] | undefined -}) { - const pal = usePalette('default') - if (rule.type === 'mention') { - return mentioned users - } - if (rule.type === 'following') { - return ( - - users followed by{' '} - - - ) - } - if (rule.type === 'list') { - const list = lists?.find(l => l.uri === rule.list) - if (list) { - const listUrip = new AtUri(list.uri) - return ( - - {' '} - members - - ) - } - } -} - -function Separator({i, length}: {i: number; length: number}) { - if (length < 2 || i === length - 1) { - return null - } - if (i === length - 2) { - return ( - <> - {length > 2 ? ',' : ''} and{' '} - - ) - } - return <>, + return {settings, isRootPost, onPressEdit} } async function whenAppViewReady( diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index 55fb4a334b..472ce4043a 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -15,7 +15,7 @@ import { import {msg, plural} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {HITSLOP_10, HITSLOP_20} from '#/lib/constants' +import {POST_CTRL_HITSLOP} from '#/lib/constants' import {useHaptics} from '#/lib/haptics' import {makeProfileLink} from '#/lib/routes/links' import {shareUrl} from '#/lib/sharing' @@ -215,7 +215,7 @@ let PostCtrls = ({ other: 'Reply (# replies)', })} accessibilityHint="" - hitSlop={big ? HITSLOP_20 : HITSLOP_10}> + hitSlop={POST_CTRL_HITSLOP}> + hitSlop={POST_CTRL_HITSLOP}> {post.viewer?.like ? ( ) : ( @@ -299,7 +299,7 @@ let PostCtrls = ({ }} accessibilityLabel={_(msg`Share`)} accessibilityHint="" - hitSlop={big ? HITSLOP_20 : HITSLOP_10}> + hitSlop={POST_CTRL_HITSLOP}> diff --git a/src/view/com/util/post-ctrls/RepostButton.tsx b/src/view/com/util/post-ctrls/RepostButton.tsx index 10bc369b8f..d49cda442c 100644 --- a/src/view/com/util/post-ctrls/RepostButton.tsx +++ b/src/view/com/util/post-ctrls/RepostButton.tsx @@ -3,7 +3,7 @@ import {View} from 'react-native' import {msg, plural} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {HITSLOP_10, HITSLOP_20} from '#/lib/constants' +import {POST_CTRL_HITSLOP} from '#/lib/constants' import {useHaptics} from '#/lib/haptics' import {useRequireAuth} from '#/state/session' import {atoms as a, useTheme} from '#/alf' @@ -67,7 +67,7 @@ let RepostButton = ({ shape="round" variant="ghost" color="secondary" - hitSlop={big ? HITSLOP_20 : HITSLOP_10}> + hitSlop={POST_CTRL_HITSLOP}> {typeof repostCount !== 'undefined' && repostCount > 0 ? ( Date: Thu, 20 Jun 2024 12:30:48 -0500 Subject: [PATCH 63/81] Merge #4492, fixes profile menu hover (#4580) * Fix button hover color (#4492) * Update ProfileMenu.tsx * Update Button.tsx * Update ProfileFeed.tsx * Update ProfileFeed.tsx * Re-add change post conflict --------- Co-authored-by: Minseo Lee --- src/components/Button.tsx | 2 +- src/view/com/profile/ProfileMenu.tsx | 23 +++++++++++------------ src/view/screens/ProfileFeed.tsx | 13 +++++++------ 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/components/Button.tsx b/src/components/Button.tsx index deac450eea..54d9eaf3b0 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -231,7 +231,7 @@ export const Button = React.forwardRef( if (!disabled) { baseStyles.push(t.atoms.bg) hoverStyles.push({ - backgroundColor: t.palette.contrast_100, + backgroundColor: t.palette.contrast_25, }) } } diff --git a/src/view/com/profile/ProfileMenu.tsx b/src/view/com/profile/ProfileMenu.tsx index efc2497600..f5e050d707 100644 --- a/src/view/com/profile/ProfileMenu.tsx +++ b/src/view/com/profile/ProfileMenu.tsx @@ -9,7 +9,6 @@ import {useQueryClient} from '@tanstack/react-query' import {logger} from '#/logger' import {useAnalytics} from 'lib/analytics/analytics' import {HITSLOP_10} from 'lib/constants' -import {usePalette} from 'lib/hooks/usePalette' import {makeProfileLink} from 'lib/routes/links' import {shareUrl} from 'lib/sharing' import {toShareUrl} from 'lib/strings/url-helpers' @@ -24,7 +23,7 @@ import { import {useSession} from 'state/session' import {EventStopper} from 'view/com/util/EventStopper' import * as Toast from 'view/com/util/Toast' -import {useTheme} from '#/alf' +import {atoms as a, useTheme} from '#/alf' import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag' import {ListSparkle_Stroke2_Corner0_Rounded as List} from '#/components/icons/ListSparkle' @@ -49,7 +48,7 @@ let ProfileMenu = ({ const {currentAccount, hasSession} = useSession() const t = useTheme() // TODO ALF this - const pal = usePalette('default') + const alf = useTheme() const {track} = useAnalytics() const {openModal} = useModalControls() const reportDialogControl = useReportDialogControl() @@ -187,21 +186,21 @@ let ProfileMenu = ({ - {({props}) => { + {({props, state}) => { return ( - ) From 51f5e6bf900685ef92191f22949d09035733c682 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Thu, 20 Jun 2024 17:45:52 -0400 Subject: [PATCH 64/81] Bsky link card service (#4547) * setup bskycard * quick proof of concept for png card generation * bskycard: use jsx * bskycard: 3x5 profile layout * bskycard: add butterfly overlay * bskycard: tidy * bskycard: separate and reorganize * bskycard: tidy * bskycard: tidy * bskycard: tidy * bskycard: poc of transparent overlay and box shadow * bskycard: reorg impl into src/ directory * bskycard: use more standard app structure * bskycard: setup dockerfile, fix build * bskycard: support for x-origin-verify * bskycard: card layout, filter images based on labels * bskycard: tidy * bskycard: support cluster mode * bskycard: handle error fetching starter pack info * bskycard: tidy * bskycard: fix leak on failed image fetch * bskycard: build workflow * bskyogcard: rename from bskycard * bskyogcard: fix some express plumbing * bskyogcard: add cdn tags, tidy --- .../workflows/build-and-push-ogcard-aws.yaml | 55 + Dockerfile.bskyogcard | 41 + bskyogcard/package.json | 24 + bskyogcard/src/assets/Inter-Bold.ttf | Bin 0 -> 316584 bytes bskyogcard/src/bin.ts | 48 + bskyogcard/src/components/Butterfly.tsx | 16 + bskyogcard/src/components/Img.tsx | 10 + bskyogcard/src/components/StarterPack.tsx | 149 +++ bskyogcard/src/config.ts | 40 + bskyogcard/src/context.ts | 44 + bskyogcard/src/index.ts | 41 + bskyogcard/src/logger.ts | 3 + bskyogcard/src/routes/health.ts | 14 + bskyogcard/src/routes/index.ts | 13 + bskyogcard/src/routes/starter-pack.tsx | 102 ++ bskyogcard/src/routes/util.ts | 36 + bskyogcard/tsconfig.json | 11 + bskyogcard/yarn.lock | 1113 +++++++++++++++++ 18 files changed, 1760 insertions(+) create mode 100644 .github/workflows/build-and-push-ogcard-aws.yaml create mode 100644 Dockerfile.bskyogcard create mode 100644 bskyogcard/package.json create mode 100644 bskyogcard/src/assets/Inter-Bold.ttf create mode 100644 bskyogcard/src/bin.ts create mode 100644 bskyogcard/src/components/Butterfly.tsx create mode 100644 bskyogcard/src/components/Img.tsx create mode 100644 bskyogcard/src/components/StarterPack.tsx create mode 100644 bskyogcard/src/config.ts create mode 100644 bskyogcard/src/context.ts create mode 100644 bskyogcard/src/index.ts create mode 100644 bskyogcard/src/logger.ts create mode 100644 bskyogcard/src/routes/health.ts create mode 100644 bskyogcard/src/routes/index.ts create mode 100644 bskyogcard/src/routes/starter-pack.tsx create mode 100644 bskyogcard/src/routes/util.ts create mode 100644 bskyogcard/tsconfig.json create mode 100644 bskyogcard/yarn.lock diff --git a/.github/workflows/build-and-push-ogcard-aws.yaml b/.github/workflows/build-and-push-ogcard-aws.yaml new file mode 100644 index 0000000000..5d6ff041d3 --- /dev/null +++ b/.github/workflows/build-and-push-ogcard-aws.yaml @@ -0,0 +1,55 @@ +name: build-and-push-ogcard-aws +on: + push: + branches: + - divy/bskycard + +env: + REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} + USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }} + PASSWORD: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_PASSWORD }} + IMAGE_NAME: bskyogcard + +jobs: + ogcard-container-aws: + if: github.repository == 'bluesky-social/social-app' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup Docker buildx + uses: docker/setup-buildx-action@v1 + + - name: Log into registry ${{ env.REGISTRY }} + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ env.USERNAME}} + password: ${{ env.PASSWORD }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=sha,enable=true,priority=100,prefix=,suffix=,format=long + + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v4 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + file: ./Dockerfile.bskyogcard + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile.bskyogcard b/Dockerfile.bskyogcard new file mode 100644 index 0000000000..aa68add595 --- /dev/null +++ b/Dockerfile.bskyogcard @@ -0,0 +1,41 @@ +FROM node:20.11-alpine3.18 as build + +# Move files into the image and install +WORKDIR /app + +COPY ./bskyogcard/package.json ./ +COPY ./bskyogcard/yarn.lock ./ +RUN yarn install --frozen-lockfile + +COPY ./bskyogcard ./ + +# build then prune dev deps +RUN yarn build +RUN yarn install --production --ignore-scripts --prefer-offline + +# Uses assets from build stage to reduce build size +FROM node:20.11-alpine3.18 + +RUN apk add --update dumb-init + +# Avoid zombie processes, handle signal forwarding +ENTRYPOINT ["dumb-init", "--"] + +WORKDIR /app +COPY --from=build /app /app +RUN mkdir /app/data && chown node /app/data + +VOLUME /app/data +EXPOSE 3000 +ENV CARD_PORT=3000 +ENV NODE_ENV=production +# potential perf issues w/ io_uring on this version of node +ENV UV_USE_IO_URING=0 + +# https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#non-root-user +USER node +CMD ["node", "--heapsnapshot-signal=SIGUSR2", "--enable-source-maps", "dist/bin.js"] + +LABEL org.opencontainers.image.source=https://github.com/bluesky-social/social-app +LABEL org.opencontainers.image.description="Bsky Card Service" +LABEL org.opencontainers.image.licenses=UNLICENSED diff --git a/bskyogcard/package.json b/bskyogcard/package.json new file mode 100644 index 0000000000..3be1337fc3 --- /dev/null +++ b/bskyogcard/package.json @@ -0,0 +1,24 @@ +{ + "name": "bskyogcard", + "version": "0.0.0", + "type": "module", + "main": "src/index.ts", + "scripts": { + "start": "node --loader ts-node/esm ./src/bin.ts", + "build": "tsc && cp -r src/assets dist/assets" + }, + "dependencies": { + "@atproto/api": "0.12.19-next.0", + "@atproto/common": "^0.4.0", + "@resvg/resvg-js": "^2.6.2", + "express": "^4.19.2", + "http-terminator": "^3.2.0", + "pino": "^9.2.0", + "react": "^18.3.1", + "satori": "^0.10.13" + }, + "devDependencies": { + "@types/node": "^20.14.3", + "typescript": "^5.4.5" + } +} diff --git a/bskyogcard/src/assets/Inter-Bold.ttf b/bskyogcard/src/assets/Inter-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..fe23eeb9c93a377d0f4ab003f1f77b555d19b1d1 GIT binary patch literal 316584 zcmd?S2b2}X+Nk|jb=-S~ArBdbAUWqGNX|K^sK78Vb-kFkH>S)7w-E1b!XO7Rb9E>daJstx_7rI5y^&Oi?nFk zta+7cRkn-h`gr7R(Xm69;g8&sThxSHqADi0=+domn^Hr%i1a)z(rsLaE|sbl-#x6c zi29gZdUxsCsOt~ke^`&>1spd`8=5-e*Eu&$5ta9xNWc93Q%8?5r4gPB+wVW*_VjZ} zb-ow9uDs~dcMRy4+PD0psnZDWO?b@#cof>7uLsA}M@kGBI%Zt0AB!Cp$=Xz;)czsE z(^4ZdAL%Mmi#$ucGcWTDZ^5S_G><2g$m60J?u_f}e%#%bJA)Yz9>d!H;M53#IC$B=2 z&Tjc-cELe^iX)LTfsbW>^3!J6xjo{L_yu(ir#^2#`i5!6x@(mQb5UPWw& zrj>LTa{OM*wvNh9i(@U0g|8O-xaf==vMeJ^-?>Y>F7mjfWMstJ7hSDall4-f|%XW1G6!pV^;ber|t` z`KA38<~Q~am>CY*)U zUJ0)RW)tsb%wAqE%!%F<%zM0hFz@y5#k|j(jXB4ggE`Nehq=H*4&EY=t^^?t+Wckg#mzU?!1`~*J%cNQNh`MLaDn8|)JW*$Eeq51s$xKn&uCq`DlxOK2lh$NRG42dH;K%?TFo%*te4Ak2SkG4fNpo9}%hh#g`-hZa+zvr)O-aK@JB(CUd`9Cl?yBSTpf02Svev_cv`tAd znpzB{#zyA12CW)MI#WU@Nz)>u4Wk1&jz)H|`i~FlYP**DPKbmq)&loKjEfvAYvtp2Yf5JP^ z_iY*5M)OR)niFDbmLgqnrQ^Dd78^}4bv7|#W4Ir&j3$^~iq+oqaZGPa+%AlG(?3`0 zcqL~uCelb7%b`56x^rY&8_Kl`H8iALRkJ^q8`dTjiN|23Jy6$ejQFEYE7xv@#N{!xeu_PbzSnDmCz{EH%l$ zGSa(}Q%Yt|SEuYuueO7FSH-+iW;pj=$)yQxAHhsy^e>z*t5T97*0V-auEbA~j>H|# z8K@unVRWz&5;ZeXY%HWm>CD~>dm`4F0mSP=8`Fq6l>P{%Q;Cv|HXDhT5lOgT6*ns& zzpga-{|=Ww^6O@lhGf6)<4L{jdkY6zb0dTQft|0a9{kldFDTeOTHQN|04c!3(-<@Ar_q zqhn-3w3yr;Z6H&kedTWdHJRYAmU;h>b~yE1pRvz1HhRa3V3r|LA!dii|6q?Gd{jK1 zasM-FP_Mt^8|iDpBmJO+Y)r@@8xwoP(nM>?`e;sB8aXX%5+kxQVXG`lI4DmgoR>8T z*=2b`Em8ywO^E__5xYsZj_a7Z&}9r^C@?^tZ}x>a_2Ky9?LJ| zNyn9y_Ls6FmQTjLP7CS#UnAqYtJ8TA>FXXvmcPMTbaV&yNwA^pl`_0G($^~|L~q85v(hZjlkuUo*aq z&dK;7T0Y~)Xxog_kyaVs2KmgFrhmvUo;IPX%<@u@e@JuL5lLe{|Cke`?SVf19q8fJ;l9f_pF>@-|2HvZ{2Bk2jGwPQ&m{Dd8vhLM zW9jsloRhl#Gt%0_{+C>od?rrxNhucCi83%@k6fRaE`<`FMCS24qK&0bv|Prs!Etw4 z=>2;<;`Wg~{}~x${_8{OYC=8)e;_g7gP z*v4%bD?{AzGQ`V?J&`=j@!t`)<5wB!)rytreJNx8e1BbjVrv;0lI4JmjQ$pnHyW@5h(4{wY6 zQJBMVHO`wUxbeIE8uqMMoFK2o9B%`YHjeSnxXbxHu(vr?q=Esz)^SYyZS420Wlwpn zdt=;A&uAMQ8^0KlcwN3F{(aV}j0x6US?kP{O7;^OdF*>+bG#mn>|&-(V(R+{EPyt_ zwZhPh%T~vX`=NM{jx>RPM|sGkiMcZ#A^a39GS>%DAKAuOiERjv1GyX7d-{kPp2v;UziQ{L757t4sg#8|sbUS^EHAe#f* zxUVqMyBGbQDI+6AWR^Wq*2c)VAG@lPC{^tea<5%dmR$*xCb%2h6Ej}Io*MUGh&>y> zh4u>a2q2G4H)&Q#GkZnGPV9T){wD4m{1&ay74V9M8iO@|!F*!sGvnjST9B1nFDLa_%xMcN@wI_q?p|-1u?m zf0fPsoi$#ntan+v1lJW@qj3#&%pWA*dg*eQ>*dvve1ttH6C;UIJ>q5@^e<%WjyxcZ z66$6gjGoGPGrBzEc=WZ5W6{PLZ$$@YycL=Cuj82f4Bo5KGcqFMtg-#7a*J1%bK4N; zmoc9^GEEYGluib(k<9V9-uB*uWz@%KKfu)WGRL$(GKuTP9n3e4St|@uPQWN*%Y|rE z`X~z`p4Hzb~0(&giH^W%-{v`b~ z!@@XM+qq=$|Md9ybADb;IcCj}eb$VO{U(n_#bk^)Loz z!UT6cW2Cbz^sCDz#>dJlWK4Fu$bIPS2DhyA;u>fzJRY}=`@ck_xjgT0LWel7L|ey+y`{}zEx#?H9e>n&9ytEIWYc|&e;pGU{gGj}8$$9)?p&XXOahr3GJ zdt2c=dnKb}ZDcZWz=R*qr0t;?jdVnnT=Nv3ba=8~2|?-2J$*Bjgbsf_of( zyJQeI7uUK!OAAAEyv&Ri;5b=^M2b=N&C=8DN&L$Ar7~{o$@|>Fo#fvp4a_)k-$qwi ziv`!CZKSl9Ca*YUWT00}9%YPAW{h=aK1}y+k@PGVSmQL14gM+Vo-mbhHduBC^H2lH z@Htc$ZYqt z%rOM>$E|XYmq(sSE#gssk9f3u8BFsg$y9HW+JyZPZ1nCR zk4bVAR>CQG5>|tWw;g*8%!cD|&>JKptvPbUD=tTj9oZ#E+{!XIv9}DyJs9_7b8KvT z61UDLNjvPaF{Ba4hU<-;(4BMkAL9HWY&O@wg_!Sl*sZ0wT~IZ(-cY5C+bNCw4U>3U z1+U0m+ZF4B{wqn;<-bX&QfCo%4~pO8iNGU*6y z2ybuYkQMaD3U8yVh-z7J^|9W?^~IMm$Lc1JSbKZC8$$0twkEp0O-XQ0(74{c#+r;|--i7}-4f=o5X^?T*or*qn zmZ8oqa*JD2zA$+0WIB6BUm1+s%f^_vAS2N45&qLM$xDK3Ws;L5i=CV0E2o?6)VB1r z=gA;Ns zOADL3i25rj7`PY8Ad{!IG7i{XaBK2@UIv*m?8(GmD?^}#RhHw@(m;PM#q3$Qdq_d9 z9}2TKSk}1hT9WQ0%T3JJ>BR5Dvobe1zI3zq$}szL>1wA-bNeXApGyxbN!CL-H~>ZM z)9fo6zC(Zi|5Vqx($T6!+bS~t`bcMchO|M(Y4#v#!xKym>|R{A4w4R*zI?6yZ^pHU z%L4(USYPdt#ZdbHMq2iuyGG|q*T_2TCDNJkpXQd7R_t{&;F_sxLMcg0s4kr&W8}L; zSH4SHE#D;!lllpta1C}4(xra%g4B1$>0~Fbv~$L(2kfG%taCz68{D$;8S~2)r@bt8 zYpUwl&%-XOhHSR$%0Bqo#BqDbGj?HB*SeOwy5@NU`JHD5FEod3&>o(WA{Wvn?}htd zm6W)+M3OG>oZ^LvFkebu+$cpZj*+C_)1}PCQiRXLzgG}nF6ESR7oU_8=U*Yta7jA< zE&fCG13ZWaY7-C3G-18aQY3LK^Iv=}wAK;6lkh{a`H^`gh$}qZX#Nrl%f>84WpTm; zSPDUrG8fYp6~Pg;Fqk?H*!sL+N|OH3ew-}$@N_u zc_48L=h)t|ow@4nB;s)YX>DYgtctdfS?mdJ^lxHq*hMazdogAm>S6K)~u5U z$PTWdhj88XwHLpK;4hQnSre#JxWC>*=27m<=*_a9HCj>D7O!WyhxJAUxh3HSSsq-Q zvc3)1y7qP^okACmNcM|I?WVdaPaj{9{FmkgYjm?NH}^?Qn@xG_@f&|LF9mx5qcdKY z{1@{v?=`26bKRFoq@bh(XO2OJ$aw`_8@-D&KH+sZ)1-Ch&pWLdg>mlq7>y^(a&ls6Rd3V z0%Lvv*GcWFC>!YXX^1-Wp_Sqp|pnM;mYW0^Xm|NGIwCyi~^iPV1=dCt=PWD2s`>70^U z=*fJip_JCO(cM(NZk(4puA`NYm6 zkC}Zp|#`k+w$j%7@M{DIqz} zpXU7hBl&ah+LV1^xL*@JXv^BCoBYmtU?St>ZqsLGO-{d$vm47|`-IHbTV-Y>yZpv} z^Fnm?Q>$#oo7Mv|M;(zNaXMq80~fzFWFIe0qGuQ<+~*C}mcl*y4zV$We{erNsUO!k z9i)jFPn;7sMjG)9gh!fk>{&LHK~`d>xxV45AB@d<%pX1F z8OFsL%6tjEnZxly*7z$7)OR#t8=0>rv+q9>cd(bh{=r(e0(<-s8AW~9qmx^?emsV5 zd|-44)&`VRUN8;@$^FhC^)O>;mX#or?4t4=^%@hi&^ZkVaxnF@=pAjoM~qKQpf5dzfsrUp7EHg&EBKgS21(AnJ;&9y!sKk^^KFN9Oq`0%)BwhZYpM9alAcVp0vlwI>LW- zdMGyxGx-H+Nl$(|IVZhoHC1H;+Q?a}z4C(f3+wtwD$kpws-fur(~R>~!MI9P1>G7d zkNXpA|Ci-WeHZJXZF0y0F~8XeNpVF_%71MnqWW<{3_$j_|Wte!9yX2D9> z2gl$nKiEowl0ZDWJD{uVfQY>SHo$AF2cF*bM#Orq_8 z@tr{3638ooyb|`q3An&&G(S+^EYvFta?A3lND|{PsRodiGnpja1rNj1{GP7})PfE$ z31~-F(r2Z;S&>avWRnfqWJ5OD8UV7%HVmf264(j{fc9p)ERwx2)P(ks4ijKLtcQI- zyd1P8M=H>tIheC^JOMAl`|yKEPShqR^Gr_UowFNI=bY3zCw0zAopT=Hr+Kt5S1Y(p zBsYDLn|3DmfKf0Lmcw>H&dJC*j|S$Jyk&th@_9hp^U?Nvv^`&cmb0vfPN`V-3n8;!qlw@bt^*MiZq5EFbZbCx*XNl23o+X}y zE%27eHOWvBnnQO$&#qYx+uxF zoKoM5ly-o=EsdU*z8xNdHSj#V4j+n?$qJ>QAzTl`;T~8D&%xX9l}K6IT$a8oi`>d0 zw{pm>Tm@(b{oq!Z29Jr9r)}j)SDthgMu=2Ib`_Cb#cDu46_H&5(WI15|@*FrZK3Y1fsaw>0ux8NlFE>fis)PQ!-5AK48;c0jg4#Q6( zRTF?btJ3DGHv+P)`hZBa+)x3o1@fqN3k(IytAF;01UOz7>8d1_huR zw1L5Jhe*BRuo1|+9(mU%@A~9jzYcVQfp9nM6uCA5S^)ie?UN!6azI&V3_V~J%mnnV z0eaWqAfR^*(YuD|UBjx-8v4L^m;-BJ7aWB%!uA>zgSyZe2Ek-l2peI)NTX7KjyDc-ouK?|9O#7NdfH+N%L6do~4)*Z#aQe9^ebkhGZc0BlrJtKV z4SPkJIY3`Fs|56Mvl{_9H+uk(Lo;-t+57Ol@F)}%0p|MVjKSv2_08u4bA9uDaExEJ zmw`rrd|Oa{%eFwjx1`@&Qm2-i;Z67gE{U|t1?8bB5U&;SS`n`mvTKFxS}%j=0Xek( zTBMDFJV1Th&|htO!EG>Gq%CRNlC~}7w%r3Ci?pM??PyCo+R|=|NPF^ZPk*$hKRRfb z2#>%s@G5)-86q7iqa$T>q>PS~(UCGb60aliIuftrC-AFCrz~&{TnpV`C`^GxunFFP z&*7p-=bTUun!rtfTstGzE+a*{=7tK;3~m8r)U5;jAkuv+3-q zcnyw=-0-l-jjiCYNRQ{>ZJf|fpO5MBlL!G zaF*K;Rbe6=fG^=P?+;*(?E4b$HCP15wBH+mO#59FNk^vX$RQn>rr!jVl}=gxivwe7 zKu!2YWZ+oXD>A4CkoRC@Js4RJM%IIGg$LkCcoE)*lOjV3LoIj^Rs-|IkRyPshWd~K zoL`61hM@ys5+L`X^yx4M3IXK}YX|+{F4ztS;grbmYCye*cY(q1n#c&^k0AaC;*TKy z2qBssf}Nbvrx+Yv4EDV9*DS!#R;L*`OpefbM`y z#>|41KznbcKDVMvx26GY97`L=(#Ekhp*^I-bfE09ls$Hz$ZfR$Hu`m3CwLFO6}jC4 z^y+rnbo&&M@r<+alSS@8mUqyOJNCjS@T%QWgTJpwaC?kNS|iOhHyj)>fgEbc`X_fq$nrC|`P1=@aJZYT#$0G+>Y zG~5SIz;5_RNa8u&oWnfWCU5BMg8^umGs*1Jv_@QzEm*h|I16zw%S^1Skr1 zpab-WiSP(K1FynY@SDg(^z9rUkl!4}_uSjzO_6!cMIO!xY48es2tSF;r_J-pdp>#3 z?*s$kZeU!@rydj`j|>IUEI?ihYCteKZSP1J^<~ApN7$02w^G z1>S;_@Vm&u+)x3U0rFdj{1zg=g~)Fqby#>=WKk|C4}>iuY|$a0e-_(N5UN94=nJdh zWq2Pbat=7920pm5#|B1c=A_~m84(! zw#cd)K>Ssl7gwDT9;Ja2KwDO4h^(RPHPxUU^oP4)8SDmhZ4EMATLcBS{!@`>DCe2^A{z=r7g#LvY;i~h+PjhZ zZA70p-VfAq6FR+VDlivrt^nk-nf$kmg5x4v2Lbck)-xi{k@h*#KKHc9wqYXA+b|O@ zi)_CRHi^7&BYY{cgTCKE-|u(=UV!)DTagznC;-)<4bbKnrvUL@+zp?LyktRfXaZ?) z2h4>{@CK02%fxw^I4>U*9`%4{MPBI&Lqv9E1!TI5zT4eLWKS*V0LXvOL_jz8Ad5Z7 zat|`w+Y8nMbMjvLdhc10eMwLf8bEiTz5Axa5+L7w^!L7VBCjR``uu8V7y+|j9qa<+ z_bPE-BhG6}f&PB&XOY(#!><>GI?xFi!>`{Bj{@y@{dM>Z&Wr3vKlhh`Mu2ShkA&|< z-dF(i$(tO%c`q!3=SAM4t#8q;136(V(9Z{!1Lw{I$m+m1B5xzFxAQ|)Xbs5d?eQ=N z)&g~ZoA$j;{_jMAdc9j39v3;-5_-cpcn}yb2Y12|_>q_Q_>cm%;RYbydkbJ29D#Ep zhw?&gxB-yaA@VsyK8IctdA|-E5&3|6eL!D+fV@9=N#rmxJWN{;)7B%nkFvMmS>zby z9J>vW$+2HVKBW&nr9Pit6ghqaF!ny9zMs*zCyE03e4Yfu;jqXTU4V2a$@k>PB446| zUovLD>I!d*oMOD4LJz-2&%T}u=S04tecvWPGm-Cx!3mM?uZ5K&r>ns2B0pgNFh%6Y zLa{NbhaM+#D4i8co;I+%gh7M1MNM3 zC;Oe0ec@U7kiF%_>`_yXOYiV(2{Qei@ZWcEKdn9(`%O3or-Aetv^}FRRD=f59(uws zpdT{m=Zwbz*=Ot&B{P6`JN}Neaq9bD+LS6M(l2C6Pzc*P^oK1>WP6wIeKtUqofI;T9MQ zcfkYj7(4^7z`LTdyHFQsLw4Tkl$~~D$3I7QAg>&E!t;QO{UGsj{@%iO6FZh$;dBHe&`P5nTIm+&?kB6 zqkPCRKmAo86?j8XLE2F8ZBd1gV^>HVbrx zZKBGR2FfXSOjP-sfU?SeBdS6kpnob*euYz_D&~W+K>CUoL{*vqFN>;N5nce~Q)QN@ zs>G{GK2=FqmAeobE2w8p&*b~_3NNB^aaYRPFt!kgv+98 zkWY{j5 zovz;+ZiZnn3DA}LD_|>-raoz|9Rt&WIyRt=4QN*b+SPz|H5d=`;03rSs$qR-54~X& zOat2IXq8j;78fa4!^rPtnQSA4qX2_)3 zL|6(h1Nk-229*GrHAlY9nI~H00Ccj&v+#zfmbASkek~gUaa#T$sug|Q>Mo#etvx6K zHG#fvP2F2>1nS%-FVLZ=t6h&wR;Po-QDj4;&guo z7-!wz1Nx!+Z=$X*0_1o7V3-EWULl&q7^wkZFiyM*ajpTFVb3j==ngVsYsXVlW z<#0&U&7(x!(o0lN^6p9h^~wskd))}gyVvKU*auR*(cRQ{;k>9mRe(Ix3P2Y)E~+ni z^hK8as=_SzSX4T)NJkdw_rgvOOWP+zaG0_N1uW8o(z&{l}pT<2J({Ku>R{+}qOt zy}JDuQRC5*JMsZ_x??a<&Yg7tIo!qgylWCHfZc#hCS(WP6De~d`a0=$QFq@dYBKGb zTneTE`Z$I0KIO2eslDJfQPY?grsW6pdfHq-e$!rn_e4#<78q00-w}0BUf3#X#%N%y z-`fOk0*>#!51xRZMa@M2XO4&aVF_#i#>PzYm`S~6{wV4`fo#CMecyF(15oCD)bD;| zcRzK%pZ>Z36*wpAfr>!*1Jr5OBvG@wiF%O!e~`KC!TRudb}2NfR{xry9F+YT8>^kVM8t`0m$Zw zPerZh3O|W@@?kh5YUNFUPOZEHC~qZsuA)CzbpiZW(Y95{arIC@hU}H7HOOlXdb+k6 z%op_(AK)w5WCEfVQn;Y^)>ybq~QBcmKO;}zyVPk`T+TEcnhe<2GT#904?Dz_*K-#v4HMvM87swf_q^r&@Y?LirS2xZKi#j z>BG&$*?daW7W8Hdb>4#BZ9!&RCWzXa16~vL9RAO}Dr(zapwG6Q6!rY|z?j@l|8K7d z*TED(cH7DO1^Vg*WcLEHdx1XM(Fkzwp#3`-13TUZ>i=SHpbjrp2HNs6{qyoNI0)2t zC;he)9ob2KJ6i)X-#HT&!dlo4#NBxUE{J+13T2@&bOz-0%4Ap$#N8Evj_{(W-Swai zq>9>;ABMw3SPJOV9%R1C-#3W+W+IT!o80EMZJapTlb4PfJ_dw7xlIS*8n>7c0YI! zJ{0v%SNK`fyW@en9i)x#p`VBPih92nyd&y^Dlic42K4R&Wc$GxQHRlw!=H#c;sg47 zg!o79fgSLJs1J+7a3I}>^y!E6>4(3GI*P6ytqauSD0MtaosWJi>LdE;Bjo+jOjrTv z>c_P0<2OWoauXoWPkxGVtc0jz=-07l;1zfezK0A^pOXKlwE0tHbsU)Od!; zKBwvP(~ki4I{m7sAL;{T|EOUxybj-q`Y9{WKK4V@Pt@ZlWc1THQD^WwLw(LH1>*fo z-~W6&AfsQ9LPWyh%7H9 zK?|UrzbC^qQJ0b7D6*d{tpRCHeQ z%3A}PK{x0pI$vR+{rPEo0gekC7G1Cx@RsjFw7*C-=mWotP9g6UWLlIwiVg&1Tom~i z-3cGU&!USZ!gVkPHj6IqLS85f_2H!G5(!WM$h*XyFb|#)&9gT88v5g!kAS>NTCh-b zDf*(+8quW-0cDqN6DJKDofjTeQ!Oo|(X@Hnu5xxr>X>1YB;SeEq!DYN`L<9_Y_j_PIa-w7Drt1#FvOf`8jH$WFci%$tKw)m*ggX9?2{DB)=4pf>KBd zOA%5R{SR`<`9G3Nj=#x;I|)I3iu_xx`sZybmYOG(9P4v zR<5F}Vpi3KF{|k)W_5KIvxYi>SyLUrtfjVN)>bPq>!>+tW2;nF6ELf&L4Ah|?63Ov z9X5QZ>X|+&HBDW|oBfBVx`A0eFe?tbb?7Kndibcm!&J%$4pjcp)I{YPJ#bjMN*X=7 zausD`R+S5w)#OLa>hk62TljJq4X zt%QA5-tKH%H{;TQ>n2?0?fgo5n214a_Ih#N7?__=z*nf&_T=VkiD%1Fh}{gYpB{G31e^};XEz)XJ+Ihk=~ zMwJ;;M39P@wp5bJQcbGk)`9Qf4ujfkZ1r)gt#$0$Cu+Y|V`hzEHM&-vRprYn?^M~w z|JW+stJJKNtCCysor>!#PKLhF6pB?`U*R$wfekPndO(iyJIgOA|6uvL<-RLdKi`FX zQ}YeV*D}xg#QKTRgd6_&A3gmS|2v{<|M1^CS~&7fWW7Jc+vm;j(!Hi`ZRbm;x}9vD zwr;lST6y(l{e@m>oz`=7b)7?9Q1f}8wo#whw;gVla?UT#ug+QLH|Lyl-nrmhbS^o+ zJC~gdS6t<4*K%#wab4GQeK+Dp-2^w$&Eh7xS$Tthb~lHc)6M1Pc9Y#aZeBN^o8K+q z7Iq7|g~-1gUsPzQo9oWHFJDiXspsicdbfT|H>z$cW^DK9Ye9g7M zx6Jh4%W3&Rew3f&jQlLW$ggr%ev@-@UM|Q*xg@{yoOp&}ge$EqWh+Oy%3~a8{`blU zl>CvLV)R~JFQPL|jLP)Wl$S-H(4W)eC-s;5D}73Tt-sOV>hJXT`m|{&vL`QnL?01b zAJv~R3a-x4j5Qm1m+{JZ>3;~}%9oI2r=(}zQxT@ooQrIykW;~{#FOqOMoy=MSJF%U zQ;Y&mIj@2@Ac&FGDe4vXdR`U7yx;b}DW`wlYHKv@cCVIUAcK@Z1|jvvatA}+HBk2~ar`5~qZKYa$$StM=ma&nqp`_A((xoC&gb~n_ z$8dY=DLfQ8Q_qz&bib|)Mf;oZc7`;*5VFF)#=b^21z+vhY&YS}5C`nL?dklEY@xkK zJ`cXjangRteu-av9kdV1<>1R38TLv0Btk!Bf31}LgZ+cD9N+Pk?G$zjD~Inm@b6;ia6z+ z3Qk3*663p)SJ|uL4fFm+GZmHFC5(&U>XF zkp=RoER;pESRRul{2K1@|K_aqPsjK_*IK7QX3NTjZ9{U|V)I&{wb5V(EESB8GQs$$ z5R8wCLI2ha`nL|Vb5?0$=4WZje2~mEEoar( zXffXkGUpr=)?Fr+N<7K@`4ekxl)N5{VHYVjvPT6 z;fn#v#3GIfVU7345W-j^hWV?e()OR7?f!JW`}fXx<}7H=fw?&Q{nZ)q>hSRF7}|y0 zLa{St=J_&|!V+rvy0hPT!+Fzr%Q@h@?Y!f>>l}36a}M$UzVm@|*g4{S=p5z$Bj;o1 z6X%%osdL=<%sJtF?tH=bb-r}Ia!xs4JKs3pI^P9;#^>v}`|rZPcTPJ$I6pd}ea1QC z{OtS`n#i*4KRv6T_``X<`@eN&&wrI(rl3Q`f-!Xs8dS!C5Js9n4uN$w^_v-*Q!q5|Wjf;2+B??@V!~I@6r##LfDb z*CGX-JDfY6i4J=TndO<>{_48rN?gvplvjq|OqF-a^PQI|l#p=M*rF1<7{ha~MBxjMc*m=xZLJvG{`hdJw zI8Qn&omKA3?oRg=cbB`{-Q(_c_qngSueq9A%{W z1R7{0l8OXkvi7ZKxkl||@*)o-O>>pdFrj`zN`f2xE_%?v*>CAr_4B*OkjZ*?p*z*( z4ZUuAx4v7NalGHz=B#xVIy3C@c7EHne&icq`>oB^Vr#NBjO&o=tddq#|Dr$EyYxyu zLpSDHH=8=APN@BAgIb^_s6MKts;cs{*7;5j%685?Q<)Pjx1-z9JeR^%{}nesO^*WWhSX(E`CyC@^VfkveU*_ z5keO~K?!nNf$t6YzLRKN{9wi8!wkPYaTdpoDa+U*wSruh^Trv)YE%Wb@C64`o8I_w z_6Tedqxp&+HTjHkMkO}qnAT;wg?BZW8g)-3AJaOgd)zG&e3K|K8~(J89;+X?g|oED zt%Oq|!JIE>UEC#{!AxG!gk$)d*2P>RZSYqK`|zc8_L;yX(%R(1H_|>%*gy!a3v7{A zCKr28!UBA$RbY#>4Dv8D39UQ>9E!bV{F=o>62-O>`99q_)i>}r->4#m=KE3BaqGBh z8~8Sf`yLZlAB+84`&G>#tZ_VSmpJG(|JHiNzB0KqiifObox+#@ta*4E|LfxZ53-hd z(7KZ~k@0O9_Z`o*Xgb<&{2Ijl(#bnL_Af65Ax8I2J-If~ZOy;LG}CCl@#o8AtQM>D zi(`|I(R<^|J|r0JjA^{_i*nQ_@G-h>vF`eUFMaSOL6ZWEu4{dO5u((IxS!E;B_;7W z5ce@UuJvl(7pMRieQx@8_n==#B32|)G*Z-xMoL9WSqYIckup|d zq(Y>El_gR+Qq@Z0`*F3c?2)>Wx>n9e{YZT)m-&jEl{<1*?zX&Uvx{l>W}~WiyabE)OjorT zL16xZI@@)LyWDQAQ*ecTB?+6Q6U}e7aVd3yG|U-tXK<`-TYZC%O;|_%hs1oX)9Mgo z+w2L{WzR5{awk4ViC2>U#&N%O`0XQBj$nt3*~fjzeLSAWTJ<2IYbncIjcZm?|FimP zG&L>I%f5kL_6zj#wm>7t1sZv0ppg>;jhqo^DtJaSi<=14A$%QCYh_Z z=4@IU*plVx2z+)-|iawd-W)LhQI4R?n-y1PV*l09@2fix!zoz?k)5d z>i*th?=d~VTj4#a2YH*l&3cIWTBaWAec*kdhj~Z5BYL>^k@t}vVZM{8M|v4vh92Xy z<*0A;3EXBEN^|yCQnE zzsz5z*Z5EPtMpp4H=>{6i>uzC=&k+-{s;Ow|3m*Hz0Lo` z|3tswANN1kJ0dBO6umQ2EK*Xx5-A-it@p8`Qck}bsTirGUyoFaRMT&m-4*?2q+X<+ zek*cqp_v$PCV2oz{Z@n(MV@9QpC%^FB^@62`gbEUL6Fu7kE_`DwY7;`rH*zR+g zF7B-2XUQ@4U-vWC)^W|aKxT1WJ)SGUbmA(v#V|T;$>>mGcY?{6=QM`!}P>z z!aD`)D4R1^RoamhXohBXtR85H;A?b66W%)RQzh<`m%XjneI0W>&0YaV%2^Z-P2mn0 zJ9$A`#Z^+JAhn`A6KmF$`eZLDe$tYpl%$j_uVopLWkkYGzF;Z|k0;DexIbZX!nlNC z34IfKBy>z@&RVZ(Lg|FU3AqxY2@*Zas_#VfNc2E-Pjq{9Lv&?yadb{}Msz}SOmtAR zceH!7ZM1Q;cC=!&L^OXiThxtQikxBm9*Z1`?2qh>Y>ljoERQUR%!*8njE{_nq(^R! zbdI!)G~nC0Wtm@+BZ(31pYu=qU$Argj=#^};cxU;`%CIB>o8B&O8(%hF!5sadH{H9_8|4k~dU{>G*4}kqP4-rc zd3n7g&vq}kKe}JKAG-%x!@uNicGtR(yYt=q`RejGzR29yD@p za7XDo_M?ulf3=6z-Ujvr7NZq2oC(etXApY{-JQ1VJJe>6wFEo4*&NrtWS?O#`xxJu z-f!=;x7zFM<@N%5mOa%TZ;!Ck?VIh+c1yc~UEMCrx2}`zL|e1^IBk8wx3k}|_E|fu zjod|AV$HK=T9d4?))1=?E9drH8`rg}T4k+bR(>mo6}7a!pwDn$W9}DK<(^V8onPnDN!r)i-HA^13d}BnnHreg zFfFdFd&ccfaofZ+{wA)mO>ASw;-j0vF}o3QI~Jeag!pkRey4c+Uh(+7;_-V0HL=)_ zh{b1LB1qRO9=}UGewTRsF7fzX;_Xqs0lIcp#bcJb+v|?$Eyh2x)*2paM3)338 z#r%xqLRXm9NH6pY(;5keeqmZ8#h9OwW9SOg8d-*ZVOk^4n4gho=nB&ssfK=GS|i!e zFHCEs8}l>r4P9YcBjeC7OdHDC?2pFM8dof>amCUaS1he@#nKvAtXyQ9nKqQO@e9+2 zayEWp+EC8K%1mo?)YQ-Tg=vkRhJImMqpLB$P|jvAElg`PHVh5Z8m$fe!nC2BsZ(a! zP|n6LOdHDC_=RahIn#>Fv__L-WrT7zp<%h9oM~;my~Y)*vvI}J8dt1bWA&c-inZzyNu7q&N)Gd0Of8_L=Eg=s@M8^2guY8)@uxMJlRSFFy)6-#Se zu{s-9m^PF%t<6ju%GvmZX+t?1zc6hmXXKNaHk7mR3)9lhKwpesn3g)k{X#h-+srzL zayEWp+EC8;WTp+}Z2ZFZhH^H3VcJm6*(}r?3eqq{B&XgKYYg{oo8&@o? zamCteT(Pvq6>Bf`%uE}~+4zNNLpdA2Fl{JjT9%nMl(X>*(}r?3eqq{B&d4G&Z765s z7p4v6Z2ZEsp`4LkX4+8B#xG18%GvmZX+t?aDt~wyLqJttzS#+%3tbT%Nu;!>;NvIm9Y&r)&+@c?)F@cL64|(i8cFf<@+#H+Q#_VqH+TiSHatd8t zGF@S)$uAaaatvKzsL3_vXYvhQu~1URLyapIYFuGxm^1lihMN8|xtO^jma}n%X_0CW zN?1J9xMHEk6^4d6lQJ_j%-Qr(EN9~i)0+M&XDzoDSW~U>Ryt4JbhcVr4Xo;1yQgrc zLF?1(u)m}Cv46FZy8=siqHLxftB2?tbbGGRD|1h*pw6Lvby@wQzEB^lH`N}sS*=sW z$hS53(8{w0NV06M|9|B9sE@h&e@$=Z%74DTUyswn*e~nBPFr*K)_9VXYkeLW=MLKu zb%1=fbDz!V(*0^Gnlwb+th%a}Qd?hQEpdwb8i!a(?9^NJI@S>j^ejD=x_X%fF`|)aoN~ivvxntOXGRmqHl}rhm=h05{d;O!lA9Wx1O18>+S;_M% z^V##5&dS}CZtgBGcAqw-Qxmg>Ssd7%f;yQI5cBW#f0%mSHhZ^HA3Y)o*5U5Q`%OnlKR@kg)32fY$MgFy=I5rbMV7b-P87zxE!+g>F(E`ze(L&L} z(IU~J(PGTj{LM2;Oe|)Y8^g+L7V0>bZp_xr5Y1l8T$ny95zZ2eS~Hn|UH8s>e|qO3=3Do- z|M$2zdj9E+3z%8l(EUHl{XX}fexE?z<@`U`{Yt*O98BD-rP$T`l=AlOx7l5u_KR4l zi&l>|phTvY_VTriXKO@jMr%cDN9$l^vK}*ZI+rSSQs>v_;0sEL5N5^oA}OWduO;8{ z*Yg*wz1E45@6SR%_5~8>MS8Wa_(ORWt2>3H`6nroNy4;cfX+zWBSRQ@@e&{_}tN>hGUV z{cgIyd{u?_pMKGAz4|)t%c^zoTa4>!>5_DZ@u_GG6Xkbrj=%fe zf2I1}f3728{MjS?T-5pEdvov$K9NG_le-hSo8}pQOI&+6SL9r*_a#<=cXY68pgXbb z`R~8++>fKUvM!y+wP{FPa00(93^+hsYfAJTcUn`ram5Zg>{Hf2nzyS>!hZJL+JjhR z%;3%8E#xibt%?pen$gB&?^N$X??LYo?Y zkqcqmzjL*6=W3n1*SW&p$Uxng&sJ)5g@HD~8)j?NXmguTZKs9nw4xtgVO zHFM``rp^^RN$ranJ6GL1S2J|3*yU>Pb?aQQgVLT+hwUnLSDn@&I1ee()&VV%cVy?5 z_D%?HaJaLtacv;Kb-u!K;s2fNk0zed>W4AALhb6H&eibF)yU2j7HRh0u+G(iovQ;n zSNnIa*so$=?Ay6wZ>>GsyK}Wy=W1x@YR}Hq9-XV*J6F4Pu7-53cI{lTkHW%0%do4R zI#)Y(t_F9m26e7>=v-~zxnhs5`Lb>2iXAfc3_CZw+NyK4W#?*(&ei6ftIaxBn|7`? z>0E8xx!S06wPELKgU;3ZovZaaSL=4J=)){lYj>{L%WKcp>|CwUxmvw*wOZ$@-MMOY zuDa}M`Tw^)htt@#&JA=i`a_*Vw-lQ;rvbS%(?d?Jc{46vTc!Jo-Sh7AcaLUxZHCLH ze|P#pR`D(}mertalH}7EiatDr(;36V|5>Pd0Vy z%iG88cpmJv8quPx4_{Y5rQW-~aQGAJy%&XByLH=5S+Di|Tl@o=xjDic&J4xUSahD_ z>@KKz{zOI+6NncU1i?>Sj}Css9Zsy5Mpuw^eNqv;$j_zGGJrT*K2tgDY@H1XsEDuEw1l zT#7q7xEyzEa3yZ#*F{{94=%=?Ogrh~iAK_mHdLOUi#sMb2X{npzPuBhjXNB>k2{#f zxu$j*7OrP8b~z54*28LJnAP0BHk94GgV zqq#mbn1VYoID;qK1V?f`DmV*wOfU&|L~si3QF6KgAyYP_;5go#Avh7Y8~T)=WWn*c zNpKRsjtox2ofJ&Q9UB~rJ1#g1_u$}U+(Uw6a1RQOz#UHi@Q1Rg%@mBmpFzRF{5l~x z1b1>U4tI1w+Z+uG#^O#w)AOUlu@1&(v}jA)RQU*K6Zv`%+{pp0BHz*a5f@rRzLWO! z!+fZ?rF@j}`?#X<_hzwudy)vUJLF6|N@)E4wGt zmgN#W86PZ;J0@5RcSNux?qn>m%GA|xOW99{)`Xi=K79cX09g&J-Oa4=+5=1U|z1b3g+i} zOwbK?L@+1rgkUb7_Xy_WdUBB9j>fuHN_keUM+OU%Q(p%&a6L8NU0v~rm5a3Ssf5#mb)Nv;< zn8k1ZH{9Xq>(YiL{I?0@5a`BM$_-xQx^+`)VQa__x{I}B?`{PbVP9p}G_ zJAB&u=0D@UdYb2x{U>mT`A@nxpK@=ydhb8P^;rL5c}^?!ry5pCp)YlOyf1CUDF0U6 zG5*!KBm7HoNBfr%PU1_;GSa^mcP#eE5+i9%4)$-sJ;c8TcWR2IEt%rqh&vG*PvZCI zHpjov#r<4@kN#0ykM)nm z9p@j7doXr<5*ulu59ewE=TiJHq!WAV1Ndr+e-Q3?e^1MLQJHa1@JK5g}ceKA3?nwTE7;@hM^fb|C)Br%i~z^he-+;qSxq z!~LPS<9ulg4)*uPJ;ay#|EHeB-^%6M7W^{7-x7DSzcubKe;eFM{^q#j{vaXi@MMC& zHttw|P29u%b>$s@E!?T4)pEWLxur1xTE|TaL33=HRNfAJHe;UCq4Y>xSs60dhXL+ zBs2IiPe%HY%U`LHV}0r$IZGWSXMLBmt}c4i&iE}X(IscQ;~wHsJL7+Ozv8~`{fzsX z_ao2$l-J(Zk}KZl{4&A&3U{*iCGJ@73*2$uXSl-|rLWD-`c<*&oTf+h-r%?K-Y2*d zy;pEYc~Ymxcv7cFcv6oic(3D5_Fl#v?Y)FM)_V~bnG#*Xdz$MB-ZS#+A9V@uA3Pi3 zJ>hdoEzTj{)wl+5dJvcJsuuT`|}sgfju)4lL@>%!8;UpvNsNQ zq&Eh4m^T)8oHri#5bqG&s!UxaL0Jtm!!KWww%(cZ?6hkKjCqwsq( z$HP4=wArIL%3B|n^4OPscLJjUC~@d$5A$A8<0drK4Ic&~8z-r#zK*TkLR<+zi* zWpRgjCGI3|IoxsHGPs>K(p!YP3ErZ(lf8xAFMrVSy?Mo7nOWqmdE8rb;STfW!=2>K ziTlSAqXne=XL4_LcW=&!JI?Fj-lXSZ=AZmyPH`0by!Ni`#_rT@+1=2h+AwMm23m z9ws+O6YoEyw00qHOkgcm{E~V-n!n^KzFdGi4vG4kJ{yftVssqtgxc}A!;m@o<^HvN{JDO#A$cj!x@iA%yz;(n7o%fk=ivJZPrO`(*FBh)Th zwDjM!XW~;zxJR(|CwV;`?kL&WKpxa_N3fD2@e!S2GMcdDwaE5J+GI)147lSs+s2RI zBCe9lQWp*(){?Rr$vx^jXN$S|&N$lDcg_~mT%N=62xLTad0zNnLhk8!1p5W#UZjcC z6T_8i|J5A%K0jG4pT1hz#ZxJq`G?RI`1lP5dDsvY_;fr_%CHC@9TeA z;bgxX{mfw;Om$a$7w*0By|_=rPvAZiKZE;P{4d?PYIoNkb8mu!c7U_vP*Ba#ugcO>l1PVP#GXL4^sJd+2K2XTK)e#HGH zVJ#%}Q`XngBxS~xodtxO&ct8p_SkJu;;xdeiaQ`(9e2%?m7sL}bbs7oDQ6|7x23n? z-jNbY`ndb63s0vnaQ#900Xas^U~T(i+$-3L7O@lUs#?ZQv>R(NmiM zrp$10@=9<~a87U<`vHz-zuLH96noc(vJSL8XZmc+X(nCR7WEApSQjlE%p1(X-hgh{ z2L0y$fNjt}k(2BEbNne-*bMcz#k!{LuZn$5&bd-*Wu&VsV#o8E*x1PL-hKKz);5=6 zZF3qnJx8-5Iu47Q{k@^yAZ*XpMDADi$hq1&|Gzp%;}BbOI=g;u{XACKzKrNm>t9Fn za!SH^^{+Uqq!(5Sb$;a+Y#`+HgtO&$EFxOGIi`LnF7HuN+dB5^Uqo~B?DL3Tx&B#1 ze_a1Gq7~uf4*4bE3=jFO&RGilifzSeTzAPkSX!|Aobzhlk9uH>FlT+a`X^W)1e^u- zaWoq|d;OzmR(_d{-6Q{wX5q;!^$(+&c``Gn_Iwb{#MMlkv?1RI>@9gOnvo~nIZNc- zs5@6PaIVNZ5$&P#*LQyS?z_Og^4*)hdn@5Ipe5u@>?I1UHa4imoJ~SZu%O7X<5-{j z#Uw`9R5Y>dSdSf0^Kya-F~iQHfwjlFys?PH5X+0@u>V+xH+r(~g_vTEu`D(rYx86t z&ORZ=SZFMRoyb~zv!KKrn~kNh99fe$=H|o_Qh*)DQdpC$!IQZ-RfLpaTd^b-CTNQF zwI}*ufG!2*|EQ=W551# zM6XkSDPoT)C-J-(`S6|f7b5C${dpQqpEGEFXaDKjj(tw0`Hg4K)_>)_J2} z|BU-o{U_Wf>p$W?QU3w=@%s0;kJZ1!{YU*@&Y|S& z92IPz{>DZpI_tl(h+^l~GM#g)c41Ffb#|3sTZ|S|+E6*o&U2?`O;?kX<$AJiy(%a6 z9nHvUY41no36^oEL9OA=!&{5?@P*pi?o6ol+?i182kl_B+UD*|s4ZzzcdhN{PEp$_ z7|HDG&Tbd{F3u`oH?a$=?e2EL50#YG4&bbc!)n7=={~kLs(yU^_}UoOx=*W(byiu2 zIUAyh>@z>McC53|I?nBsKZD(93)Rk%v$ksIaoX16wF@|LYbpMI`!?~mwTtBJt=c7= zzBRaZ8RsDGT)W(*lcfIEsNDnoi5i39KoX!fJCF?2DK48eWq# zSqf|{dwIRRKG?ubRY~%Vca^ zk3tfT@uqmkVmp02{n&}#N#4ocDcCfe=AG`H;hl-a>e=2o-nsO1=VSMCA-1s>W5;?a zJ>TWnnqG-cbu|{V*J9mzJu5jk(kI^R-QwMf9qsMdyx!^E<=ySwpe&R`n>mo_ac_KFJm?PDn0J&Shl|5z3IJ$b;CRK z!S8wRdmmu$`fu+ePR00yQ!zgCKF2!gORQkO#v1rrEFr$fUg<|p!2B88*k7>`{++7e z`Mw|cpgjtKL@p8E`M(H)1FvZ%;(Sl zf7PC>f=1CFtKETqm)}BvSPeVjHP~;s7TU%-STwJPE%F9f4{zje>~DgF?`GIJZ-IsK zR@fA8gHE!YzrDW$yA%gwx4aXU#=H2t`a`e=-W`kRJ<(eB!v1(4b~5gVjqm~3Ne{zL z~cGf3i zX>u}l&8M`Jt=tNm;IYtf>v zNBh1J%jla~)w>mq?RGTqJJH+jMsvHD-Iw>HYdwfn_rs#YVMYCz|G58z|D^vEw%*TR zqx~GM&-4BZ{)@CfFVp(Giskm}SX{s1zv;jApHF`DKgY`ZORTfM=G>%j{qOwm(d>T2 z_WNi57ynoEyx-~Kyuc5FAPnk3#Hox)kg`8?I`qKlu@mne%oy}QE1Wr)C73msEtnl0 zaZdJ-&K=AX^h9HvFPJ}AAXqS12>o%9V9{W)VDVrHw8^EgGhZfHHdqebvdPZV0t@zD zLGPds+GaoYqOK6E7_5Zuxe6BP{W*1KAev}P+Ec9E*T7DFEn3xe*tfb~uzs)sHt-vv zt#0z4Pxj+nh8=^Qu$SM3c6bPV&hG4d-4jiBuVC+BA6n)8(0dPHKkTqzcyJJw^&`=Q zM+XN7hp)Eo?=(; zGr_aLbHP7@=Yto57lW6AmxEV=SA*Ar*Et{S4Ne()D|kD2haJH01@8wR1Rn>kb-_Tb#3nZsGaS=nzqdpJipC#Q4G9nKT>4Cf8!V;AxQ;ez2p z;liAOu_&i^E*>ty-sGh?!)qB%Qd%x-gv~Gy3wA8`3VVlr!oFd@aQSeBaK&&X_A### zt{V0a2XNkJSJ(>M;cDz|UL#yHT#FN$)(O`Q*9+GVH(<~6M&ZWcCgG;xX5r@H7U7oR zR_uh{CfqjMF5EucAsiGA4tET9Vt@25;jZD3aJO*xaF1}$aA>#}yQcRE_YL<8_YV&U z4-AKe!^4BvOFc3i6^;%M4i5>(gk!^T;dpjf9~vGOP7DtZCxu6Zlfxs!qu6(SOgJSx zHaspoK0F~jF+3?encdi@%88`m8JtLZR(N)JPIxYRw9gMO2rp#){^IbG@Y3+I@N#x; zUm0E%UL9T&UK?H)ULW2N-pGFLo5NeeTf^JJ+rvA;J2}ntZdU#84etx@4<8603?B*~ z4j&00W$*W6;p5>G;gjK0;nU$WoOk*hJHnq2UkG0eUkYCiUkP93e682nC;mqGX82b4 zcKA;CZunmKe)xf$gc^PnejI)hej0wpiKt(MU$UqC>+qZK+wi;a`|yYGM{Es#W~ceD z;cwyZ^;+Gl`}Lq6*6a1C9&>tXTF>g!)w|WFH+#Jva?&NEi&+_K%cVqt^x z`EpL^E3xhCUGKwg_kQ)|?KF;+IWK5c^rQjxfq$}&UyGgb>u^rfdiC|O2iy?-X=5zw zH${WmyuL+!%lcOJt?S!3OZ)AywIB3{6ZwX)w|@8f9`!xzL+g9h_pa~5sc8GLH5ntk}3v{4_6E;e3IJgiS-cmAaM5%tOSBkM;wJH#pVV;L77FQ+bx zH6qp;r*cN!e?0TAehKG~T~@zb&L68^Rlk~D|JP#Uaee)U`i=FQ>Nj)7-K}!Ar`W@D zuFu`|d+PVr@2lU>2>}n*AF4lGf8;-&dpYf?m;aM9FF&<&EWdOoSAO&V@d?-SxU)Ow z`x|@uCH{8jK*{L2x7$C^?{9Vi42-(i1<+Gh)6>y2oH_Aa^v~$|=mk!E zdMSE2dL??5lPF&29K|=HH>0pW=h)!|31G7JVFj5`7wd7JbgC6<lN(@t*O}c&~WxcpuL6*e~8cJ|I3Y9u^Oe4~j>`BeBF9%{f_z#AD*I@wj+=JRv@m z^FJnXg8ZcThiSbGC$?+-ishkyZx}4@D=V`^~#OKE6 zai-P<@rCh4@x}2a@ul%)@#XOq@s*q_a&>%7d~JMPd_5;V-5B4*nL@Y3x5l@{x5szH zcQR{wcYF`CruT6I*8}l`oL~QN{7C$0{EzrCP9}LGelmV4ewvxoXXEGOBx}wqd69FX zUXEYkoT%60*W-W1Z*YpqTk+fRJIt=W7r!5W!2IgJIossp_!CZ%`YisOnbt4kui~#c z@#I^XZ;gM5e~f=(&h-~&Tz`vyPdFwh@sog=*E(lSG4qB`YVZB&#OeC$zLUyJ|H~2wfvt zGg&KHoAbKYWtNwdtdb3rjbx@b*_0ElHcz(TM60bhOLUuLTV{K=Pj=vpm%+)7oN%=> z=UeTX43RUwIr(MJWN5NivUjpivM*;{?VlXLtnjd8I47Kq;MA*8$>`)@&b=DLsaNBY z@yUeb(B!aWB362nI6G!?a%6H;a&&S`G9@`SIW9S#6J<_JPD)NrPDxHpPD@Tt&PdMW zoSCzeb2usMJkH6wAh|HPD7l!^XD&@HOD<2YNUlt-O0MSYtZO-w=KACY&d<7u)3a_# zZcT1WZf8FFPG+O;PVQkg`o83TnT_WBnun7|l1Gz&$oX2y6P&O06sOuelRTR|CnvIT z(iSJOancs2lyfTE>zv8<24`)(mAsw2!wER=CGRI6Bp)XKPCiOL#^Uf(&c*pW`6Br; z`6~H3`6l@``7ZgM({g@HeoB63#{1Xgx8(Pftwb{KorY;WjnY_Vztb$8j`{ED(;3q4 z>5OR)X2EApXGv$}yq($8Inp`Pxzf4QdD5QgyqpI&f4V@rV7gGcaJopkXu23DeJvs9 ze5FfsCLAYyVg1-lb58pz(_U$BIU9~Mzn163uN67(Yh{^JPx~{cJ}~V{TWLF8jnjSB zV0L}2bnSGVblr44=GZsj%%6?YjnhriP1DWN&C@NgsoaW_fVN4uO}9(8Pj^TMrGwKQ z)15dUXqR-?bV#~ex_i1ux@S5x-HTI$_DT0m_e=Lr4@eJWu70?jFvf{=qd1T5U{0eO zBWJs(<2hmI(Dbl$VtP0yjUACrPLE8F;@qKQ(kbb&>2aJpc0zh$dQy5arxBgX`D3T2 zXQXGQXQgMS=cMOyM$!4{1?h$9Md`)qCF!NY3bMWxAganZD*OE1zDKYvnY#MbV;);n=b2? zO`pw>b>~#39@$LU%-Jm2tl4bY?AaXIoSfA(cQ#MfGn+S?FPlGGAX_k7h!dO^$rjBP z%NEa;$d=5O%9hTS;asQXvPRa-@~p_ptXI}M>%(bJ{j%k=6|xnxm9mwyRkBsH{+t0d zFzd=%Svy-TTRmGNTQggWlcCnh*3H(-*3UM`Hq184HqJKTyr|8x&9g1CEwin%t+Q>i zZL{sN?Xw-SLD}GJ$84u;=WLg3*K7!9OYNTRk?olc&GyRn&i2Xn&GzHOsRObDvtilr z?4WE!HZmKPjpiJxL$Wd1*lb)jKAVsonjMx+?C9*6Y)W=4XI34b zosgZFos^xNosylJotB->NmgfOXJuz+=Va$*=Vj+-7i1T5zSYIqCE2C?b&|`oE3zxI ztFo)H0=_o8F1tRvA-gfVDZ4qlCA*cgux`)p$nMPU%I?nY$?nbW%kJldtOv7)vWK%r zvPZLjWRGQ!XHRf$)>GNj*)!R**>l-Hv*)uHvKKi`>*ef~?A7eG?Dgzl*&ErL*;|~k z^-lI~_FndW_CfYx_V4VY>|;*e`jo$&@_F_}_GR`}e8A8ljYip29#|R|#tq}@_uTG# zIc~Q53%6Pgeco*7`_0b1`qv!TrSEsOlw0~JbHwM_f z`rmH2^fcQI4X52`>+b{X`vLa-0QY?(FU+3-&L6T{(_eN~tnq92weS1d_kHcVz7~F8 z3%{?0-`B$LTZQj_CtVs&W1yz9*|PF!>+fZkm2+wNqWMq0TmCfkyS!96X!g~7Dx2z0 z(NDS2`Mr;%ztI?|+_dkTnxBoP<|C}}Z`t#_(0DdA{~JZyr5kzF_%${Cd}rlp<P|K+?(Bf11)3kUrEgzfuU9)NBs`=H( ztNhBf{95`Q@lm;I>37^~dA6FC4<;`yl@sK(dT!|&X!1MI%1P^Ivt{L)mliLT1Ik^~ zS5$Ij;kKJ5PZoZ=Y4T+8Xg5usG+&x+%a^jL{*_iw%ZAB8mxkM@!q<94`dqy8W>Lw9 z$wSNXv7gD4hTCZB`;BI<^`Mzsd9^HEZIeg+oo2!P4m*Ef_dD$33A^uL7cT6|19s_$ zUH-#XKT0bvEjQw!@ys<|R2$28ea^k}AJ+13>V2a$xi7Vy;$G#d)bkAMpDrG7m0$W^v+}2~a##PU zNBVwV)pL`_metQ@zbYR!UWK&}g|+|f!qQz>x~q0q+nHv&aN)z2?!wYtSh~CH`!4&w z%kr(u^4IEl*^w{jAM)hJ4o2+cf#rc7o?7zuI1KZ}O}4mwOi< z*u@8S@qsOTEqCrM{HBFp)tlVHS9zwpw(wQ1xwr6Dp1HU1wfwlZ@UtspZ^g zR{e;TYfHnyzbYK}9ryN~={vc;Pd2*ukav}fmdQn{wDwo~NrF}RF0}s8Uuk?wYtI^` z%6+3t+sS6tKU%-jEVceO+ZHeF=b9~TN8##um2M5MY581Q|5K{Gp+~rKAy{oUOKWG# zs$TS|>Wlef?L?#0@@Z7G4V8byOzUN%OZ{zhX}jGpy`fp67$gTaztv$#qc~Jk!5BGkvrT)X#9&39;KWM+J9@T7Vd|@X?uuDH| z`D*pK)#c*FbNgQP(PpcoCuuwjZ4VnorFU8Rw6$GqwzXVfC*QF8+cr6BYdIq)8lSf5 zH>LG^W!2AFIHlGL%G2^s(@Xbea@fn#)l18@w0^&7{Y=r{$z8K;`QGTIa@a6`n)*G> zjn%_mD)+hRqq(+EIzQCGKxyeJLSE2P%zatzi|E7M|X!ce) z$t!(G@0+dOR<6A*eZ8&RdTabj?cb40SAUuUaTvEgf3#8-?X(qnFl;X8)?bXnkv_9MSAsJE-zRJT$&d4VQaMzlKY*ZsGQ^ z_*%VcTK#KwS^J>Z6pG#m~kyEv;8{51JpP zjZX?KXWrNRZVphnYIIpUX6;&2(@S?&gSJ#0R?%O}x2W{z{#K7Q z+-AF1b??IE-pK*=OViPAx%hFf`Q2{0_Zbgce5-oU$I{V9<)zt2{cH8JeADq1a;@^5 zn;zO6VDVGECm(42YNNN+>z4I18Xn_cjbCek)sq3*9<((77+;t?n|{{N_Kxq|bJ*fl z)wBMZzLw?-?ROQf)w?dO7rE*Sd~flwcr~hawZG;^Zu!zQf0|maNw>>C)F+EyyUItC zqfWTykM%dD>5rxLJ7v{RRqbj^-*2{6Zc43Z=r2y5VfP$%as#{cP<*ajV3!`)$rJ4S zgI&B}7hl-bFWBWf?BoM>`3Jl7!J4jy>LJ`)_}cDpZ{e%n!EoHdSN(%~3t#Ie_ZGhD z58PY$+D~zB;hX-@sM<-buZ?EkDnA`F{;$~NLG@gQRrX$`KUsawt$l2?tUa*zn>yY? z{WW>F`I44BFKk?1R`X8QZZ}m=Mjh4uDX-cYYbSEmWAWGI+~#Fk9sljQ`onlshEyT-UOBrIyYkQJ<~84zP3&bn=2;WaX&y9^7mB zlq#=?jkfcp=?7(}-|6bM-QGLwyZ^E~Z#RfPA?`A*QFX>PXl&UavZYEuBiBX(A<;l` z>*SkNW!A`(87`8Hfvl0xK?q|O6P~sSo&Da>&Xhr>W@4$0F_Cwbv5{-MVEf)CKXNO) zyz11g^KNP*Mfg?-8Xx?(%+lxFJO5xO3^YzEjHON9HF{g2YU6+qSmtZvf&cDzzPHM4 zgP+`#gGQyCwcPJ~ubpbs235_@ccwg4{@P?=Zj~sngkL8cC}e9~RQaS+wDQnS3n6y# zqEXQB%1Vi>Dw#HJl%q9PHb}^Il7K;$<{Pt~PA&?oRHZ4A&1y2Ow^e$beBe7PAL}ez znr{roEZ@!Fyi)2arO5_eP3s(T^EdCU`Ig&zxeXed+GrA}JDAO9RhBfjHlaq!jV~wWCXVc`XX^l|R2Ai~MPQDnO zInHgcn`@)r$gQ5_T2HvQe5>TD8pNyr&6fHPyL_UaIDcWwUsWCm&VE-VfqQEQY%-{& z$~e#MdsTi=qFs7n_dSz+8lOTNW%_zmp4&E=(6;hy+vIy&%ay^SDm!f}hqfuVrIl~q zN9$i{gOsvrH*L^Wnv&VDK}M;I0+ff9tI93;p!Kn|`qHT6&j$T@B}bLgpmK!#T6t9M zi-nh)Qk$D{n(H8e_pM#422oWzV@f@vM=f9NJc*adn| zo z^@EiXq3tK(oBV1$ru#MdwaMv*$yvh&W6i3+)5SFA&Nbe-EwVJKMH`z8YpC+h;MeN0 zwR25tH<~7I&8pq1CY5Y5s##4!*&w@VgX`RstGrT@^n2oO`P;|ptI8$MUHH^X`(5h` z_az0 zt0t$cpDC^WmYOfj8ESnkD`naS&86utrRfEww)+f%HU15qj9|{%l=@2lvi2v}K`@ge z)^BTl=H9g*#JiGDOHZZe+2U=(7L^NITq9iy3LEUU zO@51NQPuR&w$=mIa$G!Nm9Mr7Y?lD(5@COs!0c1yl&ed zysi2x?`!$BtNN&edd7~b2NyQzEvkBC=`XE)EiHe`&SbS~4;fpUUR&w6HfSkz@{F-# zrLVYr=Y6furRf!=^~a^@@uf{#l+~h`$!BSLcWLEc>Y^u;K3d+T4H8RREGjE~-zJGm z>+egOEG|v|Ep1V`G(DiSN#xS>fwIa6E9cS{YfCHdQWsm9^Vjk(%_yO?cDFR6goep= zqiu3j<$qOPW~|WGNea|sS1z#1Yugsp+cw$J)Q3 zwk^uGZQRzj#o4xv-`ciF+qQ9B+a@X7Hrdv;_M~l-Zf$KhP=ihX()NXW%U@gUY}+Dy z+XmNdTa0h(WCDw@t{liuYfp5r&K!`9muwtbRExH@*wHqZBiE8!27BkwWm$q%PxvhF)vu%^uZJR`F zo1WdaNz1lPYPU@fE^Jb#u*vko7WE1nmln2I)V4*3w&`hYTYPAnzSg!yh_>l%ZJYmS zoBr0e$?&%JC$!I&e`ciA*8YX(CQqh+x6Np&ZIi2Q?I#&7SblUCiA?UaUuJ$o^S!kB zhEnB(xj=hv{a|VRR#`2gnx0siURT;Uywu6bMydJ+{#ZL~%XeSQybTmm3~#_tBprWo2)Nwd{b8QB&J`LrgxOKI8oZ-Q)%;G zrL~u(PRcRO;^cxkS<{28e{7hMNW=1_QB6u}d&IPkD-YP|lT1pwba3zV2H3SPJa_F4 z_bwdntM~1_YEo14k@uWD@}83~?p?djy?w9o=e~OG+6nHR{sFsmz*YP#UfQ4HU-iDq zrOjXP-1JkMthY&ZYH6E?TsnvK`#|_xN zX@5Y)cH>`GD}`AbQF-l@hsm)m3gkAq4{QHo`O0@%?q+0$jg|iE=?WW%}H@yZ-d($<|O`mCh=`-ywWy8H@NmdbQRO_tP zlr&8BHxXt1&$7f+ypD*gD#3D;iK?m;+?$B1ihyQfqOL<4%uHRlB;7<;t9st6e}7Bm z)EMJg(MgzuH0@0}%}qYdF9bNvFXglz4Wi>3fmNhp`-ImReFstb*!DOpR zR$z}TZm|?()l8dJ?oAvk_0Og?3avgw&YCsrt~zTqo#w+bN2kXy zCD!JyS`MhHNwqv-HcTujX*1L?HK}3UZq=;jRdwsrQW+_xmK}*|O)W@5oc5+y=Z4p& zDPkfp%`f@X3L{aO=9jKM)7-SDy&2H#{K87{w6(O4Mn@}|88_Ix8Kvaf9oP!Q^mZ0- zO!%jk8itath~a+G0ZmkR?E`FKi->B)Gs8{pwGG1%%e~&)NHvQ_HFKmr2Qx_SdCPhf zxGMUpy6Q-?>fy{BGbiedqh^1<`UAItB`TtGE6es}qR6}^)q*rgYC`3t-F!PZ3UNRNA!XEXEX-r}Jn zEbguRtCdJy`QW*W2WA87=ND{K{-YN3=Y*e9)O2?kyj5#+Z92 z2e9RXuGnyI;a4;5IwA2zScYLEqoghR~mWKR$MHfsdvs_CaASMZDoM_>bc6L ztsL;&#fyH_%wL-i^_b& zX66brD=o~ds4%mN!e#~wo3SiRBQ9(vvM_bBuo=U`W-trW$O{{(6{gn|Hc~075sdbv zeKEey>wsewwhWES-*3|#%mhgKJDK6n%>&_EU#&-%ov~7 zHP+24G;6J!s^Iay{jRl{dyA#^4t#H^&|ZN~-27LiTw*;{-687kW;%`k)E`L1)EiPV z^@ik3y&+BRM!%ot2U0ifTT@3ngg=d1M`EVFhT<^whGe-Lr=j$*0ZJd8Q)kN5)p%I* zvX{2J+-qL+vaxlu1!i_GtucfC#0dTGmvW}}kOt9ou@_+B=4A7BHc0Tz#5TH{$l z)*iN(&Q-EeK_#NMjy+LL?Ox}`FtJey?PJPxZP194@$i-?VcHQbv zp<`d{HLH6~Utv99)AGaS1b9!=X*H93d*AZG=Jawil!Q%|Oml16+=#7}5kIYYO>NQh zruGQ1*5F>c)|C%XgMLg8tOvFCYz>1Ya*cb}Q!z(qsQo}1XJRPr) z3e{(tW;SXTQ0zXm+%U6I+lIn(%T=8T;@)yq$3@(0m*1@X*O^nETdwJ}3HMH#U<+Tz zLzIe>BG|$=3o6r>cy8hAj6e4lzK(;qxA4t^%JeCoTllIsac|+<2)1dK8dkelsdnKo z%j3SoBFDuC3q%(`M&qvZF@M$HO}csC)iS1A^u5x?tJWi7p`p*4Ha^32!0A6t(|1bk zIGNaUtvoAc7QX2Zxmk9UW?4|!%5`Zof@QT$z*ds1T=BC4TZrp+i@ zEoP!n-z#l8t<-TH&o%zJg_qm5-n?3QvlY;$Sq_zEfl*YJ7-n%<+RQ@J^vtrdRI*8f z(#HEu?d_O`a^+09m|j@#3G#H7Ag+6sPI<&(COtk@2mVLPX^4&HaK{VKJ# z)l_w_)igCS*G7){WG()pv%u{d85XoPTT4^tN}Cudb#kLon)+9odRdyfS=ufpYb4Ry zHM^~|VwOzBD6baIv|8|7!_94>C9kT9sjInG7iK3-e6-PJ)=smpunF##k2HQjjg4xu$i;M7S;=`4h%b7b`x&(o%yHJj^wBF7q;qIsikJXVVxPptLZLn z*k`+Oh@Vw+&2KsZO;2uC`?*!CLX{Qrv)9an_u6yM9d;Nzc&DNJ?>l6dUH7-q3E!Fs z*&<6(?ToQmz(N%t;%a%V$|-TMrl&EW`{0B29kRzBLk91^!;n4jcZSaWkipd;sH|*y zsv2%ze_Pqc z6t?!2o6T+6u?@52+D7`OwOn~MQ=xhyEl8CuDq{M6opgOB*@oW}A`Q+G11nBBmm%meWMU)~IvU+o(b+qPeZb z6_pK!#M3O5C$uhY608Y zo1zjPTi9t<2A8I^R=W`LYQaklbX#Vai%`1os0%L9JXfKu4ES!Q`G{irjvF`yW7GR zj%`puCRg3PepgiOr^T~rLewuR@@H5R$ad0x$s+3ujcGR(I6 z(bPs5|1~|iExtlcXr zLp(FgF08#Steq}wI8m6PKvC(}RsUdyl!dhug_Uz*VSyTp=R-UEh zQ)w%~r5V7NW_Vs!yN685cuxnpo*RQ~? zU4UKw!>+tx*RH{?Uc*jlf?c}`yY?G)^%ZvQ4s7A8-rMXl!@Vvu-0QN9C|x>X%I_Aw z8T55&e)HVI*Zk(*!q@hcdkbHcP3|px?bn$5vh-KWnANhRDnI;g>DTs-drQ9=(s$V| z=q@v;@3LLcU1nI{WxLC}%)q`&m1+F3^lN*;+?(aU8N_v&L0p#^#C540@t&ne<%N4o zkCki7PP1v*DK;%Lyl$zSq3i1RE&IM@-?!}hw$=lhTP^>#>NDJHdAGIS;aNB=+h<K zMl4*FGY(O3`GkI{zuPW+?lpa;PjfhhrqfP6<6hHgyQR6;_*lQrz3E%F4BWJ3;6A38 zcWHU{@2zs#zmMJb)8!}ZSzUOr%U9Ti2fKWQU3jp|SJ;IIyY#~@ov>?%V3)tJ%NN+C z7dE}YPKU{D!$4_<$z?TJuHmAOxNsPBx$m(nbN-4j zW?Dz}pVIV=Qsox0RR3)9o9Ue{_vc9DZ;J63t2USimg|MN<1gWGAMlssi8E^|j!I72#*_B8q5hcu_^Y0WYSg?*T6kmZ;V0 zE5J)C>Q}-`De5P{ODpOl;bj!{4d7)J{Ee4dZ8=4K9azH1@l*b~d;xlKJrmp;^y7Lq zczH$r5Lm(n^>Ofuiu%#;N{ae%@XCt%G4LviI{wyHRn!N<{SERLZw46TnfxYL14u~- zHU=%jc5vG;30}=`6ui3OTv)yrJP6iQcquIT4m?Tk+6r$DSn>jRQawrSn_m3-X9Hbr0^!g5?A1z3U8wDE`&E#coLt@6y787<_hmAcngL1JiMjC`xM?v zQI|Xx`2cl^%QlL-1=B9L_KuLy(>Pz2*)DKijU01s0Hm%zgn!L{%~ir^-A zgd%tX9;v8HxKe*WU1UVc9@IsC4_5HEl4`Yc6p_>gkws9Kx^bJs@FQ9%3d>y!+>-XRr6p_frjf!9*EO`g&(jMNdsLS(P6p@sP_zUXq z!V*_dza2KzkK+0cMYJ(|ry`O#i@zY+1io8Q?+f1p?gMpjzafMlFa+>}h6H}duoC>R zVJ-L(Aac4rcpN+l{sEpc$ahaG;>qwc3U6unS@2KlehYqH!Cwom@t0;D_wYL=f-R@fLZH4c_?V?O;BOT{3V){vBtG9O>LuQ^A=M0 z;^)E&X&3qH(hmOFFy$(cHqKj2;hzOBuBbf$FQM=+hnG}Hy_No3;9mzXt&n=`Eu-+S zhnH1Io#roHJMh;}YhFVk^_#zp?ciSm=ZaX;RVe)X;L;%F-%H^?1ot*b+4fQR66d~( zWKFoALCRryh5stNf5~{}s5uLCR!+!k2bx zph5DZOW{+Wr2TS`ylE?Z>Xx^fLGo#JML^Mbk_LgKbuESe6TG%T-d#r#NItA9 z5zGj$ZxFvWPy{{T4Gjmt8z} z1fyUH8^j{d5*`Rfz!HW)o{PUAI27K^a0R@(B9OG~VYm|BQxQm7h8nJd_fiCsmc0#E z!}}-#Nz1;5YvBD9!4!CZ!=td24G1J|2O3_2hbe;F;o*jt;e!mH!ICx*JPS*{2tEgr z7a({J9&PvnK3EZm3`^Mq{VF$>Z>0 z3a8&sOzR05FJ4hl}@*0Sa=}G>A_!3z1 z6vPtFv5K1bEo}`5zk-if)a1Jp6v5B%i3-(N-UG3ezr^D<;z$y=$< zAUGC&-f$f(c@2W&;1>;24lgN!*A@PF_+N@zclZs3 zpTch{YBR!bDg18m+X|WI@ZM4QJ>hp1H7Vcs6uy+ZloO~)d_GY4lEx1W;{U%DzU1Xc zhPC036~WB#Cx&(4PZhx|@MntnDEM#keFf`yF6@dr;q^L%letI>ay!1+|&rz(Cpgp+e>%{JLQ;78*A`L7F4uLx#^XHbYP<4Zh1FdIy{x-kcNE@KHnus$sD2GSSF7)%gs0Fy^9 zpKgN1Ul5!Gi(f$65*f=0f|KDn6w+t-b1DK!<6H`9zx}xt!Dx6MfSe}J!{oa_<{kX` z4Clb}E0PysWXNGNctNlb-%{uNg%u(%{vwLtDR@yuP13ZOLGp5Oh0Fu_OBnWnmsE(} z=Sz8mfVBBbD?|tMmr(?g)@2o<5Bkd~0!ep6A-bX8R0NU-xkB0lzfc5sz@>pU!|!E4 zPW|4BU>MxTAn*4D{Ybm`v%I1vaalo8lX$LZSPfoDA>$r@Wrg&6GS?;uc7j(`1W&{L z!2tY!4IZedwcsv=_W&&SAo&;ER@4rHB|O2-V0DEXbFRU);4ZMHA`t)9QUnqX?TZ_; z9s#eT2*fXuZ{WQNucruZg{7PXQWlbC5KA4A@&NG*u#|y-^!OVqlKtRK3=*eJfyk8n zzPTb1KetdM7rp`>YBMJRr5XSfsI-Y^;7L6L|Y4N}BY;K7RU zMtDa>{1Pl>BRCyMp6&up0lONmf`=%=J>cCG@$vBP2FVBcO(1^mX?PJHsz@Y#dx5>d zS70B*m+-!ZUtq~Q!H-~nMSKQ)fFcl?Jx~#!35)E4M8X`dNJhdUDk+M5fk@SQQQzUc4 z6BU8P@o+^TX_}-61()GsMNkTl`*6iES}Z;(16Wg{2_B##6;0Er(+o`f$} zBsasCC<2k4OAS&ElBXb91ilbX~8g zzXD6yfJD+EbrK|t!8a*liLb;D)Zc(_QPd^hZUwjTT;%6=Mf?kVhoZhae5WG*3BF5_ zh`ijbNJOUYQ6##DKcPsZyri5!vOFx|fmq@zaRTwT@H2||8(888lB4106p7T8 ze=1VR!{-&r%e$BKiHkB9T1)K#@qkeyB(!zyGaB zq)a{npW=`B@tGp2!=EdX&EYS=@4UlzK}`{jfIUS-y$F0oG#2J1K{Or?711ade+9`f zI8sCh!?7Zo04IuQG@L3T@;}HF(V_5kiU`@3IdnlZ4xV0-Yy!`qNH&JME25F`jEb1N zlX)~jbO=0?BK{So>;>_6@GOe>XLwdc{5?FIB3S{RT@l|0&jIEnjn~0*DPm+cm|GDY z2G65Nj)8kBk{jT86$x!aumD&Pe^!DQQbd#Cg~1}k`6zf%MRX**m?AnHUR)7PgqKhx zo5D*f;&))`l^}i+M&<_FQEUk))+iB-F8>R3!Jq zy%aI^MEYhyOuY&ED3UATzKY}uxSt}Cu$EWEZ^M$;Ah{Y|QIT8%OCEt#;=i&Yc>rET zksJuGsz}7|{)+fscz_}ic^;@p)`PngNes6XiEvwy{0Oh6NTmE%2Yi?O1h21%pMf_3 z+YvV(-d>U11Mi@S-+(0__v4)&@c!Tc{Mi+jcmtW&7X4e0iY!Q6K`QleBpA)L$c5A; zP?xe3nFg}HBI`qfXct)O4~Qfjkws8j5T2;09}XL8QfI|4P`d^`LQ#{lJ5u583?HqK zIRKd(b$Az+@&n80`PH)n&jE>-~{jrI8jlPG@Yc7akh->1T`u5Qw*=crz&bM z!KW!?Ehji#A+{L7846h&3C>h_A|q!RUWdKfQAZ)=`DZ-6mNk0gO!cra} zl=8V&5iSNxyK^JY`@=VZo4MW)z6IRM_3rR(ibV4Nc14KX$T(1tNIu-DNJM7tQiLK? zcN><5?*aGX=NR}t@F3TR!Vf7zDU*i{#7Xqd;8EU@GWmxh+zWn85sG|0Zde9>LJ>}e zpHzfWZciyvN$=B&_*M8BMIvdHHc0aR2k^Wi6uEdokw{uz1juqKc`oGvVksZ-3nY@q zuPI{cPav`ak{Mx<4-kvYicElHZTL+^d@(HL4HA)^FBHid@Ry3@aQG{Qj3I-s6`~sj z-za2VBKQ`Nj&v#bM@4NT_$P(81pKoiT@L<55lWnY1;6ucgM6<659cu0H!KVXidf2q zmmKDW>k64W4qWPLB3K@n5tVRwW0N1h65 z5>NRaNIJrq6t&ghnHBN<@GJ^RTR5wthU|s286>R|CxN7O4u#hTo>L)fjNx1c$%DBK zlGb??vUVEwRMc*VC0~HIH#{F$5E*y?UI<8DN`5S&h$%-|gLja;SRU*?a(E412doF)0qX<8_s75+DrDRlQa1#NT)BJBeoWq1QVS|Mw^;V}x)VZ$j3 zX{W-cY0x z|2GW_!Xh_Z+Z9~MbZVotMFEU-%}(5;P(~YKJW($8T*DGDm-aZMeaee8~l;N z+aDJB0LkvKgpFLJ^6h5|r&~*0fz!pm;2Mao9g2Ja(al3iClI|m{8}M;diaeZ9s_@? zh{f;k!1shDY5YMEOI`UacE5!GPb_U8!KeFY3MvpdOuT zz{wWf@579Fm;$Q-&K0zp;FL{=P zcwGg0{^LPK6W9cBuL3ilBiB+Iy3!XRy z_9}S10zGH&)Kg#-CqaRplX&VYusPrj6zDmG=K=)=KlfayK+h694Hei1@QW1ad4nfW zfm7XItUzbqo<<6s+TjufEerfo1x{^pnF5`QdoEX?cUV1FDA3uor?CR3y1Y_B+Y6qg zz^PuZQqXe1n<#LqV9~omYA& z4}ebxC%XWhU3$nCz+VEV^nl0!r+yCbAozm{VmNqT1-=~oAq6@w^z>8UZ-YOqKxc=Z z{tEQYnTOg2pmRje00nvn%`;Ge&J;a^6!f4_WdT32h?T&oo{-mod7z6^ibOX^q!E1 z+5^z&zNrlWjp~DZ3vj62$e#du$J0YT1UQ4h$!`F54*1Io+8N+86j)90nF?A}@L3A1 z7C4m^&@^yrV}R8Lr*;K29h}B@fSn6Y?F48B_&f!69{6htnh8#A53uvWsl5S>#xZJ3 zfYkw~_5=7I;8ZUFs|)_70{;_ykpe9+Jk)*w&j(+kz`WpZDbPD*o}~)R2fj>!p8#L3 zz#asDTY=u6@T^c^eZf~M(EAjgRSN7O@OKn6s{hprjQZcZ3iO_ZXN>}T82mj2df&pc zR)O^ge_w&#zwoS6U{r<=6tqXd*DEk8!v+OyDELMNwjKOK1$rLtq5cN29pKc*0D4C5 zp?(GM_Tbc)06}%NS%KdH{;7hXy2?`E9l$?Rpyzd-EeiZj@U03W3VfRa?+8w10z@=8 zl>^|Nz;`N$81P*RyfgUc3L+LfTY+~0->pE;06kwQ@O1Dm6+}Gv9tGYNe6IpM8}#HT z@NVF_3iO=Nlc&J%0^g@V&kQ~L6*#r+0R?)N;yI|mshz)4pyw){LkgVQ{A&ez#^U)# zfv*AoRzU>8zf<7k3*Rfyvlq`{1-=&i2L&+${D=a*+w1vJfu7HJjw*1fkDnCid4=a^ z1x|JIi-Mps98=&_ProYA^9;{#3Y_ZfcLjRB;W-Yt7(?KzUItj$!=T>0HgF~s(csm9 zbD+Nkyf#o5`bWWi0NRAkAiZbx<3Uof?rMd&0G2j^hxZVhSEbt`i>Jsow;92N9gFgr0{@5z;AOIV5w&W$e zfil3SybFOP&?kW}1(rcZy?d7fE0DGU_(}zacJh)ffL#Ut4georr@*s-EwD*-wpGD- z8XRMWcPI2zPrHEKu(=QXO9hAO6FyCFrhxBNaHwu_fIOsq2At9W&Qx&nA@~BFk9x^( zzJg5g4*}?_oH+U^fm6QU13w_`3*bkApAnzx^%(FgDe(Kje^=lSfFD;7WaAG7 zL1j6iAjrm_3J&>dzJh}>!G~M*oq;_#%I~YDV59APXDQg%fuF5lv;nWKV6+CW0i1*V zep~RG3J&u1)l#rO1FsF7i#TXU-+2nggW%^Y*zj9l9l(P)eZaj6HvHU&!ukBr&jXKA zFm4BrRxmn%$0*ojGgiSE3Ld9mz}J283U)I1MG7X`-IoYljC=0{jy~YK6nXUpzYMqn za)0o~3KrVZcO{Sn{dn-J6ztL9O%x2WakYX$aj#LZ9|os71?;WhR1UyG+xw_~0Si9j zYpP)F1ixOvxC6YIf{C%wcY}gKwwo&$Wba0x1V3F0M7u%pJBi-&TIubr}Mq4K<9tHISPgYK39Rx{Cx8i3=8}<1??_ys&|0S z6n(EN(0QS60q_R$(!m!hIB$W!sX*t8zC{WK178fFKJ-7qDIQ=@y)6Znp{|H8SD^Ds z-`fgwe(77GK7f>8;a{0}gyfbRyrKwf0~OJEQB0@W49 zM}kAPk0>~>?MJ!&sC%;;IO?5Xq8dzmo;N*eFDF~A50}YTb{J`H(!9<(;F-8y= zeA<7Nf`NMW-=<)80Y@JsIH*5=YXy^d2L-bo_?-$S-AhLWlXRUFOp4oC!E6ssb^#N8 z*iZEW*w2GgeF1h5yqkhO9sDi@dkT1W1$#31-3s;#;5`)Vso?i0*yxM?o(lGJ;P)!n z)4+Qv*ptBTQ?OqG@2y}z4Sv6Z{VaGN1^XHB2NdiV!7nv zsU7+$*a_edE7<5y{{9N~Jn%;p>>A($6l}V`feJRY(I5r8Ciq|l`%~~C0DQz*4W6lB z&H<;o0L&HObkBhKIyl`YU{X1$P63nb(|rOa`S}z8?PD$ip9`RjCi%lc;7!PXf-hDu zPk=8`FkSGs6wE)sDR1Os{tQla1ekR1TNF&Pw@txB|Ml+y_M#7J;QN6Ckm0WWuN2I0 z!M{;3zr(9;h(|Eif}{Ttj1A!Ee*}8(C+bWEV;%Te3dZ~3XDb-E=cw}(jP>9i1>-&N z1`5Up;4Ksk)JqinmtdeCqu`%WcSFv^mO&_(hrv-6g82h@Z3Xjt@K^=&NANfx9&vsJ zM_vT;7X<_>VQH9_jp=CRc7 zq=!EdOynI4A0n7r!Ph95pMj&U3FdZiv=c$=+v~h#80hG`D61IJT62Z zCO986))0P6Fp?OX{gQ%%dwdn)1PAvxx1oZAy2fovn0eqDa0V35f>#C_K>iy10^nwx z)%d_~QJ~*g=RbUQev{???Dd#oF zXt$jC0Pe#;d*qx0G}b)sIMF9Oz?aKhuYLta3&T0c^>9G51A`C)4|~b1SbeC6fD@tMZFNLcfsL{1e;{| zEWvibEdbp58XR>;u(pGrpd&6_ys{jeIJHj5X8rf9eZEFe1x%l)Jgt@@T)9vSHU^L z_>UJ!Fyl?|MBpZr=X&s763kc*exC$0mNEWYnglcFfwz)i<{Nl5H6LXeg@Rga6dTH0 z@b|SJG*@`UjUrW~i7uk6ct|`VhKcdwaq+Uq)nC+I`MxuJ@xCs;2Ymy5!+hg>6MfTs@A~%m z4*8DwxnKBAzvHj&ujP;S$N3xh8~PjhukzpKPxZI<5B87sKjVMiKhOWB|1JMYf0loX zf17`=|A7B1|2I)ulo@qq)Y(zzMR}qcM74-YkGeZrL>tkyqvN9+L}x^giJlPsX-srX z&zMhRzKNX?J1=%|?6TN*V&9A15c^T=r?CfPe~81p8P_DPYuw#&!T9R&wc^{wcaI+% z|3m!o24;im4eDGl6nwAb8k+soQ#|)Ig4}N z$vKd7Ft<)_{oE^Zug>k9+x@U{?EU-;^IaD$%zP}!Z{{CpM+AONBU+#pT8ebhP4p9k z#3;1FL@`qw(Srr8a3fkF6|K+?tyEJx1?3&nhv6~88!6?xR8EA!H&6FtIt#Dt?(>br?EXi4&b1>(pTu*L;+{U@r z`!ic{#@y$ReR60tV~0inj{zfriw-qB6#wn8ZwE4VAR+hs16SqVec-a(aR-ufXYMSA_WtJkG2%iqn@0Zqs~J0pS&2Re9s)31 zsn4OUhdw^g?Z8XWP5r7l>GD<`7<6FZ-VXZ*A2|C^p99bwYKqW02WlL+4LoW8?)`iA z?>`U&jebDew+6?qZvpf7&D=M2U(0=$?7Lv!xBI@?w;j5V_67Fcx34GipOE){UaP#u zxk-C5_l7^Y&fdPc=j80q*_ZQb&ZwN>Ialmmv3J?t@q0(^otcYKfA{{~AMO77^WobE z?EYkD)Ljj`ewzMrT%PY4--=m$FH)1eK#@-j(J9Zq|j^lAgoR_3Cmz=S% z(vOVuQChOd)KRnMHVd;EeXMo_tE1HeJYY?+&vtsNkV-KaUNP=VV4M$qNPntN0f0?m7!HeMe0k+RnU56f2yDkt8@&7S3xQ1QzcMkZGp6{ z${r=5=ASYyHtsiW(GQy)jYOljah1`;c-@$63^rOCLyX&us>Yc{HREifx>3WZY1A@m z8|NCsj1k5}W0LWtahY*F)7W+FCf0^^VdK~n>}B==cX(A^n@95oJel|BkMf~>0)L*r z!av{}`KNq4|K7OTNHeZA<{NX(9{e}$TCKU(O6#sYrj6GoYp-hWYaeM_wIkZE!V*`B z>%E{u&M#Vj#LyeXE7PmJq~6r-sz)tqEpqW_@(XxyqFF;a~+W=~_GvBG%Vm}9gu z-Z1Ys-Zq{y4j2c`?qZCw+8AR#Z0t8y8c&#Q%vNSwqcJm>iFr|Hg4yh9b_+{qU0FAF z7u&=>Vjr{h)&zb5zkxUBH}a#p@;cWd{t9$Ig9k9I%nsr6x*+7s+4Z6ceb zJ;|QdGTBV+B{o-^&OXszW1F=_>{D$q%hKLrpJ{WfN!nUoNn6j))IPCq=I3g=`FYwG z{Cw?8>v=7k`?PKRLM@*+)LecM{%G>$+Hu}OoXKwz)%eZgEdH>#k`EKN@eGl|hl^DH zxah&35clwjq9=b++{>qm!Tbd=#ClB(;;)Iv__*N(dx-t0JrsX`sk4@DJ;$T8P1Xzi zT4A!P8e?0urTi9gHh)t*#TSW5e6e_%FA>> z2UssHz_x13*fwoBPZ8(vR8f7ye42RF-fr(;v$UYSQ@h>X zWq;0p7N4@}S|xU$_8?CawRlTWo3|3@^48)!o-I~spKE`JN3>OTw!ND*WG%HDxu4Gz zcZ)3h3;RoU17C-~QZi8M!OzfEi)P|_K3?3&bHv+PZEKsgLu+b3q}`;o(c0Rz?OFD# z`i*)E{bv0p?Hz4`eW8`9zh&*xUeKP?p4S%Wy{)IU9ojB!r`sGZP$x2B29 ztrzY4?ZKjj{j#`8+#zlkw}_jqS=K9}yM3O0qg~fNUknue#aL&Lb3nWz=828g67iYX zW&7+XJJD`zUuwDLY&%D9Zarn^Y9DA@tf``@eX)Is)ZD+57A` zZJPF?k!3eAwiwy=3_ISMZR|C2je*7>>x6Nib=-c!o?u^QUtwigTdb|tr)DqnA-lC* z&z@^HaQ53>?IdfB^^5hhJ<)#L*=tv~GwkkWA2VP+X!bYnGw-&?*(2=>jBUnlBgekn zUT<%-H`yQB8|(x2LHjHFkZsuQ%|Ye>bGSLu9AZCW4mC%akD3|gW9C3}gx%I|V-B|V zo5QR>tz*_7*01(e)>?a!z1aH19Bn^oFR|aWuePr-Czwx|6V1oXvE~@Nowd)}Y<*>p zGsjywW~SZEeA3=xzi(w*pWCn7^X*J4&wj(6<9ubWv-{guTJPC|>{Q#bZ?J#2YuRh8 zy>?IgUVgO`?R@RTII&KgF^*R@?=kz~pm8i4!WOd6d7jvhU*Pyz3z3b}#=-m;djr2R z+{J6LCcGZMMtsTd7JK+$v6s){xgv*e6S+8{&*uB|dio7|8@-P{PLJ2G)0^to>&^65 zdTaeYy|>*$A8Oxi-=hx`7w8%KaQ!iTv_3{3t4|eg>C>>=ct9U7F4QOJ)17#0x4m5a zCVm&k^%v}6PCX~Vsc+4*7h0q2YSuIMt=6mdO#3E#td^#2e>2k{Y!n1{)N6z-;c#v zhBjP#*1kwTBm&wR?LF;7Jy*}urr5VQmxy)x4(lbmkM*&hV~w`Yv6eaCS<9X8t+$+S ztfkJk*0Xk+HParhTjDZ3TL0dfY&W+Sh$Jg$KVZFV2do)(KdhNWPm^n*Ir-?83y4)b&L_jO%g$7|>xu-EjD+3Wfz zdR_f1-K&3XjkC|S=G$*sZ#X&DLMNAR(YNW9^>6g5`Y!!U{d2vVHe2`U-&(`0@9lxs zO6N!2(AQdH^bKsDzM0L}KedKiKk#^cllB9F|N+gjxu zwN^Mk=u!H2Jjrgxud|fLu#@CmWi56N ziF|zp->PqQnmAV*n~aZ*PmHa`c4L?Eh4H0v!Zb|VbXX0%#okoAo?WjsW6iW1Sell~ zZrAQ$?X?c<4((3XLF>rw)H<==T3>d*_7Ll%^M}Gpv~iFX`k}5wJcs;`;6Do_VBveUhdIyxL3>NG1|{OR{Mp= zX~+1*LgS5u;Fk!UUn&gVMx4*viaNZVsLO8`9{z~9h7SA{e@wLIqeUA&MzrNm ziTn5@(VIUl?&p(5AO4JZfWIh)^66q2e@SHUpcu}Vi|6>;;(5M8Oyw)Z3w)KB#@CD4 ze1mwEZxZwQN8)w*Gk-+Z~_4zyEMZQ{0=kJP__!<%9?}?ZBS}}vaFJ|&D z#Jl`Uv4-yv@A182tuxpe;ymgMb%r?^`o+$0y`?ik@8pcs2RNg|rOspeWM{PgsxwBv zR!?RP*ahrDb`iVE8S9L5#yb<7$MqiiJ$g_5Ui@xxBu-A=W-Hh_=LzRY=P7-lK1d(z zJnc+&rZ~^(&*)S1XPv3~2z{jfoc=sN#J}d>@NfAL=LKh)^P;|3U&8O_4~Xx@K5@)> z$$8nn*1pbYW3)Ef8tt5Y_GiYuMla(*`!S=B@qiJqM;Y^s*NoeZ4)%k_P~%ZM*?ijU zWOg?C8V?!$jQ++W#sKSpG2WP9k2V?^SK1HTTdnV`gVs*t4l}{H#z;0UH?A-)b!Hmx z8h08Uomuu6=M`tR^Qtq)nd{7RUNatc<~y%D3!FEch0dGKB6G5_&-l#v(Ku@SWPEQN zHg=eG%(|w>tZkla{A~PYYUUZv65|ge-*8Q4a#NV5Y1tlw8^Sn4KVg`kKZEHU0P7CU)nJM#|nPP2pA-n`v@-VWN+?J4$T`vrTd{gnNjJ#{ra#bOxzWCD8L%scX!`DtnvL_``*W{ zk!d(f#J-5qH)RfXM;cN$LVWs=O`^m(LO7nm8>|{~p)1yKhvHXu2d~XLAm_$q)GCCt>eY-jmv%Yr6H4*2tP^&D1Wn zUbPl$msxLFtF)%p`_{+W&GudPU0NG^pgj<)|=J?yR`OLXMdq}u=m(|v^yQI zIq^=s)){M@1g#6!F&Ap-Siv;Xx;j@ljkWHW-;%T*nAMtS_h2j=to6ib z_NaC*#TE8r#m8a?KAu0zGqCRt@`>0DZRb)eI|B_X;^n=(Lv;H?(^Z-(mmKQXIzWw3j%h_tqc9N@ln|3Tejb&+9Q*kuK3M$NFxC zo`Ut=dwNT(jo#O9*FVrV>g}=i*`{~Is_t{W6V`Nl^mMH1_UT=*mO7|+)4#=O#9cT) z*Yutk19iQ(afVSvzu)X`cGm-D53`5;?wv{mdEWZ2e)&w*2}a z>w2rBJ{fDN;raq=q%~51(;9=d)FNw~HBMh_O|YKSmylm#4L8}Etgo=1v!2&il6PVy zH`ki0ueM&Z=IifTZ&(ZU_pHU%V*P#VZEKaj4nF$6z7eb2kMs|*v)iU;S>Ibf=$~N^ z@~6JdKG!~1-;Vv3U*CaUaDN9U+f*{GV3 zlF){+qBN>KY3NrBMfD|P{{Nbj)d1ciUy2bWL>GqSxczb8-~_h>^{k?i*|U~@*AN+s zc9=tHBPso6p{PcM7SS&V#YyOpkPge+=$8dbE)Vs#^1>59ac1sq<;jt<()`o(){5ul zIY_@R@K2Y&4*1QJ4-2}t5@Rac%wk;c^iUW>P3rcf`&v(_15y$}7=e}O;lWR%{9^_Dw)5B=q&C5moZZXuW`c z5K0p8x04jlYzj3>k2)!)QF>5+H+nl%h2jbtP{c2kl_rw*3&mHd;^X4sV`Wo+28H?` zKUtiUTlnWfRjE`7RWAO~;c|w{?4RU+4mC2}KMQo8eCKnie--4l{*9oUcO33&~~Gk3T<3r`oQEQ3bn@N)d83si_&EsS~mmRWGU`V%Lj`l2pm%rI$t} zAyslzbBxam;?N)C7DU||mX)SrG^!QFueZEj*6GoxUf8R#LfTo%?IScQw0j9nWU7Bo zRwYrTDqfQQw1x?-8?nnQkfSDJhsg@*SSe48&=iIyY)ROKR41kNM%06adLwAZ8j_VJ zl437gEEGH2BATd0*y9#xqE<((LwPnuWr6O9`T{gB>QE?DSySWN5dJFCb}}U zb#&!ulcuRotDV_2XzI9CIFp z7Na$aY2soKQ^k*@F^ysxhx9Sm#B?K>en}7+rz}~m!(*Do+>E^Bn!JFf#k7N_BUy{- zL)MCfM#hYg)jGU>v-+@B8ja~!R3B>Pm`Si7(c%U2pqLEok!LASh{dX)SR9O9#WaOJ zqxAK)`ZjXZ5@?i@G-i6t<0LD6%yT4D?jeoR{}-q#l`3K6U672snDuv-nd&EIRm@t@ zc`*wM>P zv4@bVDE*>@revIxXkzu)N_h7#Dz;7`trlCeP>$4}O0I`lYI*Ffk~R#{*v_%tVeQh` zB&kW36#icrQ^o(+Xl!TNS)){~B&D)-j%{Bk2V(m}4$)qP^2FH5kSE3tkyOcwj*yzM zCG-eQ*ponGZvlq(kOqS~g--p-1Bz$(ZvXH;&&7I$i3oiEjpZVf<3io8!}9L9bUy#%u>o$M|lbJ?R{^z)lW@ zDj6aDD1Q8RkOswPfaa4;Wuaa*Da+jJ)s%9b2u;I!mxk=bkB)!5uxyj!pMxD$wpsC1 zT3I&avMOX5y0ySYc^BK_vkUDME!#ngFUxi`{x`(0XxWmeOt6tGDMn!Rv`CH8s@0SR zaazeZinb@2#^Hn>Bq5YW;HFaQUauFf15)l^ZwTbbQ?i2d9C>;(vEF1EYpSF{NoPws zpJ>4hvPjB>o}V3TCzE`lzj>5#`Z*m@igRMTvkr7@rCp$#NzXG#uh%~V)Vfa6ODV11nDhp< zH8(a$OG(O1f?h>8xBi?~P9E)**6k@``1kB5tyQm#dMeILnsD#g_MNzFj~&meBr zAsIi_gG_%f2J}yECMKTi~IA`%@WW&9=z@-lgMNs?A4ji^bo(U4># zQQq%vlJz}O{#DAyWgO~F-0(>~`4ZPVlPpG%EXc38=uR5`xXkf6Nf!~-yUX}JWZKK6 z+?ZsO#v*REm9imaOUknT&19)*N*Y79l+jJfqe*X!C0U;YgB(V(OPm`En`O zrBGwGr0+_)h?&qVlA#W1#1E35AZjL3j;8c0<5`joSrRLiWGj_YSx2RORLU~GrQ&av z@i&v+SSsbEB#V{O{#?=s)uW{^iAza?H)@D}Lt~McB;yZ|^f9S>SnAb>pi;OS5Q5#Ac{)N;$AVcr_b170iOtR5EpDgqy z%Y3jbc^`_&2g_76Z*sh6O!Og1Wt;MhKgU3GzoIg2;-4#^87fPd_9xy36c0%K6Vm4O z(oP0h&=1KR6J=<&)Mrb}thWUek_z9eJ5By+!C z%IRi4(%vsaTT7bmN{tQ5$N#w*p&!ZkvIY5YsaZ=Jqo0&7AsKI^QyJu(qKzawT7Rh@ zO)?)&q1r%c$0uV}mGlf5Cp|wCagLMB58}U4a!rmi{9q->qK_<<^e(YjTIeb@gCy-E zHNB+#h@{fT#3NFFrPN$0^SY8W`s<|8gOaY0bT)YY>G~dW_elF=wnd)~b@0av^Di5C`0y@AZD0crGW@ZK!fnr zVid{RDyfm9qV}WItRebGN22&W4=DbZBxY?}_8svdY3RQZ5hWSF+90}`G}a`FX}u{m zvfo+LX~uM}mYNB8r<7YyNqHN^H_wyuCP~*x3p1pQH6CJ)k+Sp<^Qe@kN&Re@%Lh`k zlDCrf_+7(jnK_jgu%P;PfX?se2GbyJN&loTrA_GxT(&QNob(0;exfqYEz&}ojCqUHL`gY{^n3)#T>8J>LWa_x zwBRB|%Ad>ob)s}qJr9NJmDwMh$yj{(crE1l=WxJNW+tp0l zRr1@eN4uJl-|n^B)lAJv&FR*!Q~OB$Ddov2qfBPr(MmqjizR&j6P-B znyFJm-!=!^cv?4X-LQ>EeMxG6ut&=6L+{>oZ%V;;SC5Td=d@c}%*MgqSyJg?A zu4nU}&F@-J);D!dY2TL3N`99{k#0`ok|8adb*WTLTeaZ2YD@V|J5llrKZRD&lA)2B zRxRk8UPtk=dO*9H_l+2o316%n_JP*D=zHgCDn*y>sw}6XFEvAq=OSaJ*GZc}-bL4iU2c64nD&&TM-T9$Vd=r^mn?b2QG5S_pD&EBawA)gz$q;30< zTpoV=t?HY0qWzrKx3-_teopGrqF>Ub&FHz2c#-b~;j6S6NR63+KvaD@cEfkcxP_Ca zx0;_iqWkF75u<~*A5H1mtzWl(DZ6@gqtO$sKjN+ht=6aPYFCr$4CC^yRxR4qRJBTT z5WM7Q>#=m5x~}5il|OkC~62Abw!kV0DQC= zELBBF)%J5z*4}ZfLz9-6@6vY3`F?HciPXcbXSX_@RVS6TJd%1Mtx{^Gl*cEGPp$Dt z{*VFPs;1RWi%VIXc4?|7wVwPoN^No%T4{K-)_JY-S~df%(P4CI^VH_V+sFA5GH?4Drl<6t6TGeSYqg9=hS!v(=-QSdXZPumCE6r-F zI+5S@z33`sVQL@HrL7u8-bd(;XdVuaWYnYE)!aETcZlrUSb5Q`+qnbHDXAkS+1>il zoJXrcnisqEqq|L6)vX`NZ9FvYP&-pv}vKjhmXK28w-7%ZU)yC~dZ$H|yU~CQX zb~V+E1n=62aSA@tss*h3S~j9S-Le_ea<J3ab2HsTNRd%gThS{ws8YC}5YJ!cW~ytV?!vIFB)6rz z!OeC9ZqQv>Jggwb?$Y#b4y>h>7V=khZn~lgs^y^rCVn3Ptp} zA+L%HsnD*6;ZyW%hPmHHXnzXH?k_S-(vYrH-b}^|kG>j zm0TZ2&sSXCeTe#UcwBa0Crh$-7n%%@JMKai(_O)t91VhnkqU)yN%J#9-ju(vFu0h& zWiKtJJpIdbX+qIJspis|3N1D8uPO82!)QDbwV9E>E5e5|+}Y?^M=)w)u0Hukj=*Up zs7xe3p7$oo=Nl+pFn{h&=ZLwAJ}NG6yX{;EkDH^ zg|cN9l(aUZvwsHP-AcUp2f3@-tIcYM!rb%%Nu~^GVIz$thOd-VtfcA1!^79*AJIxL zDJ*4#t({UzFBwDit#B=ujPYO5gh!Wuy+rB%dX5npy0g`2s#Na%5kXRfPp^R=Vm0ID z6b~!w+WlUIxGUVkxvs3Zk@}LW7k5QuILc6Jk3yV`qVZ08m0mLR@AXqRt?1lTN`A+2 zFAIQgN`{rKDO_We9i@UH1#9FoQ>j@MBUS}J?jfwg0u@YDEP5!t8a=|Rc8V4X3nwYq z8K{vptS*;SIQo_=cF9O?c7d*XDPd)KtcsdmGQ~fmap`GtiBH8_wuFg)Nl~)JBQmCE zsQ-hHd$44t?rSA=k(wf7Tt;NbKQEVmJy4=7|5{1uET`nz19Uw~>-8ndSuRCrJ>t?^ zNPqc5RzfEzSeN2TecaK|2O+0}|Dbe{v~Z{wF$ZJp;`hQqW>$iwc!JY%VlTRYFrzwW~^fb-L zr{O3{Do@`J7Ekn7uQ8@xqS9iOv{8(?-v#(RASuc!a`+*1vT%x)v@=1S>9kqrIpCPHd%Nkco%d@OiJoaJ(3akY~cU0^QC8RDFcUz+F z&^O$tU3$O1@FVxgY2kCo!hqt!5o-OyRX{{&*ek;KlD{S*LIp*)1J)%^V2?M|?GTx? zprk?4h4$)^;bECdSy~&ETGFg=x{6AwJx{f3KAF|OlJ+m9*+iv4y+)2wc&$>6S?=a= z84Kj|!Wxpeue6q@kZ_xaM}hLMp-POB0kT=-lSxgye!x*g{weg%#x) zMKAw5SMCR)vV=#B()>9Tt76=Jj^gA0S!IROm3$VRpXF|ryP5&v77oWJsbrc^I6aa4 zzw>c-|6eKV$woc*tKxUzepN6#Wzx)#ryFbB??X=_ipLFIhy5+2Azplb3x$Pyd`NS8 zD->nrbY{8y#BfO>CHX`xBT^HV|9XDm`6O(uyi!?d^D|K2!P2dur0J!Hl&!7US+=y} zE8E)tG$g$scR7CjPfPMIXI_>+{MBNd++JBTeR*;#$2zh+W(wk{)A8~oa2pnl<<8IF z7uMuQJRQaff5`fpaE#NHPF|zsweHeyeTsch-i%_Za$n+!&N__9NRM-qOqR?pRc(tW zTxrlGD?=x1@BE-ln?^As%aK10*YZgZ|08T$0U{&6Rl%rbIpC>eTkjof7%A-`7R2*nP+&qKOXJ@GhIpK$3nd=>n- zS*J>%ydpoVtd$%_-XkcxG;^{sR^?OFiW-|~^ez6Wh%XkkS=`deuk+I>yyUrPKF;5Z z-%Y5Tr<32wOLOv2chAYg%L#YfoxYB^Q8BF52j6x98# zwD$|jk+75~_$9?(`h?%jEt=fjTlD(BclGzx-9LYCCp)kF=W{Q24yC;RR-UxQMy+s? zjIele39Ewi;xX{+fi%+nuV(tPdPebb7G75?lqxK^`zxH}lnElwdQKVne-)uNYk?Nd z_+MpTcE0X!WrtND@I8@s}E*hpc~z6EFPTVitg=7ILAOBqx#{f5UPg=r?%(g`06WL*-_I zi@!x}#^DSF7AQW_rc0W^G~-@3+316Pi6&f@jvN+3LT>sPSfkK1gr-Sd213^`%`#b% zW#PY!Ym_@I!ir^T)htV^MpC}dvhscSD}LkI9Bl*6ZAf<&bXUo=cOvbbNLwA2sl=39 z8tx#OLfst8!m|O5&1C_EW-<+>uVLPbznq~VPKN#~>5SWCzL`wWouE7nHkrCY814e~ zA;S#Zg)tH37kq}B#%C7fDG-bFnZ_KHK*;+YimO_51)CY76Y63jo?!@Mu$yfR0Ukwf zqL@L%+9>au!h#43B21-Qq2;i2(G9grRsuz?GQwB5_YAssSV=|>BuOhcFBURvqFW6m zOJ;iLPH~TkxW`0jDE45KbqMe%YD1^;;yyD_-gILDT0_@fMBb#OI-ZNxCp(4r3r&#e z`t@$6{upfB3-pn8rn7WPr7g!j;QpD8yUoz6xLZ;4+16|LZKlp&a1&9JiKxj$x0i+5t198%EUl|4W1z#)B1|siP zX(MZ_u=S0|lF)s~(iow-kS!XjkI1;PrnC=GZ-%Vx!RQY|fJXuHBO|iSky~4|LtCk3 zI-Vht=fWcbMr|rD{5C_+VCni8+)Z^i-MkupX~2zoAzhZil=Yx| zsv6xba_o&9doz>&=5CkwMZS5PJFn1xLm>fqk3~XI_B6c}TKYa<3`%(!O0WPWH4Dlf zl=qC^M;Z?TG&-1KuN%~BJ)v4^HE3hktPFaT-3x$_@C<4X+~q@ za~u5G(&j0zDro;?ct(IqV7!M?So#Om@>yiiU>zv8+oqzKo?iLJvtVyx)+&7rkGbZDi@(FKZ-S+XlX! z1;kj4VIzSF=yNu;FlsRf9}l1wsePs!X{>g^NR}vL1Wq2q*yrvu4!SwUSMJlsA@_UZ zYg~WBVvTRz1IBmQ;n}9wJ!nR``^;!}x*6mCV8-G)jzyXA?jf@tP9vRyb}RKhFis!A zIDG`MXv}yMSOhEvmI4uDh*1kOmc!Pw9JT?{hZvXMk}+s(%+PY>XoA*`s5O^7w0n7wuVf|YfI18ZNM~s_c{^(|4rq8hE0~;`|ki|?`%!I{ESj>dQ zOjyi>#Y|YtgvCr)l&cn4%!I{ESj>dQOjyi>Ma)OQd|*RC8G~}|^8&`WX+ZHc(96)x z0A>QSFr!rl&H`!xF>VkRX*Cjr#ULyOsf_tY_*CEpU>fiuFdcXa2m&tyGk}@Eto$Q- zW#BBJ1`v~f#QZV;2+f(+d|(6S&Kj8cGW6ca?|z^U@Bk119t8RV4*~svhk^dUBftP) zATS6(kJGWv)v2TK4t46Fyl(&t(e^b_uK|=d0RIfYn*#8r0Q@rm{|vxC1MtrPyeR;0 z3c#BJp*>&*_Rf_# zWMQpB>lK9Z4ZtVB<^qefE}M^a%Ob2_76Weq%b2pb9&%2>svh$cjpBm#!bgCkz#npc z5B%j^)GF|w&Lv&ePPud8AE3{z2Qbd^jlhS%Cg3CBW8f2Dvm3;C$>^j*j|Q4z%u564 zbgdV-Iu{6H9ZTbOvW@Y^o(y1=v0p$RK3A@UaN4Hbjq?&4aDY(&PH+THDa115uo-d| z@ENefeM0O6@OSmFVojDOCbzjhtd<|bI7Roq5B+KJwu%7rVFeq^mw`Rr(DvEN3ph`lZJ7SC+6p1J)D6RE$#U; zY0q!Z!Y=td?qE$^2{;3&3}BzeKVY@tbG6}fwfRQiLtqo|5%4ka39y;frg2~V!D{Q# z*cH>xIN~(Seu~w$Cj(P~7jWWP(R&K~<$F3ma@A9~;tBPKg7uLx554U*0DZ?;0jva8 z0q?MYc_quh>OKRj`wXn^Gt8@jYk+HkWZ*iWDUgPp^!XwOtB0l-nKR%|W6;7a-K|C| zpffNA7!SXC9Kh+PF&Wp-0M7zbftP_9z%1ZZU=D!&lKHy3)m#9)0jvW)0M_G;m^zXE zC+q)oeQ+>JrcSwnrTSu>zc><=zxUpNJ?M?VO>!?9V0BB(-7Vcm3u{#EFnblN$zaKv z%s&>n`z)(Q3wKGLQgRI-*Y#+}^#%LdPRMmV+HXDDZ@pZ%zYIPDm<7BF%*p@8z$ay{5ay{B|J=$_T+HyVGa{Wnj9*LZ@k#lxI&H?0{jhwTQb2f6Gg8R)z&e_O0 z8#!kq=Rc8i5IF}6a?V7~*~mE?IcFp1Y~-AcoU;pa_M!J@qW5Q__h+K_XQKCKp7yvC zF$$b={Gk#AQGy^!5JU-rC_%6SB`AHAi1g`Gjew_Hg3`y{ij=@F&nBM7YGEqy0x%7D z5tt6V1Ox%B3iu3QCNPU-6+63-=Lh(oSXug6Kn);=_4@}1O zGXTxM&w^tuf!>>h-kXHpn}pt*R4^}RpvNYm$0ni2CZWeBp~ohn$0o7Z$Z@~Sb;l=I zZ_wJ~Qy{ed*aCSgunpJ_(E5BQP}+KAFxDeO09uy}!(GwZWH>MaD7Hqial*r5<=ik{ zo>XA>Qm`@&ovh%=Ax;nj8fMv2&T8n?O|4vup4muG)*{bsFe~G%A|TIi-UG)O4$dtC zSThH3ZV|w_MF8g(0i1pWW}5y2Lub}`tW|BwcHOBo%6Fx&iR=43-%0U&GoGQDc5hI77hCy^^nq9H&nNg zts9iPwBoHxen~A{boUl0^h30EiQQ;4maRL%jy$U&I+epv*IXP(7Dkw`KL50Q@%Gb3*+-)Ptz+t6o%METr>?(E16bPea|Oq3+XA z_i3fneTCLjsQWZ_aj~%_Jz{K0Kjqj`bY%SNWB4kJjB1q=C>S9FSZC0i9Vc7qglF*L zcTwIEGID$`Yvq_;uyQQER?DDgsKsXa$dzMgt#+~zQ_b|DQ`k!i*0<>eYqqjZV^4Q| zqRvYG$|^Iw&XaG7Ma-`yXRXlP1WvgE4b48`Fmb?Tf`)n`6{zr$YG2Oq*m@F{!-Uncq!OYU!C$pPkkD?>W44$Z`tn-dF6#+G$l zSFkJ_GeD0zwenN#F+J+g}F1mULnk#g}JjZcNXT(!rU2B2&-WY5XI%r zqC~!i*RQV z?#!&&lY%jYl@{Dtm^%xbS)vB~-^|RHhFJruqvuiXEXiJj&_tqiCsqf}i0RI0(POZ*T~Tp#Owte2#ugIw1KwJ4%$Np=m^=+2|7a; z(Ngp*_gZcf5n?&aY{Lpz3Hfjb#9$SyhBdGj4^OW{Io!kfpZ8NMvtBYCYC?0s?j=SP zAx0D-M#S?J5ix)`cbI1wm4g%S${{cmj)M>!55wRD7!D&~B#eR_7!48n1~Vn{I%cz( zPnXQlOJ?XLGxU-fddUpEWQJa{u2AY3`a5AA+y$)0k*pk$_W-l>l39Am`Xb4EhP)p( z!Y0@Z55R-S7xN;j0dVbdFY7wCkHWM14?U_u!q)4UL4pDoeBcM>Qv~xVg83A|e2QQ` zMO1}qP#w}C16ZFaYCEyx!U~a@&2+WT-i(oO_2vK0}&AAzF z0p{SGrLYW^!>w=|5KD8orh_awtSxX@pX#ta)rr9>SPg5SY-a8sw62d}(N)Zr9ECM4 zj;F1n@=8X65h+^FW3KNaNR7?vb=p57nLokL@CzJ-U*R`61jSGSaYzu$7hr)64oFbo zf)D%#LM9dK+_OMGi$=JQKHx= zQEZebwn@}KG~xNrg_{zSu~8;tqfEv|nH<=g@B;f-Q!@@N&$}cbL4gZC@B_0}X!#gg zK8BW$q2*&}`50P0hL(?^1PUDQ(d@`F)W}EqI^rO*@sk7!&c*fH~&z>)4|DVsA$B2V!{aJ|q zEJS}6qCX4KpM~hpLiA@L`m+%IS&05DM1K~dKMT>Hh3L;h^k*UZvk?7Ri2f`@e-@%Y z3(=p2=+8p*XCeBt5dB$*{wzd)7NS23(VvCr&qDNPA^Nir{aJ|qEJS}6qCX4KpM~hp zLiA@L`m+%IS&05DM1K~dKMT>Hh3L;h^k*UZvk?7Ri2f`@e-{2D`V&j#$g2g;6vy-J z1H<417!D&~B#eR_7!6}!EG&Y>a3e(FCb$`HfhDjMmcjDGZmT~GfPpXw2Ez~-3dcbR zxVFW$Ev{{GZHsGLT-)N>7T318w#BtAu5FzNhupTzRPTJ`#ES)gDB8L^hF-go#K8%c_C5%;pHc)7Q()B=k z)yO(5)OBhCc=3(uBACMpp36b6(dM@lv&rci?W|?;`##P6+zS zI%XT!G1It?nZ|X@8LU&QVJ+MRcf)#C1N}&>@F(~geu0DVEBpqBpcqOZ4hdG<;tSGO zBJ`CAeI-I)iO^Rf^pyyGB|=|`&{rb#l?Z($LSKo{S0eP42z@0&Uy0CHBJ`CAeI-I) ziO^Rf^pyyGB|=|`&{rb#l?Z($LSKo{S0eP42z@0&Uy0CHBJ`CAeI-I)iO^Rf^c6j8 zx{g`XbN0H9Gk9`m zV56Raje3T2JuHM9U=b{a8zBle!Od_BEPo?XUt?LO$F9F<1qwVGU>- z^~koxKQkg+G21i;>AM`R03;OYN{tNv(`KM}3dyMQEJoBQBPwquf&v$Oz*`-Rs8L4L zC?jf=5jDz)8f8R{GNMKqQKO8gQASi&$pbU-jHppY)F>lrlo2(`h#F-?jWVJ}8BwE* zs8L4LC?jf=5jDz)8f8R{GNMKqQJK4jme2}<&>Gr6TWAOEp#yY;Z0H1?p$l|{ZqOZi zKuK8~Q+B=m-6=wYWcv`?I(|i~F;_MwXoVLw&Ga>B%MJnXA5wv>l0rR}INx+jM2iJ^P;qI>qDd-h^G zRm69aaUBnv$-`#yu$eq@*?-|qa=4Qm?j#4xGm7OI#qx||c}B53qt3*i3;m!!41j@<1F28*e%5(!G;6)X zSi~_b;uscj4o^3Sr<=pm&A}p$VG+l~g8yGz#Efn9-w6FTLjR4>eCO2GoF>Pz!299jFWS zpguH!hR_HaLlfYQWwd-QT0R#opNp2yMa$=+<#W;UxoG)Zw0tgFJ{K*YiNrbabYBmw*i410Z3dJi3=lfVI(e$ z#D$SKR>=cVWF#((#D$T#FcKF=;=)K=7>NrbabYAbjKqbJxG)kIM&iOqTo{Q9BXMCQ zE{w#5k+?7t7e?a3NL(0+3nOu1Brc4^g^{>05*J3|!bn^gi3=lfVI(e$#D$T#FcKF= z;=)!SzR^m9N>CZ9@a6#al#N|v|7Y(g(6VN&;LF$suRz6`f;)4|6gB0L{6ySpt;DZ$4gB0L{6ySpt;DZ$4gB0L{6u8;Y2|7a;z;bXm z0i(W4G(1}DI97y%<;6y(5Y7(+iiA1+{f8cc@^!Gnw7VweG!z)YA0v*A*>4E_dl;BvSE zuB1*^VY@`A*VX(#pSp2Ras}HfAs_C57_5TTum;w`ov;q>0^Sjq>){@_7w&@s*Z}v# zM%V#>K zuo~9FU2r#$Gp1CEi;&_Xq__wvE<%cnkm4ewxCkjOLW+x!;v%HD2q`W?ii?oqBBZzo zDK0{ai;&_Xq__wvE<%cnkm4ewxCkjOLW+x!;v%HD2q`W?ii?oqBBZzoDK0{ai;&_X zq__wvE<%cnkm4ewxCkjOLW+x!;v%HD2q`W?ii?oqBBZzoDK0{ai;&_Xq__xAx&TkQ z08hHWeHwPa&P0J(?Q4shgl)bN;drr3^b@PZ3u2LYk)Ov{QSgo^6nl7o`DyX7_=G*5 z@$-@uu&!Xe`fc`DYluD0KFJpLczc3v+q}hO2kgoAJiD?z-(F$2v-9mY?ZNij_Cfm! z`;cAC|8XbHjyaW`fp&p2#0lB^ong+!_NUGz&P*rWneD81YB={g8=axf!_F>exbuSZ zo^z)2cW0l&TS-zlGo&M>bD4Cd&zU0wvXXO!tRky85m{X}aITikJYQbpJRz@@ zTb#G#lk!zrRlXtj$foiGep<_q8PyrQ?y;N0IRUWHqsG733s;%nFzN(RGA_u8#)kzLjT~rqtQr%QHdA#bWddgwy zSaqyCLG@96r}3ss;*ag z@&dI|t(I4)wQ8fBr#7nxWK=z-o|HGMZEBmmO}(q$m3itT^^v??9Z(143iY}AQm#}# zy7gqtZRj?UJKdIUOZl7|bc6DFw~gCHzTh6?4wP@YC%7lbz3wPCNA7b^c2AZcxu?6Q z%l+=%?tSuO_epoV{M>!p{Xl->e&>EKe|8VL2jxMZ>+{K9eRF+t zTn2OC8ki5)!gY`f3t%Cvg4M7F*20~z4p{vl?gmyIvED5#Sfww>?;yzUz`M0!!TYo1 zLkkPup%tv|6=b*(tnL*L0l6LoS%=8g5=Ik+(L-TDz6Ze?Ho+P;u@#;KR{IK8ABm@6 zJ3I|L;8}PMo(Il_ESL?K0?*C*8}RI`%i&763Lz0hPVcR&nQ!D?6oYvE2nHkc&}+dLne=VQMByWvH633y&M z&&z%dUI(6;{U*Ex@4&mT2i}AC;REsA$$b;;bY(#+n)l@+5Q~9fG^=I_!_=p zCiG&M0i56A{0`@IilKy+;tohq;D<_poJeFuA|n#{khP&M)Q5(EOh}%&3P5G30wdu` zR^ZSkF4u6m2bX(rxd)f)xd+M4!gKJ^o<7>ow;i5_9q=sBUj9>oXX~fk{j|BCHutXt z+T2fj2eceD{!7!)tM7vX*Z}v#M%VeE(ONHe};{dqItZ3q2|$V zDE#|tpZ}{iPm{lF9yG;YHqZYl?+VRh*32>6^RLuA|6W?>47{o%d0U5wn*U$bKJEX~ zK7VPSzhXPYQgm#m>`lWXy%T@>^Sk9jnYc{GoCG>>^Sk9jmttbw&~C#(a;Q|8e;LGCT)(LCnSJUl&* z=(b06+atQ|5#9EPZhJ(xJ)+wl(QS|Dwnuc^Bf9Mo-S(JA^O#5Tm`C%NNAs9R^N2Bd z#F#u{Odj)S9`k6P;4OaU(LCnSJe^sdc{GoCG@W6>^S&sqT2!>y3&=`oi?oXKNe&0}88vj@Ro7y@(Hw}ih-VHqq3KF{2m$K0A{-YUO? zzcE+^t6>eSg*yRx!T$R1YBG;`Igfcck9j$dc{z`HIgfcck9j$dc{z`HIgfcck9j$d zc{z`HIgfcck9j%Ieh=P<58&^x7xux2@Dc2Xj{%usUe2?T8|LLa=H)#5OZW=DhHpyq zoL$V{8NfA|oAa2PV=ajB7Cq+YJm%*-=I1=-=RD@;Jm%*-=I1=-=RD@;Jm%*-=I1=- z=RD@;Jm%*-=I1=-=S*%n>@QDMhAJ=;o+Mi=ZOa^;#~hu<9G%A;oyQ!V#~hu<9G%A; zoyQ!V#~hu<9G%A;oyQ!V#~hu<9G%A;oyQ!V#~hu<9G%A;oyQ!V#~hu<9G#ZI0qC89 zFbD?25Eu%_K?qKSac~lx4CCPxU5u_6peEFU+E54TLOrMt4WJ=3g2vDUG9e3^LNjO%EubZ|f*`bpHqaK@L3`)`9U&V! zL1*X!U7;IvhaS)qj)7iqEcAvx&=>lF{u#hk2Erg13`1Zj90wsd5yrtua59XCQ{YrM z4NiwMU;>;8ylI119>gmT;*|&S%7b|2LA>%HUU?9&Jcw5w#48Wtl?U<4gLvgZyz(Gk zIsPGB1Q){$xCCaxESL?K!euZAu7UY*EnEk@SB6&}#Fh)Inm&kk9>hBj;++Ta&VzX8LA>)I-gyx3JcxH5#5)h-od@yG zgLvmbyz?O5c@XbBh<6^uI}hTW2l38>c;`X9^B~@N5br#QcOJw$58|B%t)|cnnnMd{ z39TRqt)UIHg?7*$IzUIrhEC8Kx*3~c<<^j(a?>vZi9>hBj;++RAo-f{c5br#QmJi~M2kn6{2nNFt zSOQC787v2V>}`+-`EUosU=^%}HLw=$1Z1TAdrEjlXyhOsbr6p_h({g7qYmOx2l1$b zc+^2W>L4C<5RW>DM;*kY4&qS<@u-7%)ImJzARcuPk2;7)9YiAs?GNE2*bg5A&l!(8 zX!ES`sDpUaK|Jap9(53pI*3OdbSCo6G8glA2J903@v4J()j_=KAoH(5G;$D+I*3Od z#G?-4Q3quuKxXi$gLu?IJnA4Gbr6p_h({g7qYmOx2l1$bXyPE=bP#Vki1rPtr1lNs zQ3vs;gLu?IJnA4Gbr6p_h({g7qYmOx2l1$bc+^2W>L4C<5RW>DM;*kY4&qS<@u-7% z)ImJzARcuPk2;7)9mJ#7@^CzF8`B@}oq_j}j$6N|gL4QSzfi$&V5xKT4GRC{gmGM9Gg5B|l1({3ucK zqeRJ%5+y%Ml>8`B@}oq_j}j$6N|gL4QSzfi$&V5xKT4GRC{gmGM9Gg5B|l1({3ucK zqeRJ%5+y%Ml>8`B@}oq_j}j$6N|gL4QSzfiEe|e&i(v*_0yE(X_(!u--pOay!Ci1S ztcQExUbqhmU<2F_8(|Y{h6mt5cnBVbN8nL-40!+D;{A7v_unnvf48>6lTZZP;3?P+ zPs0x0yxs}VBr@%~MC2i^Z0!v^iEQ956E8GUO zCGRvw`9>rvI>FRKjx@}OwuJ544162#ugI zG=WUWf~L?6nnMd{39TRqt)UIHg?7*$IzUIrhEC8KxAN8zmr9NCyV?}Rxyy@$s)g#MSdrn91%A8 zoopM(?_`tT$yVTk56JIilLNyh2Zl`!44WJnHaReCB*rGclU*Is0m-q+?_?u6Hu;@w zq=$FWBR0}wBRzHlKzeL)JlW)UvXLS?lQnHw&=kl?WAh!Cb_-|;tsn@kp$)W!cF-O= zKu6#k+U-uz8M;7M=my=P2lRwvpcfnqy`c~E1@Zve$3X~=hhcC642Kag5=H?za%{fg z+8zUA;Y1h*C&9@u9!`N%;WRiM&VUIJhO=NIOoGY4yZQDxKn_CtJeUGg;e5CNrU5zd z>K93O9{`yl{nPzfqS6{rf;pgN>O2Gjtww^IvhLmj9K z^`Jg9fQHZr8bcF6E*#{-K`tER!a*(^&!CG>#mK>}l2W!be z<2hJM4%U)`wdCxIzwGQ`q@s>|6Qd`opQL`0`bp|1sh^~NlKM&NC#j#LevL;n6 zq<)h6N$Mx5pQL`0`bp$gBDWH`mB_8^2mN6X42B_qEX(5{1joZLI01&k2p9>YAO}Xn z7&sp;fN3xtE(8xQf{S4WAa4?RlgOJy-X!uSkvECFN#so;ZxVTv$e5fD*FqlL4l5xa z?tmDqg4M7F*20~z4(@`xVLjXf_riTp02||SMzoh1(N1SX+s}yhx~e03GOE4JsJ5R`?RC{qNJXw?)tI~#*^F!_ zm|R#Mxv)HPVa3RW6(bi`><^jJuIDp1fX=-*#Pp$29QTKfIPAR^k8A*WWCO?}8$ce}0P@HNkViIv zJhB1gkqsb^Yyf#=1IQyAKpxou^2i2|M>c>wvH|3g4Iqzf0C{8s$Ris-9@zl$$Oe!{ zHh?^`0pyVlAdhSSd1M2~BO5>-*#Pp$29QTKfIPAR^k8A*WWCO?}8$h1V6^&>z2serubjBb2;sORU|`FVB$p1VCq*omiw0}>Rt-~&GdAPp)(WvBvGp&C?& zbjW}jP!noFZKwlvp&rzS2G9^1L1SnFnUDodp&2xX7SIw}K@eI)8)ysdpgnYej*tzV zpfhxVuFws-Ll5W)$3QPQ7J5S;=nMUzKMWvlH4p~DU>E{J;W*Gg$0zdfH}k~_FdRm} zNEih$pNP&M%_QL_*cgAh@(5K*%bQL_+Hvk+0U5K*%bQL_+H zvqy=Vg@~GktOn2!8bM=d0-2BnO`%!*L#sJ|TR=-_1wm*HZJ=%94WectqGlnYW+9?x zA);m>qGlnYW+9?xA);m>qGlnYW+9?xA);m>qGlnYW+9?xA);m>t1t9}{)v2I7x~04 z@`+vK6T8SKc9Bo)BA?hrKCz2@Vi)>{7oMLw~Md}0^*#4hrQ zUE~wH$R~D@PwXO}*hN0Ei+o}i`NS^riCyFqyT~VYkx%R*pV&n{v5S0S7x~04@`+vK z6T8SKc9Bo)BA?hrKCz2@YYI$-^Wg%R2Gij}@Zci27-qmFFcW5RZ?ge;CK?zb8W^&W zX`+E4;u`tZmHfR5B5*a#g?WI?6A=s%5eyL#3=t6w5fKa#5e!+#y>$aDg2iwnMByg5 z8E%0kuoRZTa<~<46TPiGxE)r&O2~&hAO@>oHLQWPaA)EGF_Z(uP!147IY12M05OyU z#83_pLpeYUC7 zUVz>3BD|FN(RvwPfmh)*cpctI?BUIoJ>)6dL!Pod)6dW9@}~@F9EzpTcMGC441%Szp69@GX1?-@^~^Bm4wE!!M#2k=l?w00zP!7z{&z z=Ru@4M5H!Eq&7sPHbkU0M5H!Eq&7sPHbkU0M5H!Eq&7sPHe{~_bQF==5UV#sL})`q zXhTG3LquppL})`qXhTG3LquppL})`qXhTG3LquppL})`qXhTG3LquppL})`qXhTG3 zL&S|jL})`qXhTG3L-v077|=6BXhTG3LquppL})`qXhTG3LquppL})`qXG271L*!(8 zn&@nZ=xm5sO1{IniKA>GN*f|d8zN8Jk3?!iQi1|sBA;k&h`eokh_U1owGGM2qBoJ- zkWA+?NNlZipyuh$wD|I7~iqm@Tq7$Dy0#!y;2sH)0C; z#1!(0DdZF74H4xH5#W5J#A!r~L&P7psL|FGH3r67J=KXYEtxn{R@f;TBjTdaI@EUk*f#6;WfA$2O5;MdVl!ZB|5& z)hfzX!&)G6uZY~MyV$-PHgc~|iV)H3kXv9)aW}yIu#w31CU~0d9k3Ig;s0Imb|RnH zNIuc-5Yg@s(e4n@?vTs#bH5Y4-Gd@T#5?3$kjN+M9U|%-@~vn4UeTLxW$EqP!1n!# zJ-&_bc;ZLj7TC@)Ps0w_$^K_JZWrax@;|amggiuqJVY#I3wiZ^B%ZQ`XnBZed5CCv zh}g;&Vk=w7vG*g9^AM5q5V`h#BzhhqdLAO@-j9D-dw*GbSaC;h?QQwX+GEs5bN*%R zVQKwk?fqr#{blX_W$pcC?fsvy_Jl32hUG$tRpJ@3j7Z66;t4BYj}af(C-M^{v*$^6 zYm?ivjUBenvOAgFo?Xc8`KvwFqtA%$}{**ksCdh&LrIU7vg&W+^l%y2fznq=>MP&Ss0oh{_)97Oicp>nA6 zcX@%l!r3dYCZFf`GFLw2{45`lg|fNaLMG3k+(#zQ6XYjk^2{NR=Qrff6lCliZ!&hC zrtnC}oSC6&$g@-(a&=B3CubirWa@037m|&0p!C#WvT_F$=(1HTjTwgDjm})O+eZ`IP!V zeJHo9kJTq~7r8pWkk6~H)K_x1$=LZK89N)xAKffBOa9_EbDPP7CVS_vZfo*({-*PG zD&^+5IjWk@*r}@PT%9Ui=jv3ob*@g;KKm&)LD`=gcNQ=VPj~$R@^khx`8oTW{G5YK ze$K%rKj#pWpL3{R=jRNW{G4RxBtPeHlb>^h$_x z(ZK3X)XuW{TQh~UW)aP@tShW5*uIhom1Qj?=2X+Vm8g?t-9{{_inYUfMl`f`Suctv z)=SpwqK5T`^^vG-eQJHi?=P({`TZ3UlzP@dyN>YLby+pz*!AotY-ieig>CmEwqn@> zh^AQfP1-fBO| z@kOk~$*{NC+t~Y*{S@2V?d|N{X+O*F=ZM5u_N(@*qLKX?5g3ci!f*4bcZjT5_5u3< z+n*3YvFvYIujAO?+dojxA6c8@kbAh8GnBCQ#&LX3FJU{!vfjpV1~>!R9>lsEhkV4x z@%sd4BEKg&7m0M|V&X6sxryhBG-sZ(M);kz&RP*b-`&OUyPfr-vCdY^?@i8wqLK5E z^9<#?SXZaegD1O{V>x@AJ)#eK@jW4(4~XViT0e?@=*L5%p;Jsu$I@C- z)Idurwq5CpPSPiRLP|f89Sgk~5S3+`OrxX{@f}N6Cc;w%9a^33bfP>}WCk%F3w>IX z?OMcmELlg^;XHMT^;oi=tjA~T6YsHPL!v#F*1424lg&g!^lEcH)k3xqwrol4$C9mN zf3^p(9>aZI5s6lwBt`M79_W`9C7B5U*0l<$x`M3&qs zcTxT<>tG!DihPBVSLLgm^ELT8N4~+j7)QP(-w{6fF6&|(`M!LgW61R^G8iHD@!1bq zC*#QdtdwyWF+LXQ@&K!49CAQ^#(BPwUvTv=<(GVle9+YGYvNrs8Be|uwjNQKMI;Bb zuozLQ@S9A~qP41~s)-(|I#I7Nj4>HP=Z3B!x{w>XHhb$6?P{$Ws0JLMGHo<>o`NM;+r)X&#o6;)k3vUbW=B|8`xf?7O@>A z%2r)1QA;^;nOerRmaFA#-%6ydy2?{|Y_BB3R$Z-9tJz+o)^N;PwU+HWiLzB!cd5JC zzDM0lWbi&!Alj-8Y6HjIukL3icca=!%Wqbj*?vGhz%h@h$2g`?J;C-?wUst}lIWbp z*!isJubxw^6;;ox-J-dAQN1VzsF&1BY`?5tru-H43Ngu7)vMw-^_pU}t$Itn#oj$? z5AjIGS}}^T_5)&ue^-AOW7S@@SDdK!seP2}SNp~BI$yRJtv*qoh(YRW^|ctL$6(>= zF__<(ZYJ@)rfyS_qeo&fQ0LC3oH1C`*JCihR}n|FTr#nXs_sVOht=Ju-JNVdL)@@B z27Y6IJx6EG#`L^P5pwNS!rX^wr}rCFILy$p-nd*(P5$d&!qAs`xhe zHn2_BY*AT{$D#(~G2@hPr*9|a&k#GT;@johMftPD4lO-43rCO5!uDtSvqTnSa|e;; z@5rhxM~~3_CWE#J=nc1l+GSFm>_aY@U6hyMqq}ez-KR3T_hxk0Z!GxC z7@xryKbp5H#_%HixqBx!HGnj#`#|-Rc965)XP#+`zLeYisewk>-n7-RBCF?G0E89<6 z+u45FVrJgh1Z|8>(AL-ljj##6#8%LDKyzaUSjG!EQStP49`*iJXS(eV9Pb=Y$uMUa+uFjYW-N@_#=^+Z-+q9tFw2?6F|(c7Y+veJ%9$_2 z@(38q!!edeRbzQnHI_$fV|lbTmPZiFgC4?nCOjglI*&S!QlH1LMXDNGq`k34s$z@0 zBM z>m;MpF3}cAl~RkOI`&7>@~~xXtPMNG+Bn|W8N=}Hnu?Cb+GvBd(UN1dg;CvD7#YUG z7;7wy5!(L~eq&qIG`7Vb?TPWmo=7+LL{%(_M?`J;D3(Ohh6u{7a;rESOM($X7P0C! zAh*eFqN#j}mAAIsj&0Et+hQkspTWWiU|~GV_H(Sm4anzNiEGIhSc@BwyIGCfQohJ~ zTuZ*hirj#F8C%3Mwn)nqTO^1r@&7ZTU9a??@~^zI$R1+wWnQWE;DrGIq&_ zY=4ABVjGLZ!i)Wcl27HQ*j1llm-yx9@^e0=trNeoOR5^Xq@l4(YGIcM(M(xbCv}Z= zQrlQ38OAy})>tP4jdgOSu}(S~>!c5Ua2;`~s;lZU((_#j*d^LV8E9;jj>bmmV{DW& zjEyqR*eKnNjndQDDBX>X($m-|-HnaXQ~S_ZIeeo9zxhTBervl#8M~y0_MsV}wMA0H z*djxWEz;ZAB3+FwGQ`*-y^Sq0*w`YyjV;nk`_@<|e8&a9wMA0HSR`GIMbgh$B>jy= z($82V{f$LZ$5Tp#C+v@RL}&et9O4vXe@rm;$53N`Of>e#Bx8RJRUfJk#bjatAB(e$71G35 zA)|>0d@fE`U#Krc6CD*0Cm&{!5EUS+ytYMhbW}i`MpU4+=$m4fG%ePr`+vqC+!mA1j1)*k-EkfscUQz>AS&qgUHZv0#V7>B^jl5NrgpH z$=D)Q^f!12<$KKcnCNKil1A7ij8?uKSSPKFbyC+@CzXtK(g^G1S@u4MjnY`h5`;}G z;Va?yeeJ`?GgeD2W3|-M5e3mwM-=$2?G?+|D^-oXV!rW1)Jm~es+QU-+EO{jSR`$X zMbZX~<^3h znzEXeMAD|DMX(fn*bLP~4Iu`kXQf-er&(a=&xMCm60t>G`1|YMjCTC~#r#fg3!Bwx z!uPYEUz}N-m1XIlW>!{umR%#BjsMKJ7|$+AR4>^lLx&#qi-W>#SNv-n6EXSGv?=DJ zbue!Z`*@9}ExUE?-lcQxS~cCQlzx^vV+uN}C4qxfigIN0gT@Dlb1rJkKKbvg0etCtK4~%C9UxeyZi?gk^We%K zeO`k-?9ME6H~QA}y}7MAckkAsOS&pITn&f%4-&urA;YjI!%5C@ri{D+Jc64>8DLaqmNDvVEM7-rGeGoacq2# z(H%}%O#+KczSn0?1WkF_(Z0aa_~l5dOw>-6n{JgZ8vd?Zb*qt8%W`T}wSBF+ckj{d zhP4~#QaX(rkqUX>bkYP!vucCec@b!)ZKO02QnrE_xJj82%gjI z!ege;aCawbRop)`!DCRon0uCN=|Myc zK+_v_U#jrkhl`(z|K{{7o_GYsACNyCO!NO-TyZO9m-jCzlb}HCmQ)EkOwQ8&qsmV~ z9-Y$uJU*pAw+bvEb}K}=1eP3eZTy=9VE_E}$rL>}?erIJa**Dl}RO6%0*wYB(^ zm+OJ1;=D$T_}q(ne_$Lf&^q;ViMz|o5C2^G13m1#rPm{>KPV}#h-LYZ^SgeKd;rOT zJ8d%~fH4PV=n=d8rvL1d%_ZA(rKo(ee4)F@h}!0Oz0`95L>Aj=(e2BfNBu&!;&{%9!5U9o7axj6 zFiD62{a92XQAS(Qjoy>h)OG9B%Iem#d-EtI9FHfXc9+h5?H zuiHN|?jNY+M-Tjbl@o~HZOM+_S@#`Hm1)GqC9`y+AxOs?0h`HthH+i48`JcTWcg&i z*HSlnvRvB&$?~aImy~jC<0Q*HVpT^ven#2x`nu)kZ{?cn(mTy{bW@_zw;7e*x=f+} z=_9p$PCrW(KgAcP9_^!z7pM7uI_UN|n$|+Z=F=*VCPi#)R%3^;GoxEqo2Sdz-lBU3 zk2_Olq*u3PR@3G!?fKS2Rff%2eoFpVbFcYg#Ysq5_4o-H*M4aA+F@l__y1ZP&xwD( zIsW1oOYGgI0#@5M_E>F_61LcqNLaj0d~>!XsI>#8+xmgVG@w6Wh~`gH4e-^Kfttg&hoS-$!6%hI5doyO|iAFSMkf@`y-}^2$Ix$&J647KiW-4ug2qw!Hx=Ax0otRpV zPJA<|KT5pRa(A}vl$IABR=&Xfmg_r-Wu^ z=c%a8jMCcNubUy|uI9LBrJP^;XUX$i<-TbAv&|)!>Sil1pJ|P*IDe)0%F6v`D#rh^ z`${Q4O+AtDU`PdgM4be7V>;5uCM*T zzStgLvuX-VN2~FYJukZ+CNN|f0sDL_=oyDvyFe4 zEdO3?HU6PFf2GXw<9`qxjen?(k@}{i>f`Yb^>g4pOnuSio8!f9eWS~bao)_b%0)9P zU0qYsaKF93IHN*HD@^hVlMd-#O|{aJ(mf#M1a8`IE1U;01urNL>E2_V&Ek%;t9^mvcoE z)6E|K3p`c2=Pt8nd16l4Rs1cJdu;kOSIOV3ud;+Yas5k7y}mG?h$W_)Pn2qb@_SB- zg*J4} zoENnH)=;OfNi8+?_=`rhSrLEh%{Sw(ubfgeuZlI4!@iGS z@k+va%NqQvb)8jx@!5}-+?uo){cWg;Bid0HjBe}b>f>)y%sY5TT_<~4UY`B>KUS|m zcmE55x%Vd^~m+y<-T*UM|suA^q$c;brh7tH&5)w0(w8dLJMbAJ5odX>(N zf6_2j>>eq(EwvS=ZI7f|Lk~Ur`4tOFrLnJ(#tunobdI)4{=UUs8~sDI(jr+h@*k;M zV7L)Tyz)N^`>|?ie-h zo10QTK?NiUc(3e>@)N!7v<;PFqo$!{XKHna8{PjfcZGI**XkN>W zIVMbqx~)+@6lJCRul?nN_1xlgYg2qw@y+ha@4x?bQtFLzg3TqXOv}^|O|%EVz($-w z4{U@P=%X!CPN!bAHr5*!{j?x^LbW=_*XuW9*l&9)8gNpt-0QqSercUz`5KQpVOZ*Q z^bN!_0<{_SOB>YhxLcDqT=KC0t(0=xy(FGFFi&nzv^H|5b~x}5V@8d_TZv?*V{MwhR~r=AcPr_W=*XUcD` zqXX8ue15XrG4&7JX3D8g;GX1p?4UV5|6P5aEtFqx%(PWDtxSDlH|z4!E{2hoY?m`j z#s{*JpXc3p6Okovn`wvlOgpeYwH=7qO3cuT_9z*DSbMPF#YAL(X}hpLwO#c7fo6Yc z`>;Q?ee`~F{Xl6uu|Kt)^!{ti=S$nGWc*?6#eTKI>@RIM_NTU+-v5-@pKQO)#EnyW zIC1JC_8OPgrW4X*jbPi8;-iu-G7dQ=pmQVBw7NGVzFP2GrA){kHaW}ZPCn9HtY5rZ zHvL-m7taW3bpBT9GxW+1YT@(yo%^)(YLCg8WAvOU_r>FXC(BDB@nFdw=ede**prOnHOIT>#CsWQfT!<_PAQ)pe_xj;##3Ho28iVGQ{%JE@i+-`e%ffHafkR? z*T_9TQD{DKC!d&MRCw|e)8ZGIa%9MvV}_Ju`Ske5=1c=ip0ekc*VJ`qJ*TA+>9vy~ zmE_T0;(K$J-`8hGvOaNooRgJO|2gtnb9^4vK=yE9X?oUHe1xKkcQZfZoFA+3#|l#YT(lu-cmTI5$o&r3Fi6 zf+KgDBYBcC!}O~i7P==@uQ*?omJ=qb=tpe&j9%HniuRQLgM;x<@_6z~>hf~EqFyO2 z=P6NMu23tnE=PB1i@>rvXc35wl@^U?t+Op;CS1S4LdGXs(bB-Q41$RgljT*1 zu9jEDZ;Ov|Z@13YBaHr??4~>{^U*ZzXMV7ZGJkt-i^irkA zyT15Ax|o<;vPV~-{47y7TyeA!#pKz~NsKjup1i6y#FOQd6F;Yv>!4AxoHS=C@dmmR9tsj;`)^1wIh){etIIFEH8OVsg!!ovWKPAQ#%;R<2_8C^1C%%K3Prz zT3w#_Qa5=&C+HIj)UsfOYP9rFv zSw<22NAA-D4d-!Zm6qS4pBM3tGs-^Uzr}Qo&v>;%?^#?L37Ldhl^h&Cb0)Hf&hbd; zz4^x{&&5Jc{fTH|Wcj(SF5QEOvuAmN;U;-~U(9rb&v*gGbdY!Tz)QV+J4?$yGUd0G zmD5f~cn+rhj`-Z%Pm))*r8!Ujt@^qQrtUMQgM4P~H#LeSa>{CCTF})ko~#L1tdQsP z5)aZvEHQxOYB$qG^dcr>Gd07W>XBup*t>Ug$X&B3zVqeB;;(JAto!YL7Y@(4u$N!_ zcEQg0d-?OMDld$l(PzlD6S)8OCGN;?7I5+ z-~XQ+Lw%TjqH;2D(+;Ff<`DWr`wetKiR6EEHBx9*m)MZ&n3?jZ!vVQ&_jY= zhnkGS)6ULxzxWlUm^@s)qB}NuvGmiO9DlpCg*^SS^77fbTb3Q~$Hz$??>op_V5Yoe z+hNBqbVIs45!2<`A~46dA__}6*O?Z-AbFj5z0}V!9kR6ae1)my?gDoMpR@J+zuSu2 zadhBsKrO4(XL5EGpQZhoX{zm43Gu}typrS%Y9lH2L}f-2qEzHxLzR=IJ)8{F9Sg$=OEBD*}QHd30 z=Qq!o^ZeQWrBbHNRy^nd>E>4W-6h>ODSmzN{AxI3mphkWmT(V&-!Kz?JE;80v-&3K z0Dh@abG}tRVM4hUnWV8Z**c?4WPI8zDl4CD-%wUNj@Jq#dHf9VOId3icAj&DF(QpK zGRd@3@_3T3mY#pxVdr1y%SfJ|8MTz_(rM2uZN0+Oa(97`SUBDM(UKzHEwo0uo;UkL zIM_;QpbxE0r`@)7Qp-6l2ne4TaW|#5S87<;e@1HCm3rp7#j7WmUzvp&<(_%B)N81( ztXa$A-#ye~Y_rBAgHz^Ovr;cIX8VE3X_c&jKK~gL{o@b2NK$!}KGv~D9VJC|v$ZkZ znQ6_5kE&LDZG1uUc_u$v{#5nT>tST`@K$H!DlMOGot^TO^ixe9KTUj>QqH(jT0VvJ zDrTCEn7|`?1T^J(c^v0Ky~|cubkFctE{Ttbe;5BIKC-yj+H9p;)vO0f z;>Tb8`O1}_U!D9dg_+8(ST^ws)7zWgvD)P2#ov7Ao%ox1c~%>iq?ili*XawlHA^Y} zhDQd))rpB#TLzV*GAi%&X=DwVgH?j6ZCJ4YNc(w=O~it@=;eoDF4 zZOP-AB}ggPy(U>c&FY*|u6swae0r%dl{`N$0F;)WZ4a}nm)ED_{4@0V^)oDay1X}Z z{qz2SNVaOpYEQ*lDp$wRdem{41(dp7y^g2OhpUN@g0}aVt5#(EpwIc0pnqq z7Sqe2ljSUvCLmYVVwL_@UcSxuqCL2*+>9#8miKsNt^D|u^Q8N5c*~C0{$T0(pKu93 zl$V#+)4!GTw<|AadcO4dGhL#eM<_qdU70K|*`}Xh+3_r29-DGJ)9I!68}a>QJSTm> zjrFv7vfTHrUCAt~zFg}?J#AiE{+;M+rp-^Z0gH_8+n=jV& zF{89@AEwQXr0HeN74>?_jQWGkb4%?3spY9X<|XX`TFK*6@8cz7RV2&Lw-zOz1J_3x zlpkMlU9^?C?w~(hSGS+5?+-)sL)zgc9faZk`R2%TDhEp1OGDquG#Z7pOK||4w^m75h(bCEBu2f57x23B;(bDeU zf7|1Z`E#WxZ-{?t)xF{9G~!@dZzP^5ax_riQ!G(m*ZlvZ?K{AtJf8n=-}mml!wv|< z8W02%OH>4n1;sAd0I`dTfQo`3_6AWgDvH>z63x1Bm2nE_Z|C`RO^lu&f zFs!%EkM?=Ew2>Q%STC%z{uD{0J>Cbymw%Y6Bb8ELn?K<_|M&w}Yl~^U?MX#1mOPt# zD?RXUrwLrP!TuvNY27>!T<}FlGI5r2 zX!oeq?y6UP^{3Sev-GewOQc@Mk^EOoH`<=YF?B&ObNpnH~ZO^JgW|~ z51%jKo!d}at#X|CPh1xnNqmsmb9P=>28kqE9xYj7XNWNOFmMtZ5Q6X$7UL*34=h3i zWttTC?qWl8*aBXlMh(DQJ$jM+@Crju{fMP}yq|UBLm!M)-*~;~E9W0xyVRQSUV{vKe8_;*d|6;Ctl5V8zA15&< zdT|ni*8#G9kh>U7E_>jEsD!+3+=vKZRqMY>(utXdDwbtylqlY&hUIG^@RF~0*cTJl zOS!Z2)#c5R7J~$0!LMQgRMdIX(Ow9@rWuVsYQEIF`uL!C`fHEs z`JA=iH1hJ~K9*pWnFr4cYdt3silmd_q-6lE4z0J;K4g5N&9{_)ycRoQ^h z^q3x#+aRd9${ee%LcgBqx0Y>z)4>J0MI^BovBYWM?0a6HU9ieUzMJp8Z~2jZfkhs< z9*Z1fHgjKIQu%|m%DeCn!)4gne$-Cmv=oRgZTa3>WyQ;@vg)Nl!@(68V0Q #OJ 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ȇ@(g~$ z{J^}TDmn7Yi&1&1PI_S!gsw8|7)G|KC@bTFvK$4@5=Q+bp11&(Qf^sxjXky;RtB2& zylbx|hhkd{3n&*VyTGLWE;MT@;KPjW>~P z3sdVOHWmrT!W8E-F;JlmB^=sE!m)ukT z5>CYRg(v;f-)5mXYz)zp(j{a-P=%m!FmIBN&5dNaylYGW zdja*PsCQs{2Nkjs@ta&OKD1uMxmzU9x=M4<164zC=?tFwM03u<%Y%pA+Pvh`3H5o` z1|d}&S79xit8MugysPqe|81$MM~C-Zc|MsBP?R=NmFiTe7OFmwU0J;>$Mx@{D_^Sj zf@LpjkqBpl(GXBy)O8oHSqWo_`h#J~+O)3Gn6Z%Hv~%zAU#@=N<=FY-(c=#%4*Mi4 z{`|WBG&GiJF}La7s+M2N)VSXs9d4Y`GbnLa;^3pBThIC|%j7QEtlI~6ZkFsUCYGua zU_M8Z0`|8Mk|o%W8fc#i$qKck(DXt=39f0QtwOGVrMU>V$jN%9)D-fz6pB5;(Hg!Y z{WKxZi!CSA1d&KX?Qs>9;7}AvU=Yds5?)Sq`kpCPe^q=h+;ULe>x6HH{sF!Rp_A1} z$$S9^q0W#r;0}Xb^ACWoWPu^xekEG^^zHojuMGEaF0~6~nt^{(%tLj#cmtZ3jXby^~C`RvS$6wCTlo&gcguB%KQ*V4=HN}hZgi|xwF(`TUfm&-hL%p`1I}cOTKH$BM#lUx=C10|)M;(fqs<9Y9f=;rLt`(Uf2*QT=@9rQB8(riEfI5% zC$$??t$bjGsHx8$|07iej=eFjr4gr0>jM|GBd&ykAbAYK^94AT>5%g(Pl%3Ra8z`O z;fYRc`GWt!wh&v`%E^P;3=gHQtq& zh3K}N@^5-Jo@a~xv2R!k&2Z{$_BslrYIBJd*6br8WN`}a;yB=B`b&6_}6iizXV! ziS1fEh%QY5463K7qoa$h5Qs~!V3AVY2C=yDNJnEpyEUU>{-YL7VTk#5_PurFhY;ps5$5iM|sn4 zZ1CeKtWW19fge>#zZC0&!c<)ZkKZg13KntqmjVv`?(yx2LPy=Z$D$A>+I}}NyIs5N z#36IrwVPY{9E*SOkj0-n$Cp2Rz?Yw6VPmgOn|5{FIQn}m?p=t!;LmUrJah^@7v0}0 zu2!o5M92BUZjjdyX4q(tF57=%@+7HHBPxt+?4d(+tMQX!=c~Z3uENOi1;bF{kWw-1 z`Hz&WP?%|K=_!4uGnP^Rz`EF}TD_?+N}XeIzu6i18($$9nE5_2@cm3MP)6?m(t5(W zmdUk&Wkp@X;Mb90c!GDC{z^=)W=Z@9Dzu)Q|CHNf{qq<9Jd+OgK!yc-Aj73dCMDDC zPzrh5LeeB-bYitJGf@{VWKy<=8Ek_m=~_9#;l{DSlWj2&*&am)Z18y2hul`Se)I9C z5q@G99@MkTcD>h2H=F}6JB9vH)JA6R^(<4sT~sygHPU8YRFo%4P)a743fq}XwgQ~U z=jeD$emo1GkdcirD?;P-x+Fe~q2j`%5YuvDI)<{RNK81qyHuxX9j~v1J zE-v0SID+-n?7{kcxusw|$rTK4C#wFU9YHNF;Lh@%4|iT2e~B^3)=p#4{vwOThDE3v z;x%}I#qNOT_X-tcnh7}07N7>bibsgV+|YhSW3$hdX+7Er6=u53_Uc?6@LhJe%XDO< z(Gxaub>+j?usOP2MZjn4HaWH5;lQ&%cSpCQi1u?>Nhdt{?H9q&&2PWJfoCP_2xTjk z9j#t_okB#AUaRLK%$3>We^7DiB(vEZ_wUr3U?r{(*oX8IYmt9}V-tOzYQ0@-tX5VJ zRvR!SN}N7Ye$R5e#1#prsZvkT^VK$Ma4Lo-E^S1S(Mth!=Sv%|g)eR7vi0tTFKv`` zalRZuhC6@h+J^ZPLo=s)8{pL)&~3+B&feCehVz$TY9neZk2{?&)C8$ zY~9Fz-ZfzxayLLq0`6qB%SjxV6Ru=i(nc$@^S1muipbKMYFv!E0)7?8B} zkw;5*-s{<;bmYCCFg1L5CASW5MI-0z@MS+=bhYP_WIHf%vr`tVp;zR9u9?pr&u7wk<^=nGv)?0y$WjD6o zQpq{1`+g;_zuY=hFv9#=avG z*8Nc-W*IwP=5NB+Ek#)#{~ZTSxCan^@l(iP6gAE;22gH$U0aqJH{Qab zvtD`s-Td%6|@W0CqV z!nLz<4zFRA+2mW_vq`Md2Zv^{>dyykVl^+GXSFu3=bvA`#;<)KPu}{Dny2H12ob1v zJmL=L&B`)wpMB3etYYQF8Zq;uaoYZC&U}-RPje|1krQKHF7=o|K@(QgWYN+sgjPA2 zjG#En4T+^Y4grHLn+o+KZ#2Z@`;?VtFLh1+K{!i6JyslSM(H2q2QjL;)?d}@ntYuE z-P_zT_J`|S)$4qK99D%&i8W*9VOVf>l%RpJabjS2%U03VwQM%j02Wu_)^SLo&1XQ- zox+Iqos&;4=)zWx7*nZk#X3U{73#*ax%!Zg=ZqUTI6<%Hzqq)Vy1(2l)6KA)RHujq z03`^28hI^^_FMyX3Lg-j+Ms+xkD7_i<4&xKfpwb_9^q|JS$UV@0r7_yVfbrc_pXu{ z`U6A3JS`=J4~SWLBDO_xt@42t8x9ZG2QNAtA5h$d|EwCkBf_0#RHe$bv@;3yTWAj9 z4TdMApm<2vQIT^+Zg71^p*3l)G%fBN>Bbh86sH-~ko`~4=4hKM&I)2)wKQ4j#fr1G z6Aq&`-jE_R>{P(xD0(X8Kub4#Ymc6yJ!xc#Cq;Ktv`6&RsaJBIzAxZ&02eQ1i1y?> z1sobaNxqwGUB=5fe-F;|YxrFFuk_1aT17I=5p!o-MIniAXH;%eqwB0lCWX0jdA_=| zM?`l1IwB;mUq=qaSVtoIVh_exHp&Tc>gkm$$*FVON_W7z%{pdxtPA6T%?jDYlE=Bn3zNZ0;-B2aedrJttx_Heu3V4!)89NSo0xa1EzN$!aQ%? z+|4ha=b!S$Z?bjn^MCnXtvs^HR(LUO&J3+Gb91k zTAZ z!e$Iho9E8fR_2TOm9zZfF2-(t&%A~pqhk7iVN=TJ!>&CU@x_8|-_;DNmfEUY|302K zt?73bZ@g5ifmi$bt)d2pgA~NFv~OUQAvlQgva*Z^7Nya{)6GZ)N?@oU9}tyqakYqk zL-i_Jy{>z9vL@Hhv+(n)W}bh**e3qfDgNh$UYYIKo~5f2+YM=?ES@@Lt>MSr))|wY z?Em10YM!O|&tL5|XR=<+Ms^r8m$h#*vQ_H*g%e-&A}a8**(aca4msFc<)vvT2dc$e zS+&3AlV9qVk|Si=q|%wku4R5FKVrVQZw0*-Ic7}6bpF$~!*4cDd}~@t+~nv<6QgT| z)S8-*Fx9Z`MIXbu|FQDfr8>#H}+1Fo`MztHNH8<%O}JtAt^-3;2y zjzq2-IK{8X_Dsupv0F%X4VPU;n)q+B3~NoZB19_42q$%NjQdr`h*)ved- zMAv*H(7XOHpN+lQ6C-VpR6{9>qQpNeKw{qpBt}qmD!aHjU+WX!uU^cbnG9-t)f^ zhqtrVtlE30eh5rGI4%Cz;vUK+bF4AtMcMk{O@ga82}<2P;lYQ$4Ett7yM^Pz8;m)b zrT3-biJot}M2JahA>~J~k8Dz5=6QN}%EN`-8C|#nfIR^hbV7^TC$wF|@Xc|P&3Wmw zPQ|lz{EJJ!^Q}}N;Meo)9b7+8#`W{q1(9*PhKI%k)oUC&Z+?rYi+L-TT~ErN{B|9i z_vyQoqU#-tv7rs9&>=Iel2dCkrr`jywld;3`Uq6DfHP zgI9$@2=??eCaSBh@~%}w-+r@k>6SiH;8-($oaY#ph-qO>Aa2IKQrsL8Y^2hpQ8+ca z7AeOe>dGQyN>wghQh3pF1dSMC8Kd_%ra&W(5E^eJD!Y&d#kxVD|B4-sN9*S6;zaut zk~=$G8x7x>t-boxWwisIq6>$-6aEtg+GiOOWqTMJ;rVzFvj0fHYa?X8Bp;5D{bvM+ z?-T3Tg?wp}&O!WdynaQg9S^jcIL~-W!Lc7-lmM_uDC1!L6d3gb%hY?kec7*57}7m* z#&(z%rBLZj9rJpB{Vb-4Wa#Ou+ROsM0-*^>wCWTvMAM)}98hv)5|pAQJMdFMKEwm# zb1fRRGrd7TAQlHzR+bSwd5{v6$Yg4x@{}d~c{OWFfF%pzyFvLh%^lkzGfIozaevg& zu!ZKWMUh5hwS2B2gH}iZhF2M$HRyBvhFhjG>go%#JeNf;Pe@qSm3k;q>tI^ip;{#@ zpPOo$vwLTE?wsA5kbg5EIeCEjIR>D^0AhiY?w!lBw}j7j2y~`B_Sq`^g(9d{A3k}) zR~JRGgKgwTmds43n9oc*l>EH5cq7fFFp4FU6Lmd=OvowGIhdbBmCYR3$u}?CC+p7T z!>KZj4W7y(9Pk)ZX9ql@GIj-^AASZh05Fp{{+BJCyfIq#J z6aBM|zsmNwaEP_xXrE)0YcChI)Scm?mb!x;$Gn&h8*E9;M8C^9J04RE=%iQ&MR1+* z*lW0pafx_aD%EcJ|SGMD_7?cjaU*)wKXmOFid3&86<~2(}g@H@P#D7>fyA zD0y7dnH#F~6sPQf40+i>B3mM+Ny14yBs_ujDiR*YxD%X=cG*5fl`iG_NSFa19PljT zw*2;D*~<2*N{*Po)?jTh@D0JGc8U+fi0D716f2LPAMzIL0v;~>A95+%vh%#HrnB5t z%2{+4o#Jl!O@(5^kOT^0-y?2|DM?|-O?SXa*%KVfUc9ep>k!sHK^3~MqHWYP7<@k1 ze<456y!1=}9oSqHoQBD=M!6ez(9=)3u$-OtsnxiC@7_5_-fw=4<5dXW%CE+|4SV&< zIWi;kI7?((EkJ4THg9Xvpfo=aICU%=X6dgsSL?KBSc&j<#;)F7UHFE+%{CJ zk+(!Y=y}fxAIF0m0wYot+)>=4I#mA4Sk+hLCj3Kh?Yd2v(5>6pu}VqXFGY9n;$}}o zu>X`pGkv=|nWN?r%GSnb^dARlc$LyOBKC?l4zh>p@HhGh?5Pqj{-{mJ? z82)BCey~IdgVwd%3ztLRV@`v>*v??A@BxW(?=Le#3wwT_&6F(e2^N0^7DHczvXjK( z@AW0V{POv}qSy?{l5D2MvR_DCoKAYmP!K?EgUlvoKl%cfE}r5bO>Pn6-uc!&~RW8N)Ovp34}lVaICzos9Qw?x}G@jyH%YKT`{ z-l9EU?;_?iNjFaTqP(2xf7g)+?AqvrzlWBKvgeKPfABd zYA>aLIWKVivC#)+#`=_G3#z@nDz#tDZ1b9bSn&;qF78uxT*6RqUw+cPTv37v)~rS9 z`gRZiZJOp(P6l;iEN|<-q&hJLiojs#wr_yRmucumt{a6%`j~D#&ocY~Owj6~;_|k= zgiD~e&cj2WUb#mP^WVyMmSFuisKbIX*}gn$V93NMM@5x8fA0rJ)+0975@_BK?_A;X zA!>)o{A?UFQ2^WBUK&QJy>|TWi~-$an^uo*<29?*+05jXqXrGj9?&S$^1JMZMMkFQ zWJLJYDa(Hi9Ue7h!IY@{8ZPK-tz|QG3B49m5u31c_zl`TsHzh`^pjcjw#oG?*R5Ey zORcXgX77Cdj0@{r8D76T#Z1qC8doH0n;?~e?4it)KLg53mr*#0Q;WcmZ8FpAg4BoGQgYVxzKZ)MI!?&I7vA@{Mm)A2>AB_^(Z z-cO+Y5*=h4ag`V?%gDB(0|;yo1i(kje(_NMS6)Lk&QgVMQifO-vL5u<5sTO6^QCN{ zxwMD5)K&Fuwc0h{*o=2Cb^@=fyy6lMkb?|F%t_Ehu?(noGfe_z~2qqu`7^=63V zA9FtTJ9d-3&siJJrruyT_;|}<)^75}k!<9p_xOhyS4NM%!k+N{FAlOU_a3v@lgIh$ z$G`9`huN6!TQg?v@73qP%#1Bv4cWGPJrD^~0%-9P_4j?K)Tj@l8wxpGD9j)Z+|;{v zgl+kjr}C%g@SbMs_iRJ*S8GmWogcz?@*d*0(;s~1i4%BprIfPLGGO)1)V^QsS5}H% zm@XZ?xI$3gCYLQ9mQ>&`)xo=_*SX=oI9?b)2jm`A#EPQt{qmmmmV*iyXTAQ+L zU&WNp*`sQ7X}X~QAh6;R29PX=rt}d*TWIwb%(#V50&JgnicfO*C*l(EHp@6Gm`b zz-K5A@e}I|@9^T$H(F;=W!&nE(s>k+aKjY=5q|I|FQE@2C9Zgsj?!0jO8S-E{5`b> z*46Y!D@L9Y>q@P`-^1eLX1?;;HNKML@s}-(v6R#@&}z-Zy5je-u5L}Cb;XYJJvimg2ExTh%Sw*ig4- zLX(`EVZfnld38l_a;P8=5B}tPS%(p;qdwljaqSN;ji#*Rw@;qnUvFf+ zqi1&+w*@02ye5~&Vh1Oj__Ien#Fm?TnRM*2IrllNd+Q?0W-qZz=DjKGW$NsOQZes^R8Fd1utB<{Ow~ykS)W zRO1YSL0~j0?(xzj&qJ_o7bG!IW{x{QYibV?))Vrn8S@|3q=+J-oVW$B{1#t#L1p{& zy_WPHu%v6REhCN{i(TBM-IQ+K=XF={vgT>l;p#OOvYW9lx844Yv5R~~#pTR(FCIE* zJs~18y)6rAIX0rh$mUH)bllBKEnUX*)^7Xx%`%m&$9D2ZE99gZ*McP_uvVC_;4*nU zm|=z`c>(Xlc6U=YD;3N?s4dmO=EbO;az(9c4$GtC1m@egU2v036S!#@eci@QSLN&9 ze}}7N8(YFMnZWPXQ{Xl7%)8D#fQUHQFz>ADVcr&jd!Nq zP<|XYA?gP!0yWWvEAb~;Syr5-f69*XRZrNKvx<7}iuoqnFh^;|tmZj<9X79^qkQ$> z*pRc&v2e@Hd#t$ms_ZeZmia11-2$YOltB#ATk zGll>7l=U{aEM*a_k~#<0*#=8}zL~fG402?*MCm$;FSzg;D-f*rdOwUPwth zKbc*gawaM1%#;c|-stxke;>x*yo(oD@^j9{e##dOzV^YAPvR0jS@^-VL?!X*bki8Y zgo>tt7>_}`E~X;3B3MAIFbyon<8of)u+?f8wt@9DpJaDfPrlL46AU7bS9E!ZZ>tC~ z+0+9ofWxrw#V(hb^WYrIsGYQ2#{qvbkv-t#!_WV^4wA$rA@P0wR_5fXD~#V zfxEeALfMp*%0}CcpGau2bx9|i7P-YaU`^x9KqPI$Zegsk^!KV?bh^P2GPY_qaJ49&e1 z>srdQv~f27>Yin&;CviRUW@q6HUtQ9(de}q&u^HPzOoC<%m&sAi7D@DYtsEU+kN(1%7DzHvEPL z(c?RKANxj3w92hm(H(?2QU&NeMXZsQBkd=?f>xlWJQ-0ifqlw;`3pNdgDLgFUlx_^ zVjV`Vj$-WNo%{`B6z^;({t|k9xb1L-PP9v+6L7HTF8&F95vUEKe zNNoFxqG@m{+6@)^P$(h6Co?E4Eh^B(M;~nI<5Q}~n!mT9Tzb)5V==1Qd?Ty-r#gqP zwKU{E@yS2L7EKEFx8ip|ijgXg;P@#12nt?i<{q|N@0E8?KlLB@i8WoE=bFQd?hQMM z(I2LQs`IC{OM)+=>TY;6j*h1|nvsb>8>1`Eu9WtqXz}LRmQ`$L@v|pa9gOUAWd7XT zjxIc+>(V}9t&TqA?fDlQ z5!C5)e>ZCcV+I*Ucn}fiPVqd*Ktlx6bn|OmZU#=UW-1Otn5OBN`!B#7aeRjcu=@)F z=~vrbMqyfMU+~WS=EMz=k?SY0FD!TN^F6z^vv$9z0eL&w=Xg!DinglVtc3XKE&2Kt ztm>tUtmcNi_2%A02Y(Y)R8gv1n2JVIqyobk@V&fJ@vQ>>-FeVtiqz&=IcTzTP}J{e zMZN?oen}GJB_CR%_oV1V0-q;6SI)p>^)AojldgWwrZB@pHu%fZyoX`v-r-Zv(%v(C z?+}e`DR!05HD{`k6(6!a%zqqerJT>(&X0XRd|me?{K3JS+i~)U{vhcqes+u6gZ1X? zv9oT*dFTx4;y0wJ^u}Q)$d;&uJL$Fq5SS!juCz6}3cCE<0Fve)LG-3G{1&gqw|%sP zxg9vg-rl$b;yUi(EuO=l&N;#&ZuOkee)_X5C)k^--i>R~JpSF!*(}zW?_0#&+4wuG z_7|+{`RjbfU)=m9zVh3$Y|f(2u-?D0(Kq<~v7b%mYu9t@g00N|0Bbnq;{pA4jOL%5 z;io?aFBe&FnZ7Vpfb8QC8Rdh6h^x1b3IB{JC`v+zsTS!#RKia0Q`fJ)dnsiR^In?8 zAM=Zb3H&SG{mvIG!(w5Bj=wmoF1ony^aJL`iYKu*#m@`J^9S)C}ND->h8$Y#oX<5`#5T4QzF+0}!Rsfz!j5 zrGfX|DQgx-KD-n*khNrIfBT6K#v?Qj`RJ<*kHj$6|LE*h*M_v5_w(AQk9M_bQ}_5^ zrixFn3~qhJ|KhO^eqB+ZR+!gHaE|f$Y5~ zA6ut1ZP;$b!IybZ9v(fNSKjBe4H9av5FRVfzB&s(mQDKm7<$%GI2gJnyYEBb`NYN8t+aX@X(4CO94Pv zk-&&6(@0$mu-!NIt)eFjm5O4hmy%hDdAC{IVR1qJu2_#wFJ3NMKa$Y^Hq`cDw#>d^K;#AHLvo zF0j@^Ph^ZbGcx)3go&qzgEqQ%h4&hemr#x>VMJlbGW(^6|#i_L?_zn%S=H@n0J z-T#%vp6|XknRhpvRek%FQ#+@fpEC8z=;-My+p2oAna3t=rwlYf4Bh_J2SDi?ar<+J zxcw>NyX{_G%ba~6iLVKc?8&BE~62%XSvQ3=OwPKn=Tf$n>8o3n8HIdB;4Dx&=& z-QW4`T`oKD%w@y!@jJnnuqyd*6EfUv{jOx~bgK!!rMq^}WFHs2Urc(x9OHY%=ZEn3O&fx?6E zF0_p}i3m09A7HdyU67@(ech?bwYFfvi3O_)3f;>yI9FiBO45z**v}1vq0nB6Ri9)taSva4cA~!LHED|2vv^$ zYqOoKI;+IC!&x#EDM>hpy(nqfiv_0Wj$_|RGxgC`rM#g~e^@q-_o^sxq1UHV(ibSrTJSSgHOyV;k@9n=-^dCW>`qZTIqn-~PZq+B@R>MD@Mt zf47-EFn&gx&RIPZX1CQ29Q*lAZ+_~HfOmr`)NfFEI3LM>tay`eJARP0yIyf2-}9!a z8EpqgGuP5Ye>t;E0YRc8zYFbbdzUti(o*nheTpoqc9t(&e1+ zc5%&HML`e+i9*4(v?;i;ICuz4(qIv!gVQK5BHnKf3=~6U#YBsWCj_~i#Y`+i<%DeBjlTNpS^+Y;7Ieo`)-N9|h zH!ixs0--lg1kXj)&sRp*V`0UxU z7DP=6|K^(sGpDAf&UJw+W*77O@B$0o(|XP~3)HHO5@$uuIG@yb^sW&dBL=r_8WFIr z(f+7b&EwjI=Uh%2cqY4b=Ab4ZZa9ehK@b-#`Kbi?C?uElh1~?2Xgz|TzC@mge`7iN z)N=!Y-g+-8h8aA^symay7O?FrCXHpyQaeQLSbk%Av%UOs_Z?9qhcPxVt=Hn7eCW_q zOj*eM*dW$4^Gd}57g^vH&i@?PIwY$4DjT_~w!1rMjJ~iP4)P;0KV278s=9$N+B&R^pb@YKX`X%g=_n(1bovZ=@|`_`?ecI}i*CO5$m|fB+QUMtB6# z5mz5nY*#J3&y-dZM<#!AZTp%pLYpW`MEiliTkFPV_eRe9ki3N^9iH47acpl~?@S-> z2FJ#X`73F4=%||2Ta_)}h?O`t<+G2E3{wvBRZzyXcf6{GnA{#jQTj!0^&aJ}c{m02 z#ovpb8fLH6_5O-kobP-7&~TROu7m9YozN^%HqCNem`)N-Y!`4$M~q-^VfY#(pOMK^ z7(N?YA(>7RPVAL<7U&)e_7-kG2gz#rc=FpX!kM0?PqY^%gTynJy_b*Q3BH8Y%ZCfo zNw#0fdJ5Ai7h7{(AvOf}cbNevhS6zsMSYq-#-cvWA1Qrmr`(P?f4oI~H-FSceK&vX z_|!2*JLY^26!mF79|$vvvGaHfa6=eA^r=SN5WFtxJ0Gx%!swARN5+lhY?kh<4Nl*( z!|haxPjPtp4mLq|6BRh|5DQ0~-YAHHbC#}TjX(R8HQK}D+Gcd0wScij*|3n_#{hn z_W9;d``mfNYd-t2_h(o?XRQy_aWH0ZK#lTISo zr_j=}ZUQ`$GB<7GP3FJTc&$4CpD4lrqCHBsjuY?^fbVj7-`?J3IsaAkI}`9VLXaf> zEE}FlfFCS|n7(biv-n@g6jF4ptv_oNMXx|{bi98Xz|??RDD+2xbiEgzRimvMdt+6` ze3{``*5x2>-rTTCyQH4|>J4jljFoN0uKNVnFaCzfzf%7fy_Ei}_ym5o+^p%!UA8_a zI>J<*g}Hki-=9~jdzi}jxnd>DYaJ~u&B^F{ndP?OAo}(armiZ@b75COu&Dwt-H>Ar zgN(KbJQq*f1Z`8K$l6@i;Cj*}{`h|va?TH2y=UE( zkC@VJVc#!XhJ<%CbIVU`pYd~kxL%E}O$Ww5jJvpW&HW5N^ZC2s^OG`GRr9O0b!?Y? z8(MtOV&q%3TY5yd<6pB{tS;8wUh6|+CIp{Sv=C1Z#R?REY`2DF*$$jQ20ZVqEab6j zRkob@wGH+S@d`>0zwjIMFh9#0ifa)+G-*+umW8X=XOFO?Hd;<=uCPaN=!&x6^Tf09 z@<^iu)GsT2PXWTTgK6*OzHuvScbA06FybG+ZtHyS5*O&(u|1y39rH#Qhs z=4r#A$E^LWLeCH3vw8tfl1i~1-TnH+dRNVa%GBqH(1A&k^My>5$MUP z`JO}Rd}WxyU2HRJe$S^-zphDVCNj2xpTBjFAKHT0;k%h>gT@90jZaLT+DPBi{JHX# z_opoEm|L&D75vu!IJx2(e(K;k*7d3Ph~ufL$47W4?j1UGuaeAnvrdrgzA%M972<+C zR3R+35S>^{lpAaK6k*CQ*-D?r)!u37Q@z>P&Nu#qkRbme?`o446-t=+neuLb=Yg|$ zC`I5x{yMx?_LH8!B5ZythDZeibhxOts!mkx*x4P_fTq>h^y=+uL~P@K9)>@3FK(hV zh#Ove=GUXgQ3ElZce~)v_6DjHFY6W^K??d+K4~O04h2bDp*1Xy(9KF zYAe(=LCIZ|CT$)8C&et_unOFSAW1Cw@HJo%Db&L7*}8#F?MX39JPYuOov>jF;h&?s z?1U%3{UU_L9PNc-mU!mcZ5uIGawAFj65an}?mghDI-Wn!yXTyH?geasfH5{ez}}Ez zqc;_$NEZR6DWVh=QBY8^qS$K?QHi}96^$*%9&0SIYmBjLjESOf@AAHT&ZTIQ|1baY z-si!TesU z`Xvcw8O~RVF}2{c6<#jSuMI~M%yRi`#Y6~ZqS%itT`c()*xA*vE898CV{P7|CDV_M z>>ZSlg$+#w+b%8b?IdHki6MW52=}j%-UkvN(>(f&{z9`}3=NtWPMY-`(4HGg#OK#x zedN3+g?8%!Re1NA8{g5AXRGMlM|rbt~;;TKVs3V{BhT3=bldYcl$UC zGYrX!)YfY7nKZ|$&6W=4rV?HPoUC9{b>eX7Xl-K-%rN`-brrPe+k+&&f<$Xc?4fVz zS6Z4+@;m32#u5@+noDn1{ng0y0tvohBl|Iyrn z`?KoQnRUNl?xXy=m|&ZouI{q5~R(_Snau;;*TzaA?~&n9i4=ilaU zCF#V5yIz)xc3)IJ(u+pBjo9_Et~g&}{@x@V9ctfxh^SafKfE1v z_3e>8TnnoIh;BQulSC8oDw(xm1I@Tf%jxQ!2T0%}@?QI3&yf-5=8Sqv)Zju1PidB} zlxHcQAuT{Q`lhhj>d^Stv_gC_U(ky)er^aY$8Dr%z8uHM$+1{k8 zfB*2jx}X2>yt=>A^D6vn;8*wiAL>{4{~zjC(}6v&?r#nJYI^*`^J=-6Z<3@%o2)t9UH$)#J&emQ3$_E?X{_>Q$B3v?o1>^vcr~i)S%Uq#(4GHq20o zwMIJDV6z448@@tBmigb*BU7au&- zIVH1~azXj`b58rV>D<1b5lEfdk_;7i50~FbysMyoiH(RM#(fqDW#Y! zpQNo4^hGOsQ+rb7o`m=dOkd^t$*x7WSMGW++Ni!~#s1^Oreq)8@$?zpz7L)fYtu{f zwJ)%)_A7|(lQE$EW_4Hk+}^eU&y9R>XwHf)^zhHm>H0&*kwVREBG;B{Bye2AkBt%z z=S|rcV?S*9kQ2W^d~Sj}$)BJ{0~|l$L=k=lEd*xrkS$`&<3Y05ncG$SglyvGk|w0j zfv;zMpO{bI9c7OG;qmd|_-)`auycAWMTF^%Qxn!T)gQJle#*f(;vC#3Fsj_jtCP1s zQyUZt;Urqbsw62OhQdkhWD1EwcUJhJ95NJdl&PiwJA`1dcbEt_Hx$Zf771eSs-|c= za6i%sat$bdk=hLmCb*%L%`zx|mn-W-2T)YP4;>J@P$!kK)Jrp_&DuKg%&Wsz0*gP) zRU+3V!`8XAnO-Yl#cP_R37*Kg(;*EEU8ob^u*K}dchkuGia^ava*P%53ESRN=5y)= zM;Bm@r3XpWf@?S*w2>!GrUWibAP9BrFZe)`SnM)ocV5JeQPsjA6AwB6RzB%`^ zc0Cs^eVS{2=EC6di4PduiyGJBCDOi3xvgM_gZIyY@fqkLd zR@(sCI6}E(rcJhQ8Aw+0lM{y)IL5i%y&F3`B`SIXN8`D*#AM4sVz{t)Ui6B{_4M|k zQhIkY+3g!Pc%b8ePQ)zIvww83$E>p>haHS}{r1`R@qbO-1+P$h+btA!AIKoCsKIB37IHjVJIfOEZ zLjkdl?DJAhOdNM+k~QQ{?a!PQPBW*j!m7B13(|_3a?V`KTsV=?O{!_EXex1yq?gF& z@G#z4*Ye>c<1nGgnDe>MSM3a&3YY7o@1|%cRW;y4H*w~q6LUP8@$q;!(xmC|{C@FE zg0%PLCN*dduyuP1 z(rl&1^c@Y#yoO|dwfiEh(aZp#2pr9L0T5q1^ro5~e7fc-*O6>wM0p3Iw4^FJwHT8K zZdq(MR~sP6+Mj990A>uF;3Ov<-!krJ?#!a4KWA(2*R`4Q3t=82&Zaif>zj8Viv-$b zo?5_dKoI;++V(s8d}k24-5=vOQ0n$csoR|Jp|Vga|4vm>tDIN%t=uNd7uAnwxavkB zMhRlhCI`VsuqVaDL%R|82<;>8gXu8k60(5&B(!E-sQEB*Yxn9|OYqOg?TX)nUu609 zP@%?3zEnv8RN?0>kBDv-MI%aQ;}3$mJLSBW6QI~Y(^1CTj9 zAQp|ei&lYEJ=AXG_f>;ta*OzBKWl&iLIOqxs6L1;`ze_;sGa@ajsdI!s(eWARyf8w zHnj0-8IU-$8oCoWp~Ym?HM4%0rD zQUO5Jf-AscO12o{r2!Jj%nc)ysnn1r3Fs4(uEI{p>16GDHKOGtPHX}v1$Ll@*WpYd zEBwqMgl9e>(yC<&(Z`O{%bx>e=eZjqtW- z@92<?ZmhC~RY&a!CMFAAK`a`3EmqeY0kC+~yCz z)P?{r*^giW5WN;8c`Z@_5;K*UBh;C#qY?`8At7>7kRM2A60(Bc`$5|V0G)^91Z~!E zYA3X!cmF8*4TiZ2zvQ6sG>l~urda@h@l|Fz0+7-vsSYGtyv@mWrc7Q%A zCOJJ*e8*0;bC^mTv_A{S9An)lTwEG>k*>aFb(z#qK`n*%IRwGVMqz5$` zEO!xb@Y@8vlo_JUn9SJXX{d6$fd1Mov&PNmh#Ec#$5TqZl-k+DDaoOo*X&d6!KIg;{yc6n=grRPG3-y(Yb zmjR`_7Iz{I+9ghnNMGDn3vOgO9W($C3cHElq)}^$cEA)?O4$g_%;Rj8bWn=Y?xAW3 zP5BMl%R&atXgWP7eUZ15R^!D*FL>*ZW5WaX|Mf$pkR{q@^7hD;GftaR+M=|<| z{r}kiy+`rCawvu^L|g@8U}=T(nXNICRW)I;=A8&j*mb4#vJ=Uj`ZZ}}VA;&ay(I#2 zTVO%m!<|{S%U54bckt@iBuxWwaVqY@I*vUkFP+yxxgt6y7GcRTYsQ;Fbzydv+5wO_ zrc*drOBw8q%=nNJQoqjL!lVWLINOK}_tI$(N(K@n{#{xAfzCg#Bx_Z}cBYP88xinT z>V2xER|z>oVjesq!H4-ug}&mwLfk!WT92V_4uL(|)^FD=Zo$c1SWuTPBW>>{&h&Ii z@8cQe_NAjUsq@PLVp>>8pYH?OI0zN%NqRLV-(a9^Q^O4i8u)A?vX}wBPKp0Zx|Joj ze0GL(*n#AhhV#e7E({D<6x(}DzdB@r{$_fWMT$OQIV@k0gfk0h*-j~k<-!ZeJ<|hs z&_5SLHNq+(T1ri`Z4=v~U<0pY@u+M^qGT79u+Jj|gWbjz(JQgbJg7a=kOuKC6?KsE zp^h5)%I*n|H5^YX8j~g!X9auHvNAfltPG=o9kI9y=cjBf08Q3DyY}@-Gxd7X>cGlBHwG$k)2&Ogt8;u0 z1ectJa@0+;b>=>oEWO?dcKq<~N&PvYt8Pp(2u|qGZ4_8~7%o#is*Yr>yP`v%Y`hSd zW@~4pVE&785^$A?kOVh_-^y&U0c#Y{INJT`Q$pGi-ZiUB>o0qCi5WqF+>aGK;Kl!f zZN^l3Dbd0S(qSSYq^@BddPG!3M@_^cxE#lQb0o5|oUzh*tP(a6DP9JaMO>4>V@*Vn zA>SOdOeDL1L2ikPylZ{P+JZA9CTM!{$2C3K_;G5k;vhXseV*4hAYQXbBVjC1{F$`c zNN=-WV4ulQDt)ZSEO>kl7T*X5n%;EEbGTsdhO6jdao#t#p_@I1JR?mx}ju?y2{*vun#ZEDcF{D z5iQ2-un*@Na>$GFb!_>m0Acosig^K|UvaCn`C<7N($X&Ehs_&dwPpl;`4tWbMm&0i zz)o>BF;Y6;zD)-`c)*{eo0_if<=^{oQ?)^a1g0(XC1llJiX`?Hl_YpG zA!)yV*MN(A)VE?sUs0gXiRniT-=REcUgWIpBzif0{QXu+x3m$p#zu(gdkl9b2?OzC z_1uy8i&2~~N>8Ezg3ZSf;zE4L$4ZgVViH3t*-x;?uF8jXZ%HH8SK+4^405u8(uJfl zn~0=HKu4;{4vyUG?AVCyT#;v5atV~*x3SWBuC@?Zzyz$A@`_RAFKEZTp$|#3jM=?< z&CZxZ{Wzy`60mtQ-L7|$PSo>E>=`zrlYeK@%74KakK{w;1t;mtHD8jYhJop>F2h|M z{H^GljANrWkTxesUDBmul4c$ z-%)yIAIWNJ@7t=sTU!UqIz0?~4xX1V@$dxa(KnX0@Ct9&(z$gH2Q&R%O@?oqwEWiS z$3N0bq!aNeU*zI!qEgahdgjAVOb3Z~U_Vo&BQdkoSIHxpTn^wp{3TI~iFS3-2P4(% z4@LqmQ0Ej`OFRhiSTde2zzl2ro($YaNGbhV^tp3~{&?q(HjhL`FAWJ>70cND06p>j zQToFk3{D8z{8rayJt@}}njowhoK3^3YpBPmg%5$;4H}^8S@9x_?&`z$(E5@t{4}Ks z$^V9NA6aB##jX_k6}N*=N7|f4M65_acOdBDhr-q;rW@CEN6PNUOnNXE<6}_&U4s6K zcL|6F8)Jx=q!}dFkc=U-wLepDn%{uVq~6+}m8Z~;tUGu3!{uqpx#c633qB~6J<3l2 zyrB3Vf_IHHTiN-ewNCVYDt4i&>Rg3INydY=;+k>`-D;w|y(CNeQY0PAx z*bZH)M9`HRy2N5hn@MTMKecdUMUiYdY=MymtA)Y_N;1?|U5<8hD6m3@F=GYlYp{Zt zU%y6Lujm@mg|4?l4x`cB+K-1sclOadqU-4@Lb^vel928_37MOfwNxbg4OY?9;=!n} zEw8gPew^bzF4iitRg9BopmqgmuI)k^X{?C)vFGHWO@mrxIEF+;jr#6=2KwI~>|~|u zKU-a^SQzXi&2*d`Qp-JkU=GUf>{&f91y}EMP(J6I_7hZl# zCtR+Cdx*zJGMzLih%ePHLtl|GMfIJ8nTqw>72Ise96HQvBPb1uIOT$*ZoIK^_SD}kX_p49#8czR(o;m?{k${gH9DCKFh8JRbg(Je(5B>9`m7M<4ip%}uJAZlIF(@av9b)32JfpW~wH}Zi=urHAV@S;!9Jb{nMm!FJ zrq=9s#oSinL*zMVs9d7y#9z{MqDA5Y?Hn4UEV)W^&oyB`1WRO%`08W4_!Yp!Yme4D z0>=4L;S4Vug{L9ANJ5RN5EXKv3(h zBBvI%WQ}t{iqM zQg(ml*|kg7k0^S+J%55n=l+EF+4kv4#L=5cw`zN!fJgcki zgcSI<@l8nJ*QC{{$fLye8!M0YB+xmikiMV=KZVg!>hVyo$%pp|hn9w2-@5%=Xijbj z-8Ojc+!#VS+d0~Xxv4V#JgQ88Mfz->aF;$Pv0&bDn@Gnq-kx(a`;7WF#Ul0Gf-Rm8tDI&o(v&I{q<}*+{yIS`H5piy-GN@hyC4?37riVNQr3 zEUwooW98t*-*s+aGiCeGyxWs#XpcCj(HY`{52IDHHMcZg{OL)f?NY-vuA*X>LxkP< zc&Op&l-|%d@@GPmJ5K{bCt>DF#~!jS#u8gPm^Oh1Q^*X>nN)(7BYC1I%ufoPk*O5l*!MZ(+MwLXdZ~yG{w-~N1c5@Sb{f>6g;HE zNcy$K3$LUmzxqzwMhz#|-9o<#e2^yv=@o`QAXCc`PCf7D9U4y`U7(|q%PtP8&T*XX=E=y*l9VV%n3h^Fh6-#+*F8gU=OU%?IDa%v!nXv9~Um7G?3$--o z-AXj)_^uC0hh;JAhvSTNH9h&5dXd3<)&(vdN*5E?@ce+4^XNW#+e@JFOnbE=bI+(o z4VB|>FI`)?hz!Xe9Xz=Yv2q?AkTL@l+lAe^jE=n5w8R>Lz$SJG!u~>KkN9Ibx>5FlwuZrYJGIb| zL?pKx@{HIQ(lvDLOL}4@wytxb$eDxLRPrK)b88})Q@=X2Evb;SqIh}>%AqS>T>vQ@ z0UV(sk1|BQTV~mn@wtA7c<&wf^a{Pk zM-oSFZN$9%fQTvHz6C?hkXCEHqOW(3en?*(rMK(RBFqfd5)@0(T8UF_#%i31z4ib9 zGfZ(T8?#sra_UL30n9m4O6IO-~ zEKZEr*pcQ879MFP+10VXHGMy^*g(j4bbXUTVzhJYqJe9Zl1m~<9~V!-krrtyedD&# z%O@_RUXF`MT8f|_05U4AGop!8TuUYA74OVE+tJCucB`@4j)fT=(T~9JU7$pM=#NX z`$%w1NkYP!7{|CNePT<7V917J+|?i&rqL*DJTTa#j1SOQDpzW@iSt;VgXJF+AtGmh zxQC=U)DcREG`v#yDh$olfuT?{;77x@nl;^#T*gd|t9I{9yTAsVU-N`6KR4#=gpzXy zi08$N#Pa|l7glGT8B54Hy8KDviqMD^aj}KrA%zJc(|mpN!a}F@^_|vaDG}G@{xr8? zJ#+1bRUhZh`&hJ5V_vV}+@EsS(ekClsqfU#@R@#oGs8ot_T?8MxtjktPtS4w0oh*Q zk2z8on#u#>%8uTR8|Z>?WKK0QMxZ`3fZD8$tt`r(UcdG%l}cns;q*`>qXP$T5=as)}QlZa zwFDTaC9Ab2!clEp9g;80uktIh^g6!+cMGrs+|}TZk?5-wqcuS1^2pTa8V^J(-El+J z3bLl#=y13Me?#kA2edgvTCP7#zqxjvZazmg1TKh)o*yt^{-Brzf!y-s>yM^zhqbNL zbvMzgC$7-r+e?W3Pl-hl5k>KdE5pNAqW!6uih-D7y7qNT6Hc?x6BgWx7>LiBpL&?| zy?%}aLqLoUqt>(^c>_ES(>v=(ap3%@=mi4;7e+_T4^&27Aw9RRrDyLjJhX1yE&aNi zNV}`7<-!CB3oKrFl~+U!G75>xdSy`aaEZ7==?OJYrL0dN(ecJ@U|1r((V(}i2tE=o zOW%#m*>~L{0tnd33!1{?E6!|n~D}hrI~cA(ARh$cemkt zqh{&$mh6n>n-ZKLUedsm12-{bHFt9;${QqmD~05r<@?7;kv7NutXPqam3T z*%_P^_A0x@nlg7W93I1@3OR@<>1J2yxvyikq?0-Qe1=5y4D0NZ>5UYZ9-8a^tCI3H z8%lngG3J&ne3s1hXbtoAexFD(-3*CRxY#C9m&OETPKT&ob;nHg;u?z0%lD9Cp zVjVwi;HjVU7QfD)dV9+J7lY^(r%63}k9LpUr&)fGUb*jWY+)Q32wu8T`Iy@eUY5P$ zaP^6?1v}AYr$_ZTZoLg%(CU6HGO-rKM$aR}9eLf#6O=2;9?yv#GJEzAw-K(O4Pc0U zKr3ryXQs?3JM)kR47RqM+bAA>tDnzi9NQd_QE z!R5}5W3b(mb+GlELH8=yb~@O@D<3PjVGdYIZoqbsG&Sm)lv#_pOu;2UWbVNxeCD)*9hdrpxq+xpFl@{8|G_QcI0WKNviVJ^X9`=#8jtwM&m zdJc8$mJ)cHC?`$%uzk#)e8-Ln(}q}Rb8FKAcp2Q2@WnAKXkjPfXbv<&x%yeH^3JRr zG@7ivs5~Jmb^XvGTzy!{&ZwB7T3SKTM?2##NF|CXj${j*(8y^_P_NjkevoCtK6-8M zNqT-;zx+XRKMFXmATD|?N8Cs7Q!Tb#&atxz9qQ(p*sWV?;AvWpe(XR$96H5MA8b*J z?%;+h`mz9Ss3o|kfW>S}8XD7xYm*oh#*PuJ^9hxEw~3D!7VDkn`WcP)&JG)5Lyv}$ zE_FM6O6L|cPtUXMRh-yon1hbYQ_Hu1QyyX;oe|!GZ=<p#=KWtE%H2QoxspdcVXy~Wy-xeQtq7{KG=r1gwYFi zm6c%3{QM#ABV4q(EoPja*R|*B#NI$QH2}2Com#$aPkFFIbVgVU%};!51tDtcF4(HA zC4bWfM#ko%KC><2B$DHZ`qX){wEwg?$HD7|(qZRM@piDCrW1o$gz{{mi+8=EP3e$4 zV!)+C*z}|Bd}b9OjyJb&jI9Za3o|m}Gf$nT!*qbiQU-|TYYXVRSX}r$fF$pFMSdYG z^XNNm0WfL>+SW2);Bg)0yZ`4m1MqIm)6)794Rn-XVL0&xP@SG^~}A5aUvD<_iras}=y zVoT2}^Q)`ziclT;4Ph~WU337hjDsx-mOWk8HCUzz%i#ejjO*kCNL}d=han%jbz0(n zx-=uJo#QgxZLL~L_a{!<`ZH;~%&}cohNclo-Va(Z!pkX)Jz21F->*66Gb8&?zD4l122!RiL<3?@uV(gp6~ zn9a5(n8em{Iyi=n(0a@C#QoZ35;~-@=HoK${>159ex{Elboi%qIqw8??lE+TbFZ8j z(y|}W5uP%1?#k%+l9q#WdTHy@T#^f#Zqn}3|5a56*JOC>xUwvDI6PD^BZ|U^H|5xb z!0rl)E5d#ey=BDaGpyOR(B*Z$AlQLhw?xt~j#HT$35c`8D~Je|lyp>*=6y3x`GkIl zM1LE;*+U{p8F3?RaGvOmqd&JfkJgNy*VmvJe~!Ad%RbimCxkGLpYP0(9&>Y2z6t=eW!ha^Nk8%c z4sadFOC)*QhRbHW>AO;T?$@b$dR-Gedk(f!sbcn$Iy<%zZbw`_Lkr(_shUonzT-9D zA0$oBOzggtzM%_$dPNt0$Tjb1O7FFFw&7<)?VpjkJ~V1$deWgW&|cJ)^)O4{;uhK- zkjOtkR`;u{hv~*jddelF4c>8*MTi*xn9S-c#)n&thA^9N5}njqEDvDOLW(Y)>P_&M|jl2jp#I zqYl|5U;|;s69=Ny{TA%D(~D21OnFK=Z{JEfJTC?IYcuPoDgop4zvMo_vYajFDg8&e=>1 zSFNJIZQe*<7YpP#X>$DI4btQ&eR}*jX>^o-g97>pvd@y96Rg!SAP~+$;9_R2O{Vj? zMe3OM$;dEB#R+r@`*1xp zo_e{O-nbL#FYR#qw8we|-NJ>$U=N`*mCRd420TR?z;C`GVgU{T6tq;B zSCI-p4*}?PH6W>e)$u>Gnj4apWF7#}J;Y$)Li%ox)*FzB52qkL1+*M@+>{qckcgt3 z6bp@oqpT+bP_vvlDGEn54;UBlp+cgtvAUDs0%4<$3lw~)GFRA$$vprgv=4lN5|vyM zR8wN0kltaV$lkfe`3RoMQq&4X$)!(9!sHSHI6#O&4%e0RZS}KXum6UU-LSyATyfcWEpg&(JbGZn0C=MvGnlcp+`nie8S3Vh@2OYhY zxi=z@Zt9j2b~NJ_-t||u=IUcM;ayir!D9It?6~@HUOm_aDHm@<9@*F<#qVg&Ep#Fn zFE^C-Pf1EPC1wln$w6rOlM%ecbY>tY3{Zzc z_a7#e4^{M>FKvyRAQT`;N@c}I)@py+ z8doH;-E2}NMT109B0T2UexLCJBy))mnY=-G%!9%4Ivxs#CFIj3B-1&633-&O>Q$H9 zD5Y|AE?p?Ddaf7sm*;qoJ!kKUZ}9t*#$BOgBlEz$4P-KF{*%Ttq)HioX`GeYD5Y`$ z&5(e@%q(o>SbBceBPi}~qDWDsu$3R6*$SX|Cq}YSkp!_L)w^gD2DWQ3;c0(%wI0G(jqB4szmIl$_a(EmK zgQ!*Cm1X#zBYj_u_rFz^;r+?XK}4~eo6jv2QZOm9f#+scBG`i%h<{`yf1nw+Y;!Vs8a2~USCew*9Q!k=Pcevz+2uMAr-O!LVthgikDmQ>bW=Whei-WZR z<$xu+Nagubc|KQ&-vsxWnu!xcKZX`;eTKxr#D*AZmARUpb?frSxhW)uOUB)B6HL08 zAOd#yr!`zrL1kx#pW*~8;>YXO)%1kOoT6Ps7LzHc0ws2_mfc>iRrEtMk(g32D$21% z@JvKwN<&ta=>TXm8f~ubhmF|-5<{|S=>vdxKubTbKNjm1aJ53yR)z$~qV3gaJ6f5G z%5*Dli!dV}sWS1Qc?yo`6MHcQ_7|o{$`Y0>dHTxu@~@Qf`~?zAS89i{p9-GnD_q1B zoN~%KCln)M&a=1i5n7RS?GZZXj$Bc+E^o{i)T+!WJ}OegNnl#3p?F<2UY{grW-4c- zC})&sA{j#hT>*`NaJk|*{xM#aFcQa=&)^@^6(j-OeWZvGmxy(AVDv=g+=^h_RJx@i zh?WbRi3-aq(W?liABjrXOwk;Kw5d2w^hb^QIwyXFLe$I?Ov;)stSRnbd_h6HWJwz7 zHa9LdKcBx#w~_&xZgOdSfe`LC(jDco@dbb?4`3$=BZxRQ1dJl=BMksXZw^V2$?t*y zi+yw*uhsDA%XP&mu|&BFC-pdn9RSl7v!2O*6y}Ixu!OZnI!81i|HZSP$nfKn<5$PJ zMt1Z?sFD{q@>E{b`VnqE9eqRS{{P}3KmPFBF?|B;I;1C;mQC;p>uMc8Qak#uWnk>f zq+Hx6jFwyqtfkn1YBX6&3muwfKZ!$6O^@0#qPu@r??^=Ssgh1ljomuTsgEtba;*7k zU!?Aft#1&mU}0<=@2uglQaYbd!+7KtTdGD?=S-lOC) zcTC)UC^-#!{K^Sf7lL&3fNzKr6je^Hh!s}SUxlETFOz8#f~{I5rH7PkHw^e}y5OrNPNMrb$lqcn543EC;(-$#6|G7vhEA!-^cZLF*ulu(VV%#=!;M{|q6 zpa-XsL3Hgj;_(Hk+n9Jvqa`GA8a>z;F(XrMzTC9w`&v2Go)V1Lnj>q>}0(JJ~5QE7h` z72!s$Mcl*gBl`Cr*)8AR&Y`1r3kjy1NwDUR&#FnFuB;*kYu6G3?APC|T}$7s0)3xU zKG18eZUI*bTPeG$%xoUn7SXu^8pwHfv@T9wR`uirMK4YSAH>ZWa|!=)=jO|bquqDhGY zX=2sfT2;qzd}#bQm5gl-<_d`xsLG{VIaeZU@Nj{kaDcZ+iIk z^SXU|$daI-B}4vZ;K2aPgIy-OhAj7+9WgFqR)4TSuJTLqyjrP4;jaiNtCUkSOIiDq zxWmB~o6ye?Ah-7I-?Oi1vwFLMpLcjz_&_h;I@|Qu{1p#n>7hfV^Yh(ZhL21g;p|pW zfTguia1hef^`J2{m$;BA!At>_w!5FZ*KpHk+|SFqKSTc6J<7o$s(ZI6N5?3^Ve0)E z)9+86dLM60B~2W4<=x=Dk4B#GrNUU%8+aT@uJYh%oOK}C6Wdkeh=ujgn59!^M;+|& zLq6qYdssdqASg2N-S4ON|5C3}MSWwZ^W)R}Lb_%6H)`?q{0;lw{3#TZs{;mkxf$i} zJ93En_n7F~VI(-VY3X6e)7cevy_ENbMH>1FRv#R+@3%TiE~oc>eX3E znd{a8dX%6b8}$Bwgoizq9Z1#Q;}0htm$}9ccJ~-EL{JDW6-R|#QEsj=(QfX8pf@C; ztc}_iY%l35V0nrD?D_$TPGmW5#c5H;vS?J%F?u<|FJP!RSvyg1*%LnWLaDYpH$O5q z2^e5AE5ZQ*=7t6U1lwn|DU|Kal-@Ko<7awi)hgofGbh3bDwb+o))UJErNnY0)1Tm1 zC*K9ywi*cPRa-y_zHl(0a^la#0Soz=pS4aTi2J}FB|aPJ-O>Z}?s_c*ngnJU9isdl zYKofzmQ2@LOF9dlVh8H*5(nkwqiKY}>kftyLdD~F)d8+v4!S!4c$O5z%9Z`(1+AD)X3+bO5O6ij#k~w{hZ=O@IHJ#LY)NmI) z)qu1vzJrj*M%b`}C>OpJEc$e(yIOj7QmR9PNdM*@cB0TPvAJ`nPW`(z*lFaEylO;H zx_e!rPicD5{joF3)<-9ZoNJ%1eeI058oQ4y9vPJGQIGGlchtNGlk?wdI$F4PZ?x2~ zVUIR;Q7sqOGwuc^NK=&S$0)CZ+g%he)Vg9iF%iy=%yfRBOs`==-r5Fzla(BH63NSs zHy{Y2=j2_VsML9ps_>=+hfNXg)x?V>PG*)yhL)z$2BK6#Kg)Zd>N7eZJPA+=D}XW9lx+LF>Be#ez?yD(s)bm&szp-7kH()`}DNz zX=di#u$!M}nukZ4rm z$(WQG?6I+6ho3qvh`GV#j6LONG2_UXpp~KC=_m7unhg7v?4Wxe-=s&kug2X-^+)8p zyG~6=n&n~SJ65@dS(N=6lcq3F`dGL zo6(o_mVO=5ThvcX7W4&NGfx1P3g;Qkbdi{bMJB9@CFFj`_Mr3$ei2C)Lo2$FK zd9`Uef2~{L3~j{Zo)K3&^l3X>-?*-zcIe*K7C5EyuZ2XtAn2QLg<~s;95Ny;$CAjf zG=Y$iR!Ip-F_m;w8@r^2rym)WdN?B}&BM4Z89u~gV8?`^Zjl`Yg))fNGxzT{`q|Dc zFUC3aGhfIl@>cYjG$6akBL~ZmJO5Is4;tVgS*Cz&eySz3(H1Vq6FHYvkH(LGwDQtN zHT|Vdh_BNiTiZxy_dy1vh2DGq<+Rs@g|DaG-sT?Wo;z^RWOu*b+i#;g-sp}~Rd-xL zM_KSyDH&$a7>TS7=qA75L~qn@;F17)C?W6{G>zB-OTN(;w;T{dIpP1d*fzZ#>XF9HwfxJRmupx3mN(+be*Z&`K^^VGZ1*Ln zn|5i_@T(2zUlT#W-P2o&X#!6~SF~Xa#|#xYq;%6ZIqBpnLm@?-`l6biqwRx|4*qjg>4E{z@;nTR>-lV#d@`*;SBpzx1Xl zF*O_%6Fjzl1NXF$QAbCCbP;JT_3M*%Q=iO^9i9~D5!pVGOJ6;zletg8plvO@x+LA% zy#2}Oo_@`XD&9@n9Lh~A$eFm*XQE%$0-aLQ8N*hklofPiGt zO?y}wq%8|hK6gMno(qt*5?L=|_O1V@7hy^Y>D77>S9yWUCF}Hdqas8l&QTkRHWn6^ zHZommpKuH#9e+H^L$(>1sJ+@RbBl{}b00DUk-vBMtrOnzq^F%QcaXbVbhMjW6cAzh z)&O*^Dgz$2QwRZNf*(1q37Oa)VRKQ`%Zs){L{c!lbCw)Ynv(=F4tFrAltJZN$gUgO z44Fn`IRNQwcT7*y!((NT{7m%2c*=8Fr_z~yM#O3r&%`hZ8lL&W;jv2_*D+t8l3z+M z59;OpwHH`%3sOn`Q^ZnWBKk$-Hg@Zu(&Gm#^ow=iy+-bJ=?YU#rSUhnrmP2xsz5xyp1pR%#kKmuHYoKheJrF z7Jn{(aL!acktDGS0d&>C$TM|7#*sA4qR=$A2K7lRCV()7It=uH1mbczV~c+F<6dlm z2m-WsG44Twi5#|THy z{ROMXU!8WotRZP#H`2>9yqDAN6$}12dG5Y=%f!a3V{41n^{ksr%}iJkvUy>{?%lhz zYSFH4%VvwmEZMplB(vb(a+}qE)`&C*mdk*x0@#+p3Cs>#_{jyNV~18It@JuI?%VCF zV@#<1sR{1ZQbm4M*XM-%Rg5e7KZTZ9u&cO|t)W=Kq_{%{jw>VW={*CBqRgd-NymuZ zuH%*8an2gWx1p)$4sg@7+d^Vv0R|ElBPc6YOqVgXHO3Gv!(GSAh|AeJr{M+w0&1PO zV|cllpb%NG7V~P6US+SN5^o8wB>IGOU8KK^bSyNO2j9Ra^yE6juNn2VsoXqmI#-}k zaKp4q0j3Q~O#y~kfpOI$0RX0EkfhRmqzAnQyw4yb_i@sJ-joA%;MFLS0J@!&BM=KX zL!2|5nJpL^o`b19yGva{1>5&8okC6B`wp#JmlmQ2`e%s;O>O@@mu{-}%D91tjnG)z zhPz&|V8U8JN&qYUC}T3f4mv3_wZJTrKz^gmc|U4P?m#$jzUudoc<|Wwhh%KHQuR4m$+c7(T0vW0q{8JF&cqmq?%7YovlU(OBHQTt0t;rx zv6Ca=*!JF><)@cT3n~@5kwu<326o5@iWmVHjczTzGp_g2vAZ~2UU!#F?a;@!gHQj^ zz@xVlcZ>*II^QqPpd6qF@^6I60Nq}qZI$V?%1EPI7G(>n)DkXskfaNjwDiR+o^)4^ z3yqkVlR9V4p!C>);7p$0# zql$+lPp+q5Z_()Fbx|O4FuHdDbdZDC?2tQv1+keZr69Io&hAn+&R9Frc1mI)oO#O| z;;_%#;Qbk5l+5>+=raqT0qFUi^RZ;UNRc$os-Y$+wL7P{^-al z&x>3VdUlI-b&BsLh!goYJ!e1Pc86Hazfov!zkRuX@X&8YPd=IQrgMmW*WfOlg6!S1 zFo0elUIUql2Ix3?4cAFKnm>z7W(xk40dQwmsRRS*X&C6H*hY7k=>$o~tOHfrBWws& zqgxwoFt{<()vM(?H@xuonQJB|Pn&pa_MF?qctKvs&HB=ifjvKr*By?#^++t&OgN^BkU3u8jtwRT&R;_$GxCd~CmQJmm z`&jg9Tz-Amu}+d@_YLR&zHks-J?=x{@SXEOeIOT+)?9A>q*mVE}4fmP=rU zXhGIVB|p}xH9)F$rKW@|mrAbIl!$&(Ti0qzgf>#ibyfm;Gn}14r2m~3NoW6~770ov zYg-7hQmZ#~czC14Nh-NngBjn5+lD^hk}!i-g&}?p%vym*{x7vEGX6hlRY2K-F5ouv zi-67@9Xd&LkJi|AA<*$+z`dvIcr#ionoA}3btP@^j;U1g zKv&WPOe5F&Syz%ki}@d=cOL3W2GS|Qw|q7r|5Eb~SIV=~{ztl!A%N^Fz4L2zt$D&^ zspPS)BpB};kZL{Am5ikGgyB-jQ(Z|AN@ntt0r{D(#0#|=NbfwahBHr?CP99oD~Uj@ zxl*l{x{__$aoj_p9^kyvm9#}kj8yVkS8_p{%e@ks;GN%eCFUq`kxJg^N>K0!dmbnRw#MPN|<1&^cUPng)CUcekxchWx0a4{41Xd?t;Ipk9k@!eGCT=_=y!u zUZI#i*5?)dgVcNZg4CmLU&Li*#>J1%Y(!d<`lJqNN$=nu`Zx5(hBke3p7rg{>^RX@Rj7=X%gxQ|?NYneQpR)+Nt8kLeTWlBI|xUbJ}f&L+kzmSuA zY+B)!Dlr(sCjw(TDG4X*B)qW5sy@A2Goi72s^Z+MF8(7itE0T8!g zx>s#|h-*9LTn10-t5V&9Qc}aa3~gN26V6^wn}{tY`=;(p%>1!I=+Y?LUZGx|(N@jd zbA2A&Yh@QzGBj;#EJGF4Y7F0(W-RT8r-h_=G1}`CFWuHq8j5Ouz#a>`>&V^nL#IAl zT>8$8J~xbZO3&5rP!gT8JwD^w+~AC~VEc$}qAH_7*X+xMCkdBxcvic1OLIK?4q7ub zeP`UAC~xk6eskI zF#l26(wU#2Yt=Avmx5c_OGd_yNAK%dM z1_5&hCN8R{ZZJJKgc%1yf~Pl7*I$${a8AIb=8iVKd@Y<>clRJyobBvgUG43hK}kO# z*i51%J3^FHEZOJmKWbR52JM~~0#Y9QqaibrIgidU=vX{t`R3ws5a;5jCSpoPmqW?YDe6iwtYLgMR)YJ?lnR|D12@NYLxk`*+b9n4*M@bOf&W+)ILrDu=NhsaU-ulalV8Rfw110U4GJsl*A8H%aeU>q`1pL$=YC_@ShkR12%1 zRBKLUAO0Z+-9U7QU}HGXfj;|?xS_-YCA(0vymAeH3(kKk(Gw-?!s$ZVl`G^&qr?j( z2T{@rC9R|qZ(WH$57DRg$2*{gzUDOEp0wsb(NF zY$LTKc9@yEO0WLE5E?T3zCb&>--Z-(* zCE_!q+fDjDFNc=h>6wo!0g0j&>bH{X%li}LD1#fAv_EN;J&9@=fJA4)=S&nEou#{G zINl@%d6S9Wtsc{9Dahf^ZvbC5kol57ijpAyDEJal9Bh_KC87LLa3FgJ94M6pV$WeD zm0+}`lD_;=@B^y_evnFH`5oW~R?@QiogEmohA6?HN$)rT@+RpW%u=bOe>G$bk5uA^ zl4epZ43AWT^9vtQ>IRT#-DV_%+E+<0WfWa435(G<;wIhl>6^ueyH1&7t>X)P3T6B`yw-T31 z^?ReS^SbByQoZi)SUso8H+o0pa_)gt&qbv1i-tr@=Pw~dG$ibAP)S-D?5N9_$u96N?~W@#{`Sait@C3}BZTU$W|!+u=n z{z0nE3xEHdhul}HhMk$e@J!;+v-2089!e}d2M%<1iHh(lKbCZ6?xNE}hn-ol;7lU1 z@(hb~cZm-7SdC>uEYBsm$ypaCWM9l?zb0IKlI%S(yLWQ$i4*zz$tYzHvM*#!IR7NM zceeB(_hUE7r$U8}JThopt0Mu5XhrOQ?u_y;opJc@ch)HF-1Mw{2?_hMrk_hArjdaG zAz^-j1Id!aedA{SkeYIK`uP2cFQNhh!XtwEM}Bbmdm1dRroos4BSst;lPvxEIoUfQ z!JGZqEf@Tyod5a1ri~t>P2Gc^(I!oiqnPslK7IbmET7SrF^kctb5kemi;LSgVd}Y2 zGW|mR10u+d`K zU(>aG|38d`7Bd_UK7yiWx3E#ctfa@=t?N>N*Gl`T=5s3BCUKtrFELKN176<6imFpx|$wbGqj} z@!Upw4r`HAA9E1_a_?1*r22TS|LrHwy(IXe_#=r?5Zo+PYt|3Kq2>U3IZbUrEzt921 zNdtG7UL-TG=>Q^egILNNsTb*9AL5VW1_Wnh1P>Tj^_#&`ZxjC1_x$g|Qg5q*QS}=u{m){lvbTC7 zhE?HeqXLH^xQoAc<_yvvE(R@@h^2r2Sq~iJQx7li5f2GJr~{;954aXOV2c0~I&&pd zVc&(l3R&~&&X-4o*w}V0Dbq%Bl;de=L1J1MXDR^y?1Y~=g4HR=rL3) z#zpX=I1&pkQG6qw9nf?B`2%T#<%(H&VdrS>jT1Ks{JToeEWLfN zE6+xL*#K;$WdjI?f$~*0^nZ~bW8>~=Y28)wWBKwG@Sj_a~6Exl`|J3|DF9wewwUryU(BvW6K{A{ShzeQ!=~F7s zZuK5Kq!-Cv2VcHC*k;Jk)*3Rdmai%o^^Bu|p9`GXk-@H-kF$gTOFGjBcG@vE40se; z4Ak0*XmU6C>V9oKJab|$45W(Z&9c2VC_C3uk4sF-eY zXFaIx-3Qc}b&>bbsB))I+q3a#zF0oWcIdmI`QThgar5$wnR2c&PQL{uaR#Poh>|39 zSjDC_O5zQ>KpIFHsEx|=#sq;(_(Mshb8jwsU2yOj(Dh1{S`~}yo5Y;`VQawfR!Ng~ zk41ZT<)jj;Nksb*hTR-Q<3eSeavWC19QR$!cDx+-ApvqXW>d2=T_>{-wk^B&9$Y-Q zcFS6PUPe3RWd*y`kC~x?`X`smf8rZk*)Tzpeu|0c^ z<^S*|n-m+xFYh}ZkD#?G3svJ0D1JWSjjq&dbt*h`*9xCOZC58IYJ}Vq$_W;xcFeWv z1y03+C!}F(#8j2k3PUm0e~xDz7jrrs`DQD?@1L*9wokEndAWZmf9BQBYk-ge+j8IJ zHNa#IRqnBg7>|#p+R5WE!z8N)80zU`4EyR~rrZ;cU#`=%PDpF{*d@Z*QzsDYS|%(R z5)f>TGixq9Q*9z_onU!~zi<}g52++r?Tqke+cfWbJF1BH#w@=U}!v z=L&F(DYje|ar(Jc88<@a=~>*|v^Nsj$Bbp{{iTF4H^^qj32JeSAXn~sZU_vKSyIgT zAW_rtG?kv)CUoR=DhI3ja@R+mWP5Ik<*T!1t%JHr`}%ZX=`MZS)*NG7%D`@L6G$Bxw3}sQr4PVl!z#*h0V2(b-vc3QjuenTkzuF zV~F`sY0Y-R95NeN(GkG20LQ~?DEnBJd^Wc+1!%{qK)x`2f+#2q*#`~_75HC`+La3K z|L&@o?w?!=usL2tR|MvHu1&1v&E3_RLNK9GIYOjisCI#}26hf{&xJ^&o{M;aR8#8n zCAR!N;7_S{i0ddboThRvKA%R4}S;DKSpnkP0z-;O2?}Np3EbF76&GDkT zBB3SS(PO)*v=r%Zc}5g_p(R45D7NBXZ%w5H;ik4GJ8UZXR(!93>I>!2fEy&?e?Lqs zkB@9G>fVZfDYk<4Tb3oz9Xv{H3^#=j{Bb+`9%x^u0Y!&b*mfAXRL-9&1Zzw-PE z@m=AqqK*TA;~<&(`Zy(ueqA$~87>t=*p7)PF$ATetoMG`HTtd)OD2)Dmgjg7E{k0p!PC8V!Qvy&SuXs9v`BJ90F7;ei*1E2HKH7km?`>M_ZmLS-q0*jFkKxfb1ua zv7glbCryf67twcJl)P5i_*^)y_&pal3%7oxrpMr;A135WX zYD$CPzvGiXpa_DmB8Y|HBZ>f8fP%h*n0OlwuXJ+>2s$s>{-uQXijwcYlvrBr;3XJ2 z0wc#-M*ceaEd8J46XR|-Ki^I;H?TP@zB851r=U9gFj*S`E{p&I%Aw%=Acxd4CQ9~v zDdD}MWN$vCVJO**l7nALun~C4q5KjR7L;UtDZw+mAI=K!_baIxZR05K!uHX$(-I8y z>52rUvf;7Viz>3lVjg>D;3#!cGg|kj%pJ{f(HUSPLmm7K(S<9r-wl6JMQy3k_+x`- zKWS~KqTv&6QmgXPZC_!*1l*)tXq9az157(VVNA_xh{pfWr}G+I3|DL2wC?(JwPr=0 z#-_K8+NPag)B95k{fRt)2H-%=P?uw_^TVO)&Z}%SM*i@Zq3~poi;bv`rc@Ok3J>|1 zx<jg zVqU&Lg(v;&Y(x&&=c&?TV-+)ail=Z%;3ikT82h#gziP*f(N!iNTLt_e?s+c0+K$?e zhW*{*cotE)d8BoM5>kRQKR|6g3woj20g zJ?t?%U;2gG|3$)3gqJ@N(8xxQKe&}gf}tdQR|TgC+nQekyj1IO zQu6xT0BxLME`JJ~7Q)fxKdG8gMdHT6{YtpoJRBjashL8!d#DSR1IF1xz{-sqS8CJw zU&9ftzG|5=RSlnA?CtXlQAGjvE?=rNV*U`^ccmK80ibZdivKncZIO@Vwp>@lXxicI zp7G%S?_+M2kLUWWEn-aZATnYO|1V>bW--ey=4ivEsn{IF91Z6TS=s=C+Z8{soc2r$Jcd(_{)=5qNRFBQ5PvU#ZxU@jE`@LMehdVU!d zOWjUq9aoGsEi%mzM_9)qcde_9 zp!E?&;ki0^S7i@r5S&H)V1>nKRKzzbAOMWPK9ZF#P(>Dq86$x)LpFa8WskwY*jDqa zm@Bqg%lT6vUH{~jwY{W&jVojoU<(giP!F*?d^AmYng(2nRZ$jKtHGTWw?}VxtFs8a z0!6M*<{pInuE}}50>AC?Tl;9t<&i|AejWq$7d|hiHHmCYP5L%z+P5(^j_`g$->;=T zYgx7s!F*X)d^etYV(Q1sOml^ZIJHPr=btNvfV+MEA} zmyx>uU%@XR4&)=-IDKtwY)%`=O(+A;W68-${R=x@9l;t5E9NHS8~-bF|LV0y)97x2 zqsEXQ`Hh)2Z7liXb5vlr(MFS4sfoL5lkZlnXwDX}1=dlP3#8cl7;Kb1F| zG>Jc&%8s(bqm@f&uySRiX6`kb@kA!gYPdIRv~pz=90y-O`zl~X15xB)xcpl>m1X%k zI0eM|g8n5Prx*Tj$2~W=N-A3^PBn2s!h(~txK^>HnnFqCA*>0ii>an*%6-!`(A_UB zabB6*Vz-H|+yg`_eytPU?FjN@86%Qd26?3XyHl~vE$a7>=Q?_b@M)yE3js$b{aQGn zX@dA|+r{p#wz0N}G7zL-ptgzOjA^;ZL!5ni4%Z~hlkWU+7s_uSP)A`W9D4hFX=&ok zg$jTzwtVGIA5ak% z2rZ#<_hy#^?$(N!>Rjxk2AuG+^5ga%5J6eJ935RC0poH;bAi3}M>X&xj(i%0PvdWn z9eXqV%$aocbK;G0q#b)>{Dd20$KIH5>Xi9&{Ee~9IPS(opPt@7dH3}4>gjdHyQdF6 zyn0GiPxLkxSQz{Zlsfht|nX`H|{#`Q2j)=%#?m( z!uT8G@XSv?rQaM!Wjw>Ye)0P1%>J& z&;{PlFRHzT_wy8ZKf7eOH1E~0RHxvIKB3*DuBHp3_e!+5e(wLq51O!zZGF}Ee5TgS}+JJ`~aALg9LQd^;=9chF)VqY;( zNieMy>WjxHP~OFMdbu`+=pvTKL{=m>VL;mQP!;H53gWwAV4 zL|uV)zY#0`X-W}G{Cxw-#>%f*&7QN_H?FhU?D-lB9CR!&u}5<==BRI+n_->?X}z|i5(cXECt{dHVByX23+%9j zD$U|cj|K6u0~;abm}JqQOU-rEE;kHRI_ercHU&R^Ebe#=3q1=9tpu>GIpWeBV>z$V0mXM40Y47bJG{F??M-2do^i!H3({6`o6 zYL15G(6Dqq1n^`atz~>&xRCitPaW8gy2^L?;%?<5=8vZV;Jy~XL5}!vg&-DGd^Ce= zEGf6lJ*JMR!HD@y{cyXsv@JNI@DCpnx)aoqx9Y$SU`!2#*)EW=Gt3Qf$2K?9p5^mvY>bfP1GP^mjL)v& zpfEGp7#%aj+c1f@2UdXrklpk=%Z_DngKPxL zxEy44EsW#D1Ik$Hs*Dj2Xx9pvN@wFU@FJJkXn9j^FOl;lmv~osC(%{eFJ0l)T?_Oj zrt{Y?*jaNS^P}34R_*9D{|-jW*_2kQMzKkfPA4PmJ=eSRH)WT6TFe(EI$oj?OoL2m z#o~)i0^(Ls3|F)E25^HG{1ng-gU*2hcQS45i*MPaWt+>&Y5O7`<9l6Hl7yolabN~MaZStGtEqGnx@ zHS3G4*-&K7#-eKE!@|I?Lm>Ys^e>|3xG^1ajD<}U9IeTJ(8tXy& zh5D-pYJnq`Q-26C(WY>XmHHR12^P7|=~6@yZLHM4aGQ0ad3FldSgC)Z z8c=^TB`;i4QsnxjuRx8w1Y?V+u~PrSy;-S$;TkLTFI+Q1s#e4}R_b54%~%T!DO_Wv z{)KC-)W2|zmHHR1u~PrSHCF0hxW-ET3)fhwf8iP{^)FPDPyGwmSgC*E8Y}fLTvJA5 zU!QBOy||-@8Y}fL+?ykEAbk~gv`$6gTR!zK+*^4`S43}C>R-5xmHHR1sZaVuJy(|-#z0HICyVyPm!B()iRqV;h6YS1r z>K!sWc+@tW9gCGg`kVTSvYX`0u~;ukZSq2)wTGNhH{5m*${}5b=R*unWyQ-`85)6MMQb~bYG^uD#1nGwkUA1*Z$enqx6xaenCaNuo+^NWhjVy@Ou@aCm<0;*0L!5pV z3wP>1Vs7BP$LZ;h=XIT(9O^_{+}HljUTlD)e((41X(JV1P32x}`1BXvb0e4d-E9;) zHnip}c01=KyEmgoU|O)xiVxq1}NE$r+$c7E?M z>hS`im~3QnUbA@EytckGhZ)#eYHS#>w0+3(fqV86oN<0;Ywy2d%g<1ncs>2aibEvL zoSUcF7M;*6A+STT|Io)R#`Nho(t9!zV;X0`F!BtEWDoE&lo7fU+!7O70ghV9Lft%u z@?Wu>({!IDS^UjV7t2TUd`fu)kc+Lkb|R|AzL-!6#CNvIpK%+ zDaQwndocH*(A1bGS5gMtCjb5EFWHIgcWnIAg-Ls&roLRb<^4ozB79RC&I0F@jIw2J zu(gj*vLD}0JW0U-OC8pBtn8+nrGlHN$;3qfp|=1|4Kp|cEO~U*xJf=<9220({9-qR z?dhYdp?k_E{FTj4yiDIut!^m;xxRu1nwqD^^r zMy#P6@=ENsm1>@Tbc$+hN_v$!sCUr(@F(F5LQ0n}wU#OX^`7TF*BzF}V?%p$)Pp96 zM=^k{ED+7f{{Ik~`E2joTlGTfRPe0WYXI5Q-^|_uw#_GxQkOe7sqQYo=8!S9|H3YT ztA>Ym32eB*Njg}BJ=t9d|wdlI2%RYO?%h_GZ1_so_)efy8G zY@`SS%jo!u-;{8u98&SgGkb}oR?@5`*OF}nQEoxw2FRo<)gv<|5d zc~;SlS192FUHD9im)ElMzmHncHsShw<~iqPvgx{58*?;US*ks2+a7voFCK`(j}ubp z`6NrQ5KI)jMi8Ek=fMCbaeD5g<54oIdswu6)vDP$SsZ@IlZL&ec00DQBQNgVSi6{( zZ({Q|TtW7Y4Vmotoscda8};%|p8abQ6@T5k|J=@J4_|zO%mBSv7iMHgFrN5q$8OWq zKj$VSk5E>yzuBiuW}MTuk!Rj84UV<>&-K zw~9{id*&qiFr15T^F2!Cz#YQjLP$`WVS2{{&3?9^nTllgm`3SmQ}!|z1`m!*>MTv* z*k$gzgo>$D%iOiz5ubz%L>jq12tU_*;hd|+i4Cmv<@zi^IZz57e-T2QovblvL=V$t zSz}r+h64w;0Pk}_9IWTAm)hEn4^P&A)@a)v+&9h+IH{fz>LCgWhLVzkKG(Ep3G0cN zh^58BrbWs>FWC>@e^0Gn_zg>5FkpJtoQ`o*nlzc3*lzX@)1wzA4lejk%1OR=gy#G$ z4Q0<#=q=}46nYj``7E>S@DAJYW;7j+ypuU(TLWZr>Fu?v?~J2K zUaZZSx77XK0jhOZ*v-OOfKWv`X7X!0A#jq3qJN(uWgKTOm-^HaD+&IlZd%ctFe#+NVrkactCZe`VlOxind z>a;nuWA@Z(^J(ubX!Ec81_r zy*#)^-xayFIX%H@s*yh}tO%z$1hr-v)LD!2C)3kSU#5ISx%x}KU*F6x_{i>ML8~_& zQcfk;^LW=Ln$=EQvUK!}nKW<E-a8xJ<-K-8ov>qk^rW8)#xO9O3->(84 zV4*VUK68{_OG<8Dd+NWLJwC{B!i+ECecFDQ9lv;x{l1C%19ukM)IWA?&qhP08N~Vz zwVe%_D(2v`U>V0EPE&AJ4dmRo_!zWAARJV3j7pBmb3mcv%%{MhDrSpLd=bmicG}KD zO|68@m&k4PYIgq|=+)*C2}z@viG>UAl+xN?CO(}v<8~T34{hCcxN+>)dk2<&>DEH* zrDH6V_pkUWVYHs4)Da0Ic<<+_8gfb@T%qu%df<*t@oSzy&S|%2%>R8NXz2=Zf|RMd zh8fQX9^G@Yz|RegV>TZV$Fi+(J@2VJWm;-*tw#?c?6lM5vG{g!%02B>fsDJQZl-;0 zn|ik=P2B$I*zb9q{D;a`eZ2k~*ql3Ss`JS~oWtrki;$TB3Mv;QVJ~CnRhlQu!fYbK zA^#6tLQKijM?CRwD?0VZOV*tIc-ELQjeR<~@>Wuu?$okOUWcHrfRhILB>Z`SBPd0l%?#EFn~Q&mVwF~0Z*|DH1S)Q(O{E{ z+(^g4!V25L;D7_86Pt@3*!gx)q)Ad`WIGsrjkK92q5;RJZM-(H>9nWYdY#zg-`08R zzVlJklny@ookdsrowkr$4qNkW+WyFyf3F~EL!@s^E1aFM|KXL!@+z>D)S#)C=zz4hrP5$7?#jD zfDQhYJ)L)-rjzi3B5&XdSnAN;`6nX-w@#!-%T}#tHn*z+zh%<^rI_`e4a431 zn>Nm~lJ~en8~O4%6Hn=vtE@s3{5Zzy;dM4#lITA6wlD}fkt?@Z@&SoS8M{tQ-GcUN`XtptBS)x^NnJQ=_)9lUqUbD?fB z0?lxkLHk)pQU98{|1kXgv{_1wK(7WbXdf_t7~9R-Y15S2bXmBkOQv!xXVy&i{>l3B{#_dCKy00p$%;ZNB~221EDIAwW1pA zJ@60HAY6vO|7qpGBleb-dOlM2A=Y(-Fii|FK9ZbGdw#*6GfJ$m7;Mma^gj-(?1)Qp z73}L88cifR(715A2jgiqsSYFuGIFv41@LkOh8ctFFpZ*_!k%qZ{^xksVR@#~HF{|f zNiX5sx<_bCkCf8yF|=QczZ83EJnKRiD1aLI&i{0XdDDf}k5F+ zH_Y+l#|bQ4@DC7js|INvsF|m51Kh?XWnLe$5w5a8SGnn1a8eRXDpEY8C zcAILw;g&fLD!sSNiMG%n+V-Z6{aII-VDquAlMP)|=Ksf7=>9Ib zNf=14+4}Wifr|5$RCx~Paxy`d#mlh|_o%cFBu}M%`?+LH(+w>78ryPs9>pjh zCUz@O&LwH)@)%L4(n^gOE*+NMt3JH0{JyNxaqjGI4M^e0Ufo;WExAuf{g$l+vJ>yD z3Qg`2=-DJd3@_luE2t4E1jB9l9c!-7!Diu7H8}mjgclb~xSNCTH2GeDg+`8&NwFb4 zV-cymXXqgL9Yuhd32qgE+*1*e8o_v1Uc}DXwy|`<7q37U##dC1&ahOrTo@4^l|+;r z**95UMBlRz;a%|1VL?GdhUDc54Ov}jZr%sDFZb6}`V<3sg#nQ#dYa=5D9`v5n&z_# z?615>{29ZTPeGVGFea)@L*_CK{Qd(oh<%Eq^`ZKk*Jr6ja)kOD-n^;|N#-&nJa47; zGJ)Eg7ehBwef~;*d*YXLv&;mr9aa|y{X)!%a_%*ZMe9!$)bb|i|66r2ZOn^Fe9?qF z&=2{Ng?^~l)iOHA0w)A_@#ndz9q0<>`~xX05#+G{n9qphwXo9tVK*zM7{q7V!e@lw zZZu*M%F9pLLv%bd(P3Jk;F`YyvvrKYM$jXRinJElY!eVhYu}*++DwY@v_wcj+5t1M@{YlUPmb zt*dKphuH3RTBJ(SYE4oTWsVSUJVWP&c4T22tUaZ?yN?YSFu!w`S$(>u_ia&f9rt~&XC&cwajZ;xg z&_?sLKQZD_ke;=9An$qo4QFSlOuN}r6y(#NrB8%(nkeL)TnGp#@J`Y z1NM{8H2ZZ6bmaiHS2p;p4 z^HMF-L$T^{9FHaW+d2_AD$TBqQ%7g=NVS-O3O`3vr|l$!^|;--Io6kKO1cccz# zVTN-3pjg`U?nl8rL%E}D_{x(&t}Y-7zF2|Es;mlkSnkeJ#)*e>m#g7Iz^C6`Wx`!` zTzG=;8QdJ!k;_uJZ96j5s%@yk#WrVW_205?&rr|srvIFFt=KdnxJOctdhXq1`4^Y2 z?r#3=!u`4@HL3XRnGfIfYaG3E?Y054PfHy>1*Qk@WdCIKJh(4p`*x~X%fW5Z>8Vqf z`FdXaRNd1pbar(5(Ftx2wb;#_TY~o=iOMAT`=E3UeZDJ%=RJoUNJLZ*P?HLu{}ID^ zSafW?L?MIq6sKeBvD~BVAzqUwW}B+-jS#-gfM!-D6If2%cgi3-alTc)H8q)vV-PWT zd)t3eEAZKnDxM(6MO)aJ`ybf;ZC5Gfkp!}^v%(V-#3HpF^H{$0Dr z%r32_4j4GC)pF^dRqXfdBkacR8@Q1)4x(t7N|qS7YuJE;iIv^Rwu*~Ktp}rb*4@m= z%E~}LK9+v=%jm~uH{kh(!{f+h@|M`7?)f2M)H7O#KFbn&|Kd3Zt_;PbjEot;Oyvjm zCW;R&{{9ou#IArdGjslTA>Cs#MCZzND=iw{uU%^!#414;bMa@LNp<3I;RFk)hZxH| zR1LhVerC6-4%=zmwcD^g&XwJ&xBPF4`ujcg{d=XXtJX8b$0wv`wO*O?x`)ooL~M~M z%3~ItH&%7qcENdutA&di3j?-}x2V_Pp}mOR5~$d-XJo59=CN)LzIcH-J1ozTmfEDF zU3LDhs{X+X*&8wU_DDDE%PW8sR>kAGYTF|t62)*=*T3n~Dzel}(ODk5d*c!-tQb}M3^3o)o{-dEx z-ffL&*s3J4rFrQV!lSazxFxG+D1!AA_Y@k{rs7JHP+a+kD!qHhp7L)vOlQi=Sd7hj z9HtcjeSTOVFGQCStGfAEV?vomK2{?H@n~C!zBTgUq7*p&jvqXPyQRs?5NKw|kT$75 z4x!1Z?SqHK1qBVm%xqR6gHYZ4leUJ1hLbygm- ziFAxI@o?zf% zz{wc*$AW2ea10ev-<_pmLtbucjR z%i5j_bZT7x%dga*b%SWVD@7(nMJ6UiM2yh4%+4;&#zLG3iZ-w!13Y(6G&E=D_ z1DHW>;f)1FGZ%72vPNkfWJi~;-#{3@`N|)E{K<~!a(O%Qd5Z+7BhYf4d`A0&O>4A- z+Q8CMTa61|L9Tt&@a{U%8DRzQJ$%gW;gWdwfqiJ_sCKp&Z^rxU6n1|1Zt|J7SgKyF zuJgK-AsxCD(+E@_oqlK237vxFv~)^{I%Jjbeu+gSZl?w8?&JIHCM~9p3n@CZUYFMX zZdKjq4(RUQp=0~%RonsbWkBpf!5-bFR;XT~v0IJKQ!BVsXoUsbA%zKxRKX-X;Kgb? z=;c!v=-y`F#bEDaslvA4l_C-+sSiAT>hhF7WZX<6@F*E`v{6cyey%!qJS`q@06GID zt8H?dpBpwO{ap5E#D)FMhjjmSD%m6XroA>w^Sk-^pfAt=-Zr`Qm9IRmxgm!M4*FPr zss=$wdV_oeP*PfXhZnf2y(J{+yYr#dY{EP_mV@r9Pw&Pzt2LFhw~oi`ndmmSX;#uM zJiJdEEX>hIs45-K(7C%fIhJvBqS58L29zj95n8#fXDMxPvs)xj9u_x^s0hm5REsuldl>OS9NpOwT<` z&-d2pu|96RM|fX8J=!R;i@3NS&tILw-k|$~;%;fB#KkdcqLhQ;b8$DWEToyLtkqrw zV^kGL0ojm`i#d6(S#_ue>2R?qlOXAL6x+5N?kmuY9qiHGH|#Y0f|~wul&WlCBgC#Q?_!eZ@Y!#t`P~Eb5CEKI@;JIgS==4D5r(H&`y9ngQlJ(DLAyl@&0ocx;xy{I#jP^(>@x#jAa#se$+`_ zHU~$JUd;t}J0Xf%CUyMXrDa{BPH%_a{IN}czlkfOp|K~;kF1Q)lic_W`hgD_cb5V`{FdxF+=3VOyK*aS)@aUXBCqik9BiV zFBC3zE_DScr_CvM)n&sSLq?G3YL!JN(R#Vyd3en_YH#gv<7&arp<};BGjEODO8ze) z_ujrJZ;ifUsd^*RhE{#=IeBE_ltzYifxV$OP)+QCyJt;RFXf7K6*3ZuS zx!6n0qB(#g4k~ES%{B*M#a#+~SDJyV8Vie}&VeZ@fkBCh^f_q6h@haPB-?2e33m}~ zT&J$XhIQ=}XZ-{Zu}VJ4HrS5Ax^mxc)t$q7j1}ygK){W|{~h&Ixqm~A)q2tzsMPvN z|EQ4;MVz|paq9Z2-8-XuYu`XYRSv|3;`mP1bI&k2CZy+}*zO^Ngs4G1gNF>pFLqNs zjuY7f7N=|L9}&^2P47Of{rmQ5jiXs;zHFHzG#tKV@G?#14QvZikMo!LKN_t|TuAo> z1iOqEvf@L#$H#XMjb|&qoZPz5fY?FpIu3{qXdfHfzFka=kaPTaX*PzYl)z{+@osVY z-}0BnzM3dnd8hpMQx6O`Q+o((7eOA?V=(glhK3Fj`V9%~9zzt}BXo%VH})B!)MBh} z*1At$fB#5aU<`qHpJg`h^B1Rx*%N^jhQQ%?m4xoWaY9yNNN7BNod=LXZ)GXwx8Pwr zJI0vb;spz)9q50q{wiBsoU7OW>*CwPUD+7*@5S$fpp}K9-imd>AERUWBU@}+{r|aR z`J)w+te=N?HhHLH&naEevG6q`sz~#T}+#u^~4%lQo}C>zgSJ zxGG+_qAKKX2?)<>SctceQs15n5esZ2!uy)idW;?0qsN#rl)>LK)g0Y7RRu#Z%#nX& z;Wp-7X%6^Ypg9XSHJ}`|`Pi{GFJHVcT}=rNPT~JRhpuX_ifLkH^$pB9xB&fCVLPiz zc0yH%M)n9DmHptlAPg^gL!T_Ui5o!)+=VT2g89=X4xc(BkXzR zGqyi-A9Z}TpQ`+rdz^(LK?-zA8~x)Z#b1~*^-^M^C||!tkJ2vFpqv-f|NOWd3NnCE z0@Q7wS~{pgz1Iw-+?Df?hD)Y&)<3TnC|hMiRi!@8hhce7wa)-|4YQlP^+a;#&cv#2 z&jHJsZ`8-Bvn0Hlm9x9?m3G#6RQ*VKAT%o+BqQrnRo zpLGw2I55sTaNWc%eiQnYWq<5_OV!Jjf%aODUVav7=vVM`(szUEN)>!+4Ng(cHyu(-aQd#s z&}7=iF6re^A}Ue37(0%P?h0p!{pH1?Ro4kj2EQr)V{!Y)m2MIe!hWt zm?^yEL^@aV$Y!g7W>N#lx&8wrX;{ST>sucue0a2-|^KQ`CE2MSFxP^1V**>`)2e?#m zGr+*9EM=#hgdFXBHtJxTxSH)3d|;tc4H=xs&1`h3Qw4O@1+AH<;OplgJBssZtldK-W~3R+BK_5UPY^z5vq053c=T>mk*_M`>u}{C2NCj*XIi$g)%qfkNiMKo$X@jtc;tH4a*f&d|pm zph8(Fj$*w>4Xx3%ywGs47s=Fs^`?y}gTHN7iR#6E2Yv5VN$%B8r=wQIOR~30N8>-? z5rE`)vC>q62&~G4Jf^#+_k~908*mY>Trq7n8dH(fSz~Wu{G9OS>JBSta3nVWm7T^l zqsgySn<5=qKZoSBKI43e{L&?ZY4Ac8CDnxO$s*HWF`XUWOl~LMouKL)Sq^(LG9`F! zpZDPlLzMH#>fS+WYZ#`TuYBO&g<|X=wFffa&U-HWz;yuH2I`ei|9{m@BVHnF;Q4qw0JNx6OYKQt! zJ5W$A?fy71=4bZv`S=$nMi5o_bM(#qZ3WxCOzL(su~TTsf`qsggn`esPLo5rr?+Yr-wBvCAJIq;VNe}1Wng9;sG1iJS#=ZU zv;1Z`r#d09%;VxG%$)===tJp_V*8GUDQl?X_G&>zRzQvG% zM|1~HP?`BFnX+bRT0>d$my+@xHpiRW1-OQF9r=98h^sTk92o6tU%Qoe%K)mHdLeDr zp@iz?8v6sdX^_oMfY8INFh#WCx&nkYssPNvjDrZF?dT*)93GJU>L6K?goA(|lG<03 zcQ5@spJ)NuXP+aN6|64}PmV|?YCU5hF#Vei?DDM@@R~lodF%otAt)8Gbc`tgRx4}# zJG!|H-#a<&`h+oeW`zvy-rA!@&9WZX&riCN;+Zy-8YhhR3XnH*MI(-aQIHu3x~u9j zXAR(_o|GPdf&a&u3hQ_Axw5kYyDUv(m!0yziB;dc;Sz8DPulbD9kLV|CPswccJnX39wjk;v&MhESKwC$4f^*CDC?J=P4a_^lXZ*ScHvxVdepfqy{R zy7uBYYErydO1!l4{R}OO5%qGw##z#I8V_X}A1N<~D^N?=f zMBySgZzx-UI+obykgelhTv>1`DF{dQ!B{)|amu~{&sn5UFKl@qqEeL|MpD%tmFiX` zvaejF6jA+eds5j1r^@)+bD=g|Nalt<%1l;W>XzH9QEDHj#%*hpklj4AS-EfO)T-h- zsL#=l^*c3l?33CE?}0NMuBT0#q}8k}kz0fm8E<3`qVPaQdIrpS^ZJE^kw zPinSmB|GwgiyYC> zKXhgsw$!p0DP4NGS8pjrz*wp-=HBw&h+WXgye66jCawmY3W&Ix);#;>K%qzlY&O5M z2;jY;H*<62K#3(6OviMuCY->h-yCL-H;6P7vWU^CZbYuBeOXU!GwO1VeSiHlX8n8y z=?})tpKzkj>8KOqgwskh;WUQ41#7gaHU}X-hG;BEy_N7_e1h4O8P{}kWFj4pt$XY9 zg$?)Oj30-0=p0_NT)TEXTZwvV!A|JwX8kh$-DYO^ljRuw5<8Gh=;RJPyp;$pqSl^mdb z0d}2g%S}4=$`~

f>>Dr>}pMH28R$5_tXQXm}lDzEwtY1u9)iUtgp+5B50GSXM4|5(6vc7U_v~Lea;%RaN>!+llmS{ zH+|-jlCfde!|s601cyNe%1IDx6UwNz2_Qk-Ho@cvON2G5CBi_%{!c-ehSQkkMOa5W zZV|>e66lb6=9`@vM4kD!6R&aOJRN<65VrN)VU>eB**VpA3h=G&OMNoO?mAEGGKKc( z5>-e4jd6f$Dw|L{41KJXG2UJ*K6#OM~?1l#&g5&rX`seD1;UVolx6 zMBLkpIg;mx`*nKOujEs)QLI08VtrmST?wivN!3C+AnRabC0ldc+&BoAE`VmXB>_Wf zd;bHt@s@*{Rr0J@x3`kbUegBFCm_^SlGw8nI_#nA!dhvR_#V;ayTJ~hGe8H0X~Im& z3pZ5@BAW#g2geSLO;3*{ppEb2+D~=JKH$Uk-ywtJGFz<*fx93)2SkMEE$U*8ne*)=18E>|BbMZ&7pT z(g-%s{aZoUXj6RDOpJpYqP#|>#&6J%P97~sspD8~FrXBW-*f7nXEbs7{slb0i8F=z z_9z!Mo4`k$Qg4n9Fu&47g2Qn)UvyL+nkSaTg$JAYdojp>aj`3EU{Zx?~d_pw&`sn-ZO-?(VPDWV3^3?+z30 zk%#eZzxJY*T`yJ)a9H5NHyc$Ghc$KKfwQ3mawQ>-g=z08UaXS`xRVS!Hi-HP-FWw~ z_xGWBLL-Alh`nw)X_@#KbwW<46VhKZO?}amc=6$gbt8f|DPhKRf)bubk#*6oF`5v< z?G0N(N5sEa3~M1s6e7*#{)A$@vE|?5MdJ=rkTe(jB?Bst^O&##4kKT7GzhGE?cqKI zcmX>x=p{S7b}co{IYQs8VTt53wA-*z4H}Gt9YPp+u)XIf6!7Om~@}ei-4e}pRvwm5&?jVL^Bq!MH^nw02+@Q=ItwO4hWQ)aQdl0?;Zs% zUL|Z?|M=qVJ8(AX#GOrKP4~T1Cmf5;?i(1`nk5|ldD^v6>FI$_np5$una85~MucgP zW@ne+UM7&LX%@q?>j?6G@U;R``T_T_R-`4|!ijJ7h93MvhhxBWLW!bQb z@TRJ)#fl&532jP!>*H3cyq+p`X^^xj-i?i4Qn5!s>6+zj>(0@oKuZ3M?36a9Fs(rl z+NZ@E#<;bq!w$$o`}6Q{Z3Yl02VM4OT^49ll(I?ag>~#BS7DX)qa=5Ikl_qRVttSav1~xTo5)p^R;WRVaf4dYNby%% z=B-vs)_vkg19nVw!)+auH4@ih&U@v{E@(Fkn~K~}-tkSGotxGjFi7saVH)l9q#8Ed7AlY^etDCt&Yz^-E;3$n@lh}N!nmhacxM4TgSvL67I|{o$ zsF{0%CLR+gjwMkbRS+9MY|$C>daNEuW2G%MUn14G(ygJoOQfGEpbkC&uY?v^&?%c3`i$j7|$MO5hjXN)h={KjNdI_ao>ZP^(ZpN0JW_Q;Q#%vkS>tE$9DRv(K z==f{_L%GzD!MtZYZ18)U8rqKLLicSs2xDeEFMrBgV(a^_hjXbiC9<6q0SV(in#~e# zsKV2k@^%(y6N-n;B7gK-lw6q&G}X7IL7|~Ti3a!R9wYRN?GZW%CXXRKVs-O#a>%(= zMBmn}!o&S7T5MIQ{0c&4q?6;W7e4_0+%8xc_@fluWh_bi#=||6@uJjqDX00NHl_{6T~7VEl_E z(^;yZv=eXW+t3ac%r@rafU=udU+F?#8<4l5dFMX0f!6#CE6*0ffQ4@&%n(~>YM959 zh>4m9c~5ON05v3VkNm2|j}vGJY>qQN{HuWw1cs#3tz{Ph`du%b$&vM^N!6+AKpr)JUHJJBLWu+2O;f zRQ!>psQ*svlU0-`juDbLxExzo36B(KAqlR)U>B5Y@^*eK^DsvE%`cTUT!d=5RPN=M zN_5c%r&tVR5;(ykfo~q(6$Bp_0Vyh`D#s~{QcCwqZBt!OtM%*#%aJw4^@||o5~%!E z7Hi63yU_CUYbc&v$azw6~p z8$DM%-}s81fpbyQKNr8*T#NN~>D4MJ<=X~nQXcEg4$)=uu2O~mR1O5s3gSm*^&A>_ zY4EBJEZ`gJ>|S%suVbnw9*vVOe9D#wO{96}K^*=92865UG_FdJ(Mht|NDzzf0O2WT zX~2kAx3@zaU2{?eP%ikFSP%nurRrMuzuCids@=CkL^^r1d+hc4r?XqmBS)H>2Vy^$ z9Ot#n_odmax&uKYgBff!h8tj(Y@- z`-A;(VdCeXl4&vxddLjZHU{)s7rjVXj@8%>sEc8JtEs1qnd35}D+rtVh2QCL?(yTV zy0D#=B*u;&Lkp8f3`wHJ>)6vD!q-o3*zv{egY4;gVU#$kb2rygY`ZBp?`O8i$6Vf4v$3(dtWnygIN-fJ$?E# z^>?;^9})Bfzdxgn`}VQ@&r%k&B5JiDC3!)sRtw~D$`oOgF&cT<<|-ywg}!`A{m-3a zD__23OD|E64IC0XYWB>z?e*Fra371MQ@Wr8g;b4zvxyf=xt&4-YZ@FxN zxF;RAHpIlB9J+yggwsM#><)iB=%|Tb7gc2fcgN#bo61n}9-&%0iPp26y^CK@r{)wJ z)pKNqDSODAvlp+`zfhjeP}WRqJ{YSo~h@y&1}{@_%Q8W8n|*p zmb8jGEo6_bf4t5fEyQfzxpHOownZC1Zdi&d%f6)>K5kgNW$l$Kci5f93(5KV$E)PB z=z7P-0~$`6u6@DAO@tsDb)FAhi)NyHC&a$4IkG`TNt7#oO z^CFA=wneaQ+1F#_skquP7bdTna)HXvyTH~ztbLDdK0ga?37R`V$0o4L<*t2@ds$rj zT)yJk=YxB=N?tWw!k}6pdP!YafRrIKSC&_XLKidR_lMZib+kEZ$f#rzLut!;;A9a+l<^B(jdS1W$K^G!IjJTXdv9|RC&_-D zhW!Br47fW8ubP9!M1W?vZvxvv$f_Cke-nRn>zd$|2Tlb0HqY4c^Jo_xU%k0YJp%6N>t9;RMpQlKX8tF%^l z?M$Vq`?@fg7+i9OwcWOLL0MGegY)?9QQv+WwSBzv=xxz;YYp6H2(fkk@JNG1r~taE zj&m^BYSo~8{H(kGxw`H?rdPqa8m+r8x#`ocaUL{{TB}FEIGm~|m)-Sgn#*`Ecib({ zQ5p4)d67**SL1;-7eK^B2yVQ2$2jau*(aG!y=^lcj*X+0tUZ*q4;c8}qW zw`cdp)%cU9v%ZoJMiJP*_R!tc;*qWqP1_veR~7S^B^_p~MaVC4wD5EnRxg@@?l8h{ zpQXSLAB?*`OI>o$Q+n*63F(7l(}itg2M?M+G-1%-v0uIk8)yO^;Ki{M$l<-z>b>!Z z`iHg$$HxbUBqS&YLlY80gW}`0Gc8|~!j(gMsD&Enn z!;kFEkHgDV>YB7xNE&cuZ5^L)9V@-3;XfZ{2k+KA%T6DnuKhPf)*O6bc6)2ZnPf@X(| z+aEY96p3mZBVnz67rC(Cl!04Px=iS_bp-yb5AgJ8M*$w5?SQ#Mv>$02WKmr7Vz!fT z!10aKo2Dgi*^*qpMT`2{j|)>ruiDz8L7nfg1%}X{G)*ca>+~}<8Y8^(G=b=1n7NA} zFEdD9Du97?{ru{p7wyOO-_>c*V(Y5WsSD9VAbJ?4_As0GfQ$7XO0=TOn~BTvOyjsu zFaRh}GTXSEm+u4aW(pOHU4=0y!8gZ6SQ zYx@!*YQKCwP|xRs&zJ3cYkMv7-CEz%(q7QS<~3zZcl?7_>qe6=i!2BBTw8$?14+aRfYXxmpmi z-G8YCd6A|Na&S7{UuOXf(4l&CA z*V}c+MR9fQy)(PB3pR>~6-BBjNMBG?kRoEIi-4el3Wx%B?4n}B-ceBz5j(M0?7bu! z6MKm;<0{8bdO1%}+CZDeJG~+i`}fmbkC)T( z)9J9NokV)hjqSHMA|gLEXk3qlvg%yo31WM41pOGB*Q@u^n98h; zG4?&$Hq5&|0v)e^I}qVQsiUN8cEn@X?1&eG5hhy;$$TCBsn{_bk(Ck*2T5kZt_`C% zMt5Lhi`qkywHY%M_G@1*N4)tP5&;vet@PLf_$gT>->vzDc#cYp%=B+UNK8@Y@S}bg zoHoAV-jgf!twVY=u&e9gmf$&5as+noGsDrXlR)$F?8-;np{yRZ>%6$?1!J-U^OB7-ibCqGD zr+8F5ADJ+a3W2!jD-Z|@%w+!w!WkcU)(RN!lPAE=nfn_;DbQiVSNm!^&E2e7t zp%xaRTD+@e*82Kd*x<~kxdz*oj@%|u&83SA4^==5P#G_(Y2(im+A&Nip49r2E2>V1 zSIiSrXdCSgxzgZ4#Zk?2bW#kEeTc14&3wyNE_h0)r$s4PJSPFZqM-2Kh0c@97K>%d zZ2(w#oEB^(CNUZnYEJ3U1MEYGOoJ=(B}t2Mk$^i?bH@}7E63i2|Bo&)9+|Nw-an21 z=44m-E!w-9Oy=Dpbqp>y(TkL}O#!8>t;3KJ;QK=rOfoB$_71FLte|I%oyR{~O6`Rn zpR>%mlwSa`tcqubzTFHPw*yemyPzL9+PHBpkEYd!ak42lBKq6xp z1W13r`ygZhyIVnO?-l6)fngB~RXDe`D)^lIlTxKy&n2YK^s_bXB+ z-gvbY!)GPte*bDH&yL$9L3vmM6gXi4G4&9R$hsKL)zfcbQKDv+@j%R(HG>YiVCKkT z^RGs@hCX-}y(%2xk6j{;K8_?JP|M` zwlD>7bs{E&rNj{YWdCL+*}q8}I{61;v^ccN4$fmMnJ}vv7gYHlkZp66 z5+@$1@@8lfOa`e~qL>5C2Pnmpti zX02k?ycLWWg^HgaG#pztB^MIKA+nofjmlVV{;;liu!N@2P&$C-PfA`ZEb$wZ*V(LD zGqa9e{R;}}SvITZi|uVGT7D3YmO3X9QkD`R^jW(zmX(;fgXAQ485q~j4vjo9xqa&3 zHn!d$G)`#Fe#@MkIypU0|EI=C*gS|e2D$w5C4)vdFFKvS1SCJT{9t7^Oml84w4$#; zbIEoKvOU8YnFX<%?vPLqN7KsNLDq!0xW=vS>+I<2LdZ#l_MCGA>$C~JT_c;HYuV6c z@Th=6z0us;g#@}y z=8`}OvpZu;C8TLj$?gX*6OeBk9f~SRL5zCBy`(uAa!b*}#%V*pZSSY%(z`Dwy)DZi zYn8Jz$B&vaGA4Zn4dKQb^MQ2lhXySOH{#*y1wZGgGk1+O?b4Zf*RbQ;Fdi!Q$06EZ zlqQhq70|udOCG@xQrO0WxUr!xH!u_fTt%mOde<#$?DXJR&eBSqm(6a zn@D2-wZ-)5wxp%~8y@D3NSCF~W9wT;;ZO!PX47<}yMNFpwvL)u9l>u*H zl`3V&iqeSAv?T7%VfW^4E$h1eaysYE)JZQ2Qpd-J`gL|Naldgq_tan#=`0_sXNLtr z3f&_~nY*yFV5eccdnRofbeE)1>_m#B>YWJprJabAHG)9}HeLNxz-qya9~5>K?-VsF z-qD3uu8{Qaz5~4at5}!5*XHTc1!CB+K?ryiQG+W!WJ+N&bJ@SxP&^A0F==U9?1<9MwKy+BiK#X&~Cr|P6qxql34f7A3fz< zPLrTNx0RELx5#0-O#G;l{aI${`<{}-ju!~9oK0Ob&aI3}+?>iec8V+1qHDh?MAA-N4I2b=`KzZ58&ln6l}Inw`!gmoJd z^SaS)$4S_oUr6}DjqoW5C2iC<#9+~RI*;=7(nFH)=j0T!mjrg3y=Z6V+GU>o{Y!{! z{xtf>g#+}Wcv+(nrvAS55wu8u>}*M59CG%)UberN_@C^jBg#b3l@Rf;&dvDq?vaM%``ypiSmPL7`Pgye8g2!COyD zgZtUe`Fk^xl3Im%uX-^05F@8ocu-p?| zcZoR+NDAWW_sG;3sAWfF%hnB69gtLX1}p@-CgaJnebm;LZKs|vW3+>|s2;RFQaWh| zv1YO&NVI&R#VgYH#93PK)Z!UkeU=47s%5eIFd;|sE#S(T>_28Vcttqr6>)%=EQR2- z_PN~-`uNJQ#l-kPBxLJe%j6#y(x(@1(C^o*CJx_SCXE)+3&pEf6>ccV-$?pdCdNnh zH6=YK{I=!riy=Of&gX4=J&w?wrg6~;3C%EkK^lca^H`}$eqXDSDWgX~{zhHUQ%u{2 z4{+k7u~k)baNbcNw%WMy>zh0vap%v`4G)_*-)~*KWXskii?)&; zp4kJ_M>YU5GO6Q$uQ~ zI!{YX8L#G5jenx+&knyz>aDYXLK4rP!1hqycOF_$7wN zdAjzA{W|*M>hQB9UfX!=nUQhDnRCbNOw2gPsx6)}xQPkJ_B97tI~f*LKb-qajsKfU zDgM2IOC}e+(ZJ?g6*W`;TI%mb1^;e>ufR=r=$~uzZv$U@9eQ% z=BMl@#>>vr;_rXFLAL^07Ta?t@Is)PiXybN#QhDcUTraAtUPpco-F`Ov?(nhO^+NW zjaQBTc=-E1GsgjJ7&~~cIyidn(G-g8w`zHv9#L^1in3hk~pfXcF zPUAqIBMhP0!V1N0SX>~ACkf^ER(z~b2<$5dpK-`WCB1Me|L zuIn@L6mKmaxQBlZae<7ghzqXm^Kumz{8T)F%Oj8Q&oRzV`fdMRuKiPp1exS}Zl&@b zW4JPb3UlAq&O;}Mk`&@ht{rsx2&ut9Zhm;BHNpSlYFx3FN$UPWK9IWMH8VK@$Mbn{^y@a_a%J} zD1w_Q^U1|ETy~w9tV~b31Z?+Ry$_mPhi|AYl}^Q9oQc&s&_@z&h;l_%+NxD)St|;BoLl(#v~c#J=lbR5 z_8XiteV~)Kx08bxY(*H~LvF&G-~b0=dBt>$|2c~8ux&BJoot~!%|)6eiV*&)(inHf zD0@IJ44$BPOkSq##8i5LP~KIYy%|_s>emckrpvfAb(5vHkGYN#s43t?DkfQ;gF-FAsH^L`;?FkE`~G#+&@9n$YS9rnL;NtA!}(O%Tvtd zJ+-?(QU`jXbP#*n)C4@HH8e$Ui^``eE2}GvTwju{X~DN))q?S)e;a=8F_W-hZaC)SBoLy zFWRa}g#1{SXl{|(QoDq3EyeGN3v0oO4uT)e!0Ak9nxR8zJ{u)=Z15=TOwC0{|9-t9 zy<7sMkrL~j*eBM_xii5ST1c-K|F%&$gK-kjp%w-VLmxUA^)F7I7EWk5>ep59rsHcU z@eUhi-}fcRQ9U3(*MLh-ph*S;>m+STvUarc>rkg($sm03B@II}hJ{589V*uDH9R96%5YRZ zS=a)12@%-({!%PGMkOto#-Cv_pWhePGoZ@9UucAvbKu*sg|S_-M+dhKs=nDesOqM3 zpz?jq@VSu|EiBZYHGAbnI=gt0g|)-yhBvXd@boYY&5Lw#QOgwNPem2|sAWzvt6baZ zs5MqZt#-q3vI4Ku4X!rQEB_!f@6sRV#dHdYVbh6a=Noi& z5Pd*WI+G^!C*)WeB^rvyw2P|;87!BG2QfD{BWlfyBS+{E;@;zDI=Krd#|`N63#87J zxfe*-bO1)b2^L~QmFt)KWOgDD8&2H1bz(bMT6JzGSS-%SSTn7=j~e!z04dwk6oYws zmsimEGZDg4!u{krY*NRD0O@U(GK6NL%b>1s6dz$})nQ3`f<@)i1*CGp5^Au7{3$vuBEK%8CgjZ$T0@#72Gmnq z0{zB|ilt0msB{nwXjAFV%L*`T0FqgY2C{~@W2bsqK9 zmCGb)V}sC;8>T#uwRSf2QZZ_M?Yz(60pokAtM}(=jd>832PH8-}Ey z#fb||C@B8Zj}^(G^J8Z03CrsM8+F(|dfYL>>ZnO=5JFt~yi)O7%lRO8HADn?*yt28FAigyiE}aB$;Ddfp zViK8S`?A!H2t6y40oexTJZGuIb&2k|jsW_?Ou+rp9 zB{6v>Z2~9sZWuk-!9LqL8d?zxVYp~+n2#o9@*=2dOlKjnJ=p47gQB5K%021WY zlI@xY+PbottE7Z*}5axTCxB`p8O;*Hdsob&IPir)McW2{7a zrF0&{=3{QAQ=UlW*IasVj2gFS(lsi0&IlV_cHov6cI~+aXlP})Fj#anWP%pc8e;UT zzqH%46K$ze`a`a{ef=he^#W|OMiGPI>(}oW{ae@*zDQML@)YD!7iFk0$U?Q5!3|7p zbQa;7~FW8yqPxT&6C>(SvPOeTsr2-6EcI0mT^RYjx$MGr1TneCTt2xl|N1fOLsdhB(Z#` zbhhMVL#4wjbG-{pFHw)_={)X3MJDND`4rkaDAkKuS*H6Z99z?u8?DjEJ-hU3rr;3+ zsD^FQ;bprS+Z{C?@}@Lg-n2Yjc|(+Q8ur(U zl}HMICzs7f1r~iR8(k(i5)PmV{MB+1n%y0m0=k{~SI5&h^ob3)U)Udta11!yIYl0#unsdhZU~`!9=ePlHxu4!tj)mdvtRr_xk&A0xm7P;Wj$&L+MGiAOYz(~A zP;OHBx?h-ro~~O1$D99edbZB&oB5%uAtBV#%!XWu1(I?1tBD(57#cJ1#1cpiw{UP& zpFt$GcT~!euNxhY8#0S0v$i1`$M4;elDc&rI9P0Vfy6zqzE9#W%%^Y4M&##Q7;K$# ze)7o7ZCV+_f>{gb{Ng#`B>NZY(qE7wY_~R8k0hNi3|$#r-Fs;0*ip7NqsI@K3AQ?X zqFU~FP1?dm%;{H$>BIHhm_CC^YD{Db8O*7i`}ynNK5gwq(qJKAPLzzUzcBm5NvW_Q z-ICD9-%*uvoHS+(bQ=lA3gd6_pm1^1Xg8rmvz^*@Cq7(ru7TL0@@<%i~vpe%-69k(#%o0HvHG@qcnqP0p4E&b)<$xz| zY4y^nDlmVzS+q<(2l2wJNejN7ZDYrN5uVOjXO`OcbvCv!oSofypkrNYbMa@oZcJDM zbtC(UZaLffwE(%@-H zQTm@OOc2Z^{^q|Jn6$xxdUW)^7?!|Wi1vm2A!fbK#~B7e^#ozxfv7VOnymwT8$io3 zztWSi+HL=eeb1cW-f}#N7ZXSuy7MfFy4B)G5_yj9N;@{LMgNnNx$fYG*kbSx(>{|w zR6NtY0+zOrzSsb;!V8%8Lc)Im7F!w*BSpg|s~sne2YOZbnzSz{peKK&F=x1Z(kAub zDC>k{lSmKd^TPUjy6p^?PVDF{U>!iggj`0*{gEYePYY?jh}nYTf!7v)8Tl?H24y zpCQeR^+*%qr@2;8Fmran8sBaq9W>Y0ubVYnJc#ng=_c4`}Q;F(}k5jFfko4UdJ0DG>IVVq{B zyqYLAosG8T4&cM|!D3QE6{3H#qLG^L+mX#uy5{xWh z{&<_EhQH&ry17W}$UpOygZtAZ>R>MdSHefUfJvBudz+Qym4F0CK}3o?aOg`M!QViBw@T=4=if2C&OqRN#k$`{WV zHX9{|NgR%XAeN}E|>A;fkJDo zyUGu*|Kwiuv%}WsJ~}o1Nh7+Ej-p#8rlwAsoRT_G-gxpL<|Sj$WR)+CKyZ|A!ZWkN zfMnf?9F`FgmY$9mjKvEE;|0tI6r-Sh#sgDejnch9cTyD>k}0XF6A3~7Q^d{0jGU(< zHLoV64FZ-nc~IIU;SV@94eOPaRf`2;c^Z*1ECPPVArsFLkg!~d2@le zPr`>}D5JIoKiP?wJ|r<;SvjZOnt=E$LfUloCZt`@cKUxh_iUPyAAyS=K7{!6!0mu& z7&SJl-MWv%R%`2?zP2sRjcV4Y39WjQrubseKsCeACBWX&+Nh?ghG88`OSpG7H83%3 z0#D%xa1u=`R7=plOz<`*c1pEENi%cB#ks%EAR}{COCCapTj$}o4^a(QVTpDlaBBsU zdRVIAan+Uc8BR{pnZlB1Cusy$ITJ*!nWj%dSg5_qIY8I;jp71XbefaTkmM?IsZpH^ zv9znW%EQSoE!nM%dQKxwAjwGNQc#_XnWeH~Br)U7#D}FUm*+@QUV&ViAqhK~;6-Xn zP^pxa_2?6lHF*m8?Kr(#xSAz~E;-_dACRXD9*ELEV9FlAo0SzV$H?o+(_rQPg!b7| z_}K#-%r9z+#oB1B9VU1Oj&*M8&re|WG#A(r%%M@2-obN+n>42@XciZMGnlG##M*B; z5U9(A)tH8Dm~0nj$-pUV58a*xhr*f}?e5Ka4EOc6BrbJGv^+)`8Xfm^U(k>tz<)ko z5LRn{&^?K1W(V*>rv^dL0`ccr_k1;TSn^edZ*8?+m|_k>o|)i79s2^a!Zg+DR}(K=UrRREX-L3N5P8kDjx^At@fIUU<8?&P#F zkzpf}0$Kz#wr<{6S?oI}Yw+6MKYyG5!_3h7B~9XYET20eHmZN8nzeK4SpYVU5t8X# zWfo*A<|oQcg8yyp+$71!=F3ZsavwK;e!`+qa+mXTb$2Hl9+CYE`cC#Gdx*EIhnq5M z`iw4PgIl$6cdhyY1M3U)bnuB%IW`&3u)c4|vvI)XibfQFBhDvF{rp2a^pEiD z*F4Q;jgqFh>OsI{UkUx|*don4m4Sb>>44(o&j7p0kqu)8+BjyrhXXVAMlD|HYvBp8 z>Hkmbmuj(hH~D_#rruGTMvmSTb!w;Dze!+tSYW_cD$p{$ zssjDD4>cOLBPn@jx>3>XU7qo6je3s-xEDDF{a}TTo`M?4!T4gT7Y4@I>3}A)Dj@1J zh?V&wLs{)~8y6m@l)jWexYHBdDfB_tY?pJ`+m%!q?f zUhyU)yHVQkNe}6f|2?sCoH_W@Q3Z6hO_oJ!g?ebLUI3;GQl z)4oofz_Iq>0W}Rf_v+dsMA4&dn^`ta;lW(XU7~gB!Y;((-p1Onb{epAm|4o!q+JO; zJH;f9ime?z2CF*Nu)nm$fNSP@F*dC*Eo1pfvf{8VjhTM`ng#55^!{J(YYIgMR z+KZ6T@PI($d@|Z*moVtSqOFS#OwU!fi0Za3IJS1p!HenMmOW7GPQV?V4G&}I(L3pa z3s|DGQ>&RAud#i=ouylM_MCsp`e8olXn44CKLRZ7q!V|p-2D?k@VIf*4$2ZMAfB9r z7GpIvw$fNVc)$>02uk_*<)%TgUQoiKW(tWwyh&p^-&5M`+Qm)PT;km^QP-mw(MVTy z(q$8sxLF}F0nDu#du}(qzM5YD#{Dk$<9+2&&YL~==?`xjiyT@SjK)-#aD2!erb?a2 zi%g`2RMY1R9A6J67XZhfzmd9-YYfNdD=Lu>r!O%BW0gehf*fFGXe^#9q9#QTc;!Rk zcsK4@*Q)p9?M+b6XLw5IQ^Cp(GlYo(tOD>ejfo8>p!nYoQA^m8$OaI)k!db8B_e-b z4SCepebfUIx$+PpnS+4 zVT%)E6Z$*bx9~rI?)&dmGjkiZsGmDLBssXr_@Fi8}DFc={% z6A~Dv(?94n9atS85a_h7wjR0=esG`Ec0kW@!J8K?-+!0)@NsNIhx}wLo z;a+MlWk7@0W=86mpu{<|Q(Cw{XV=oCQR=|tsoh4k^LAoGj46*w4bL&D&=38|CZV}f zCyZ#s2L)4`sv9Y4HLdH?K^V>lO_`&TZ&C2htues;@Ze=MMb!Yu7$nT?ubUOViI>G~ zTo5%|Oy0~35pS{~NluHirg9^8?ZQ}ikL{z5!B|k9n@Wo`(>yUA!C&V;PHy$;nd)=} z`VHOASmU&414lZ~?%#W8yPCDy53}qJr+IY4I9)XCg#qK1`Hv>pl>(U z&e}aN?VHqkX`A|PONelf$?zG_u6AS=7eNK5R`SA{^4zH;0@Y~(CD5;5DuI;d(q_+IVNz=%`w;W0N8~^6U~HJk)ky)_!1HM*oOw)N02jTlQ-raB$o=PW)p zJNnX$m1J42)w%5?@Fuo#NiY?9Hb%O0hRkdXs zwr+l5JselO7d{44{QY~iPl)nPXr5xbVgyrM76}O#i{lp5X&V6LWJH?1L$+(Aq_o@x zUssi@Fj`75%&Ij&e7(UdUTT+ z=zFZ?^v%X(=TFvC=3l)_u!4%N!$^2X*Y3gg4XiR@0A|~(r)@)P^7otdZRzs|4-7Jz z`}S*M;bhUQeX>Q<)`yyR7|^t-Yhw$){*9Zo;@V;*<}3Cn;>7o8Js#(>W+;&>qy*E&)WL{wVp)GHM@(5+ut-u?c4Ep( zm8x6kkbsV=Lz+~1`~RSCktgt7EX{aL!yqVY7mF!7@^@8qN2;w2Y_#hv}M+DPVcEjz3YQ zEUsAnvh-K7>CrJ2l$c9L%qMr%UQvNn?R7=3I$nQ8r#kivNQIo8jxA><@~+~2GC36y zI`w={H~lxha>4S&9TsJ|4y--%6nC%xos)CYmp0B^)K(eK)rP~WM%+j7&tLHmQ*rnOu$8%^3ZEfPpQB9>(y%n>R8YF3hh8Dh z#&l&c2~@Z7C&a&vw?CN_9nC`o&Zq!I;PCI;;M9j)gB+%2l z6Nm~vo&jVM3H0z`#qjY6bZ0UPS+*P_=^kn8+Q;9pmA>U%SWB`NbpAUZ0rZ%8!a&+a zwM}OXtyqfnbCbB7Kck(iqsRO$!r~eFWAluy729@}Z%MbMKRD&TMTEuQ5n*w)xZJvT zxY{eCck|d?x(JJ4v_TWTRC`6oXbXgf5>refkAKcw`!5-73*x|N>*V~2(Z*1PFy9`Zx5RFm}VGhq1F+Dl^!}lY2!3gCm3DAKf z4xQNzl(iBNEwfg`dK{-c3kWt6fMv@cJe2or)-HPN=4SfxZ0ZVlvzhv`Ro{cb$VlM=5=DfC-Ng=*gGC1Jt{rZ+z)VukG*Yp{tW8@97&nBXQtr=87&14lkOaMc9~VAjz+Q zs(BoDtO!AKR67NNE?sY{gZ-NEuH=?nOL9Y#KA3Liyn>&BJh%iwPV!d~`Py|vzI+wc ztnrTZ92wCz)^h}(B41iwm+VofEBl>OB!76aoaV12WYtPaSFY5`mXiU_@d3$c(m&|b zPVh?7SYTx`(Ur*5fI~L`b<@9F();VJCCu#@)feGBO^D$H8V{z8pE^cO$nbEVzUu6* ztJo_LiDfwv){(1Cv5uS;_xcyzhDYUWF_nyyuPLwT)F*K85a)!Tfdgqi0tv|Ul{mWSwABl+o_aqkBQZWJE52_Qw=6Rue#D6Qgv=2;rwvd? zb`Ej0ciS~3*)gnVXiJ2rb?P#BaF?zF2XRbG9Xc@IKG|xhHw%I^3-3AmU%#i}7w>WERrQ`>KF$Brd!RiX zi%GiIU>;UW54=5iqDml{z_B*U7SMbh3VV`7G8U59iuddL0a6#^$OzDsaKqNvj#uy>FJZ zs&L*3nbM-7yiFAF2fq#rhqQ>0fznFSW#DJo>jM$~)9M9(2K-mnDo{m8tLuMToIbGk zVWlk-nsL*W?=V22%Ji@_HRA-f1mz3O$o|GO)D}rD39Yz=(m1F}VxmjZswxS*29)vs zs>M<+(m+EFWIR12l-ZK)jcKrqITrZ0B(nE{5eH9cpy|Nci%k+~mb`yw(%NBU{~YD` zsoBnbdRVZj0citilXYqRkQVA}OemuKgP1tA9NB-`7l~z$V8`DDjvr7-X7OZc{79Zc zW8{-bj}PyJBk0*DG)@u9-4m3GZ8BNKJq#*X+#4|2)JPh=f3?&n@QsWYBA|3LT|8-d z35EI?ru&=naKecAnT#S$<8`L{^eH62W2AlLg1!h?o%=K0N}J~ur6J_=mcLl_8)}m_ zC+9iGwHwzxZ%A=p2(d$`f#M;ASUZ$!CL443%&VA*L9tC6t{5kI>G&!)LFc$qx5X)% zi$?Sk_p9c_BsiU%I(X1zPLrLQHhF4FYPRymhe@hQ;tR$d2+hdQn80=e5kIrg?VGrv z%G2_@(jF`Y6_6Ha1iyfpvLxjyn-)dBUo`RF`P0dRHg#Q`K)9uu-<>@gmrw+ByP`)< zSbA6|Z(`aDTTxgiWcZougTTMF-wv=%kC6_nksi{S@J>pbV;LvBQ5-=dRIz5!r6AFOPH4&VpwI|!Dq1OS(2~j*ujTdNWVe*O!k5>a7wM|G zEwBHq@|k>8MWEtP#kyy>LNl9d=P3#m@p3z1wnEN7lHpT%id>PwYD9TT@f#Z;+IjF* zlfqKSzvX{oDTL!lflt*QTqC7$Tv3UXmyj}Bku0S=pm;B(SfxuTKcIhN74EYqO87^3 zB40a?8?5XrJuy@FR{2cj8~kzud8O%pIWB)9|yn?;vmrar|?g)Zex{<@TsH5ZYU^K#BX8H378$2rb{URB0amdGRVTcKDiKgd}y zh9{i!G31Q7!)DuHHSqa4^2ts3hR9}4{k?GglhJ&hxM65Q&t%ua?5=a&3p^ij77O3b zcPOOVMbqCE&bnDYYCz99aSjt!v}?O77@>P)yKoB0Q6{&y~Qb6i^DBRkML+5blW#4Jse%lemmc9Rp%b# zT@dGmbatXYHf-xa$jm?17Qf6UvB&AR*Xj6Ud@1!g@EvJ>X;pA|*4W5|n*%hFW47hA z-+s)x5$W8tVP_}*OBuXH(3$&b>z<66aDUM}@C;{BIW2&vtc7U3u{hyw#+u4Q`NPmT zHhli(SqwLY73Gik(PU%W1X~|7b#~B(Q_**6C~V6Pm6B5#gocvTE|Q-IbIAdOz}S`uEe2O3a(412 z-JMs@pT6Q{hoHXQ8a_Wj-W#pSo4)duZ$LsCcg{aQ=O*2A_#}zE|C&Var(H^l2F?vm z-#6?gaV^W z`I(+QIndT8#Jzj0hrCwh*~cGeq#Dse$yIac-+Uo@va|k6UPJLQ!YJ>-%7VA z)@jZsEe@rR0>>2|OXer|N4a>lX-ywp*}3)l{MVJ^NiX7fWZ}<-ZF<Gm(3xt2P3hgK2orR1 z8nH4VHH=BKX^6e$KAqk-7B-OX`yMS{@zVidP~RMQx^&r3duSm%byKs-t@uA(88-tLTc@sKOl8YYmr8_@`|Oha`rFPsBU=$0d;2D&0 zsrTT;C`b@EExGO7dn?%Da%cdt&JJ=uM_{WKZ;KIh8Fkymow(`Bds z(-oH55;zD72OCr1u1jEnxAw8(won&XQqW}vHe_ml5xPJO5x7vou94oLpA-@C4TKUN z2${?fE^)DFG-hre0p)?^ttUt0yTpse0w*WZih>q21xf^WCv2=Uyb6A$-7I0<+n?kX zJb($Y(_QjTg6w!I;~mdoF4EDE1U-mQT9Bqn+uk=m+p%B!Uae7Rn06AC7}#oTd(W zO+WtFYW}UU5Lq|Fc@;Z`vBq_kbz$T8|4>e1$?Mv>E^BM_i8VZWvrZsxN(m!lYYz(v zAwvZ{J(Xwa8aauVs&#_SBN-r^ZhoDsp@nGQ-lQUz8^)?NWK7;IVmS7;W+CY(r|WgC zfH+0-;m9f)tw3T)KV8z1Lq_M_q96YEEdb9WrMdJG+KfKTr={=Uz`-v10U1pA+2T8? zwQ|y46XlPN^KOsDEWZKTa+0bMliL5Cq<@oas&jhJw&UHS&Tcxg$C$>LoglI0#F*q< zyGSR}Uw@~oFL7)3mM$X=PHjI!>MuTCdXD%WehHHPk6YUb&2(*CZ*tXuP5gT*sVS$=8ry&^)C* z`8;`0xux)+a=&Ic*~vFV4(Bi#GceG3@ITyTogqXaBk*!ut)->J+6q+EY7+~bRoNQO zAZ~WQ+K_}WFF2njsZLdTjue$~M*E4$vZ?glCGiDUk2E7izdWT;^se}Z*vFMj>6&$I zF1=1>?W=)2o&c0SofIz7(4M|vH-@;mKCALrmS*Xwr@ zaKSQeXI|#PGa;8|)7#I}Z_~$lq{U^Ej=^Drx?V0j9=5L(GWm}(SmzPfwDME z;xR0WN#Z}&?klz6QMIpDSvQ$k*F&alE|+>;?q;cE{h*U|84;9CZoNH^o13W==SRZo z*)z5EN&`!!CHZDQFhKc&6tU0T1GHAw7%tLGkHzmKI77X z)-i2dquXq6LMKdqR1&|6nC&HS0T@w~8#?^^`K0OJoB9#$^@bbrMXS;uzS8pa@Efv& zG|eGR$d0#Hj?o==b05(IrB7G~azg^v%m|yZCH%5^x1M3a9ZjfVz>@Oa>CcJn>`NAL z`<7>xmVuKjyC!bqLkx=0YhW=O<6LP>t`TGU+k28j$I*OJTd5(n-_mLCD`>7vS#Aft zb6-iPt%P4hV<-k2NLGhvS01WuxPY*-gS#806%VwWpoMvaPeFGuHNF%`1>Is+ew0fP z3zprx$7_G5-)7aPtAD@RD68H>x|sawK}WgX+r9n8+7tKM2fgqORM`}gmrOTJ%{wD% zZfqd^9Z?gA7Qor$9Z_6Fv0C z)m3UEwHvT}MwuQ*mj&MA?(Qrj5gSKGFO0rM+Nj(4)@#tgt4$lU$R4_an5sG!77_o6csr2__)fIXzkINQ{ zA5rbuO_cjba@ltMu)#c%)`$^)h_u{gOD|YVe@wqoc7GeS=wN?`#0U?q_T$fjtJYrm5+OUh>muQchpAN=rx zCS7ds=@)J2r$@F|c<6qp*e9;mZh})0sak-vK<>BT>EpfH{jqaI1*b&I(mL4o>A%B*%a5{)sYF+6MKauWF=cdJ$E{ytB9nl9x zixgLCS7^hS?Cx)7qtn6Zywub)A1I!SU)z1j!ExS)+kaDDl)k5rR`%4VhM{+Y*RarK z3AGHRcb*sgwNu#{w3kOcIhb&g2Yo#oABLlAiK>6{H;I`AnNfB}+U>3=3Ys=Ou=Tc^ zwBb{eMm1{GXx_G~{m_VIeHAqf6ei~IHqnm;YlF0|XxAIs^B-bynrTLZHA?Bu_u3Wu zJ0bLi)&ps7;!Zru0lZf@1j|exY;0`hb%)b;=6xISwQvHJ&hzl72%RU+YwTy#)R>up zfAOZJTl-B*y5RIo2})V?KC{F*t`CGDNUvw~s@ zmMsl)Pl@)#vGP0FcZktrF3oeHm%^q0Nc$cNz^BC?3M#au&-JH(*;O$U90> ze$}-tyJj|6vOXHHw`7sjNabNL4wF{e(V53@f>jJQD?~9#)8tovC4Cu17jRALb4|%9 z%}464`BQcEPd`J;xYsnNqOsx$6J-Ibg@|eLd4`MSw&2Jy?6Nz73pljvGO;09r)5^i zvOu;+A;jk@aSzu8@175cokiWP0m?rP$S|a2&e!2?-4u@ZrP$e6Ek6a|Wjbd2Q}jkL zfx_A=tw;BvZF+SxZ`q(#OkqgsqL>z&v88*`?c+(U+x&hF3qiE)~=blaoOx9E}Q#T`gU z!$Q*VLg$R5nNb@-nu`)nh%XJ`G@NZbepSA5UM}WjiuxCEk73f5IR#PwH2_j#dy17-X!FZNQHw&;bn8VHSwC>;T~bvV|lmV(nle6!sv! z+(w|sXS#f}I~T(E7isW$dF}>rE@X~*$IhZ&lV->*D(}NX|0;4-EPROVXQ{_SaXtSS zXEfqZY7M8AKTx`2OnRY$gM3g^Q_6SO81nBCpbY;UMPP!XsN+f;8mk~n7Bkpd7yQeQ zjR^K<=>hEB60rwSLZBmGFQIU}ViMtLBwXkACaJ8T2wIm)W3yQ?r^}mEJ|OE*%xk)0 zEBi@|fhmK@3y~&sULS&jO_SSjy*fO~p zKZ_fws~yQCa;l+u#UK&AtaQ+`E pjVyK_kxo9cQ-2UU^-Gbv4@;@jIY%kA?mpt8$aVJzk)QDU{{UBU-YozC literal 0 HcmV?d00001 diff --git a/bskyogcard/src/bin.ts b/bskyogcard/src/bin.ts new file mode 100644 index 0000000000..ff550809db --- /dev/null +++ b/bskyogcard/src/bin.ts @@ -0,0 +1,48 @@ +import cluster, {Worker} from 'node:cluster' + +import {envInt} from '@atproto/common' + +import {CardService, envToCfg, httpLogger, readEnv} from './index.js' + +async function main() { + const env = readEnv() + const cfg = envToCfg(env) + const card = await CardService.create(cfg) + await card.start() + httpLogger.info('card service is running') + process.on('SIGTERM', async () => { + httpLogger.info('card service is stopping') + await card.destroy() + httpLogger.info('card service is stopped') + if (cluster.isWorker) process.exit(0) + }) +} + +const workerCount = envInt('CARD_CLUSTER_WORKER_COUNT') + +if (workerCount) { + if (cluster.isPrimary) { + httpLogger.info(`primary ${process.pid} is running`) + const workers = new Set() + for (let i = 0; i < workerCount; ++i) { + workers.add(cluster.fork()) + } + let teardown = false + cluster.on('exit', worker => { + workers.delete(worker) + if (!teardown) { + workers.add(cluster.fork()) // restart on crash + } + }) + process.on('SIGTERM', () => { + teardown = true + httpLogger.info('disconnecting workers') + workers.forEach(w => w.kill('SIGTERM')) + }) + } else { + httpLogger.info(`worker ${process.pid} is running`) + main() + } +} else { + main() // non-clustering +} diff --git a/bskyogcard/src/components/Butterfly.tsx b/bskyogcard/src/components/Butterfly.tsx new file mode 100644 index 0000000000..5a4124975c --- /dev/null +++ b/bskyogcard/src/components/Butterfly.tsx @@ -0,0 +1,16 @@ +import React from 'react' + +export function Butterfly(props: React.SVGAttributes) { + return ( + + + + ) +} diff --git a/bskyogcard/src/components/Img.tsx b/bskyogcard/src/components/Img.tsx new file mode 100644 index 0000000000..dac223180c --- /dev/null +++ b/bskyogcard/src/components/Img.tsx @@ -0,0 +1,10 @@ +import React from 'react' + +export function Img( + props: Omit, 'src'> & {src: Buffer}, +) { + const {src, ...others} = props + return ( + + ) +} diff --git a/bskyogcard/src/components/StarterPack.tsx b/bskyogcard/src/components/StarterPack.tsx new file mode 100644 index 0000000000..f73442190c --- /dev/null +++ b/bskyogcard/src/components/StarterPack.tsx @@ -0,0 +1,149 @@ +/* eslint-disable bsky-internal/avoid-unwrapped-text */ +import React from 'react' +import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' + +import {Butterfly} from './Butterfly.js' +import {Img} from './Img.js' + +export const STARTERPACK_HEIGHT = 630 +export const STARTERPACK_WIDTH = 1200 +export const TILE_SIZE = STARTERPACK_HEIGHT / 3 + +const GRADIENT_TOP = '#0A7AFF' +const GRADIENT_BOTTOM = '#59B9FF' +const IMAGE_STROKE = '#359CFF' + +export function StarterPack(props: { + starterPack: AppBskyGraphDefs.StarterPackView + images: Map +}) { + const {starterPack, images} = props + const record = AppBskyGraphStarterpack.isRecord(starterPack.record) + ? starterPack.record + : null + const imagesArray = [...images.values()] + const imageOfCreator = images.get(starterPack.creator.did) + const imagesExceptCreator = [...images.entries()] + .filter(([did]) => did !== starterPack.creator.did) + .map(([, image]) => image) + const imagesAcross: Buffer[] = [] + if (imageOfCreator) { + if (imagesExceptCreator.length >= 6) { + imagesAcross.push(...imagesExceptCreator.slice(0, 3)) + imagesAcross.push(imageOfCreator) + imagesAcross.push(...imagesExceptCreator.slice(3, 6)) + } else { + const firstHalf = Math.floor(imagesExceptCreator.length / 2) + imagesAcross.push(...imagesExceptCreator.slice(0, firstHalf)) + imagesAcross.push(imageOfCreator) + imagesAcross.push( + ...imagesExceptCreator.slice(firstHalf, imagesExceptCreator.length), + ) + } + } else { + imagesAcross.push(...imagesExceptCreator.slice(0, 7)) + } + return ( +

+ {/* image tiles */} +
+ {[...Array(18)].map((_, i) => { + const image = imagesArray.at(i % imagesArray.length) + return ( +
+ {image && } +
+ ) + })} + {/* background overlay */} +
+
+ {/* foreground text & images */} +
+
+ JOIN THE CONVERSATION +
+
+ {imagesAcross.map((image, i) => { + return ( +
+ +
+ ) + })} +
+
+ {record?.name || 'Starter Pack'} +
+
+ on Bluesky +
+
+
+ ) +} diff --git a/bskyogcard/src/config.ts b/bskyogcard/src/config.ts new file mode 100644 index 0000000000..fafa18e743 --- /dev/null +++ b/bskyogcard/src/config.ts @@ -0,0 +1,40 @@ +import {envInt, envStr} from '@atproto/common' + +export type Config = { + service: ServiceConfig +} + +export type ServiceConfig = { + port: number + version?: string + appviewUrl: string + originVerify?: string +} + +export type Environment = { + port?: number + version?: string + appviewUrl?: string + originVerify?: string +} + +export const readEnv = (): Environment => { + return { + port: envInt('CARD_PORT'), + version: envStr('CARD_VERSION'), + appviewUrl: envStr('CARD_APPVIEW_URL'), + originVerify: envStr('CARD_ORIGIN_VERIFY'), + } +} + +export const envToCfg = (env: Environment): Config => { + const serviceCfg: ServiceConfig = { + port: env.port ?? 3000, + version: env.version, + appviewUrl: env.appviewUrl ?? 'https://api.bsky.app', + originVerify: env.originVerify, + } + return { + service: serviceCfg, + } +} diff --git a/bskyogcard/src/context.ts b/bskyogcard/src/context.ts new file mode 100644 index 0000000000..f92651cafb --- /dev/null +++ b/bskyogcard/src/context.ts @@ -0,0 +1,44 @@ +import {readFileSync} from 'node:fs' + +import {AtpAgent} from '@atproto/api' +import * as path from 'path' +import {fileURLToPath} from 'url' + +import {Config} from './config.js' + +const __DIRNAME = path.dirname(fileURLToPath(import.meta.url)) + +export type AppContextOptions = { + cfg: Config + appviewAgent: AtpAgent + fonts: {name: string; data: Buffer}[] +} + +export class AppContext { + cfg: Config + appviewAgent: AtpAgent + fonts: {name: string; data: Buffer}[] + abortController = new AbortController() + + constructor(private opts: AppContextOptions) { + this.cfg = this.opts.cfg + this.appviewAgent = this.opts.appviewAgent + this.fonts = this.opts.fonts + } + + static async fromConfig(cfg: Config, overrides?: Partial) { + const appviewAgent = new AtpAgent({service: cfg.service.appviewUrl}) + const fonts = [ + { + name: 'Inter', + data: readFileSync(path.join(__DIRNAME, 'assets', 'Inter-Bold.ttf')), + }, + ] + return new AppContext({ + cfg, + appviewAgent, + fonts, + ...overrides, + }) + } +} diff --git a/bskyogcard/src/index.ts b/bskyogcard/src/index.ts new file mode 100644 index 0000000000..ef8d48494d --- /dev/null +++ b/bskyogcard/src/index.ts @@ -0,0 +1,41 @@ +import events from 'node:events' +import http from 'node:http' + +import express from 'express' +import {createHttpTerminator, HttpTerminator} from 'http-terminator' + +import {Config} from './config.js' +import {AppContext} from './context.js' +import {default as routes, errorHandler} from './routes/index.js' + +export * from './config.js' +export * from './logger.js' + +export class CardService { + public server?: http.Server + private terminator?: HttpTerminator + + constructor(public app: express.Application, public ctx: AppContext) {} + + static async create(cfg: Config): Promise { + let app = express() + + const ctx = await AppContext.fromConfig(cfg) + app = routes(ctx, app) + app.use(errorHandler) + + return new CardService(app, ctx) + } + + async start() { + this.server = this.app.listen(this.ctx.cfg.service.port) + this.server.keepAliveTimeout = 90000 + this.terminator = createHttpTerminator({server: this.server}) + await events.once(this.server, 'listening') + } + + async destroy() { + this.ctx.abortController.abort() + await this.terminator?.terminate() + } +} diff --git a/bskyogcard/src/logger.ts b/bskyogcard/src/logger.ts new file mode 100644 index 0000000000..04b5d90469 --- /dev/null +++ b/bskyogcard/src/logger.ts @@ -0,0 +1,3 @@ +import {subsystemLogger} from '@atproto/common' + +export const httpLogger = subsystemLogger('bskyogcard') diff --git a/bskyogcard/src/routes/health.ts b/bskyogcard/src/routes/health.ts new file mode 100644 index 0000000000..0cc69515eb --- /dev/null +++ b/bskyogcard/src/routes/health.ts @@ -0,0 +1,14 @@ +import {Express} from 'express' + +import {AppContext} from '../context.js' +import {handler} from './util.js' + +export default function (ctx: AppContext, app: Express) { + return app.get( + '/_health', + handler(async (_req, res) => { + const {version} = ctx.cfg.service + return res.send({version}) + }), + ) +} diff --git a/bskyogcard/src/routes/index.ts b/bskyogcard/src/routes/index.ts new file mode 100644 index 0000000000..0c40f89d3b --- /dev/null +++ b/bskyogcard/src/routes/index.ts @@ -0,0 +1,13 @@ +import {Express} from 'express' + +import {AppContext} from '../context.js' +import {default as health} from './health.js' +import {default as starterPack} from './starter-pack.js' + +export * from './util.js' + +export default function (ctx: AppContext, app: Express) { + app = health(ctx, app) // GET /_health + app = starterPack(ctx, app) // GET /start/:actor/:rkey + return app +} diff --git a/bskyogcard/src/routes/starter-pack.tsx b/bskyogcard/src/routes/starter-pack.tsx new file mode 100644 index 0000000000..cb3a553272 --- /dev/null +++ b/bskyogcard/src/routes/starter-pack.tsx @@ -0,0 +1,102 @@ +import assert from 'node:assert' + +import React from 'react' +import {AppBskyGraphDefs, AtUri} from '@atproto/api' +import resvg from '@resvg/resvg-js' +import {Express} from 'express' +import satori from 'satori' + +import { + StarterPack, + STARTERPACK_HEIGHT, + STARTERPACK_WIDTH, +} from '../components/StarterPack.js' +import {AppContext} from '../context.js' +import {httpLogger} from '../logger.js' +import {handler, originVerifyMiddleware} from './util.js' + +export default function (ctx: AppContext, app: Express) { + return app.get( + '/start/:actor/:rkey', + originVerifyMiddleware(ctx), + handler(async (req, res) => { + const {actor, rkey} = req.params + const uri = AtUri.make(actor, 'app.bsky.graph.starterpack', rkey) + let starterPack: AppBskyGraphDefs.StarterPackView + try { + const result = await ctx.appviewAgent.api.app.bsky.graph.getStarterPack( + {starterPack: uri.toString()}, + ) + starterPack = result.data.starterPack + } catch (err) { + httpLogger.warn( + {err, uri: uri.toString()}, + 'could not fetch starter pack', + ) + return res.status(404).end('not found') + } + const imageEntries = await Promise.all( + [starterPack.creator] + .concat(starterPack.listItemsSample.map(li => li.subject)) + // has avatar + .filter(p => p.avatar) + // no sensitive labels + .filter(p => !p.labels.some(l => hideAvatarLabels.has(l.val))) + .map(async p => { + try { + assert(p.avatar) + const image = await getImage(p.avatar) + return [p.did, image] as const + } catch (err) { + httpLogger.warn( + {err, uri: uri.toString(), did: p.did}, + 'could not fetch image', + ) + return [p.did, null] as const + } + }), + ) + const images = new Map( + imageEntries.filter(([_, image]) => image !== null).slice(0, 7), + ) + const svg = await satori( + , + { + fonts: ctx.fonts, + height: STARTERPACK_HEIGHT, + width: STARTERPACK_WIDTH, + }, + ) + const output = await resvg.renderAsync(svg) + res.statusCode = 200 + res.setHeader('content-type', 'image/png') + res.setHeader('cdn-tag', [...images.keys()].join(',')) + return res.end(output.asPng()) + }), + ) +} + +async function getImage(url: string) { + const response = await fetch(url) + const arrayBuf = await response.arrayBuffer() // must drain body even if it will be discarded + if (response.status !== 200) return null + return Buffer.from(arrayBuf) +} + +const hideAvatarLabels = new Set([ + '!hide', + '!warn', + 'porn', + 'sexual', + 'nudity', + 'sexual-figurative', + 'graphic-media', + 'self-harm', + 'sensitive', + 'security', + 'impersonation', + 'scam', + 'spam', + 'misleading', + 'inauthentic', +]) diff --git a/bskyogcard/src/routes/util.ts b/bskyogcard/src/routes/util.ts new file mode 100644 index 0000000000..718ed592a1 --- /dev/null +++ b/bskyogcard/src/routes/util.ts @@ -0,0 +1,36 @@ +import {ErrorRequestHandler, Request, RequestHandler, Response} from 'express' + +import {AppContext} from '../context.js' +import {httpLogger} from '../logger.js' + +export type Handler = (req: Request, res: Response) => Awaited + +export const handler = (runHandler: Handler): RequestHandler => { + return async (req, res, next) => { + try { + await runHandler(req, res) + } catch (err) { + next(err) + } + } +} + +export function originVerifyMiddleware(ctx: AppContext): RequestHandler { + const {originVerify} = ctx.cfg.service + if (!originVerify) return (_req, _res, next) => next() + return (req, res, next) => { + const verifyHeader = req.headers['x-origin-verify'] + if (verifyHeader !== originVerify) { + return res.status(404).end('not found') + } + next() + } +} + +export const errorHandler: ErrorRequestHandler = (err, req, res, next) => { + httpLogger.error({err}, 'request error') + if (res.headersSent) { + return next(err) + } + return res.status(500).end('server error') +} diff --git a/bskyogcard/tsconfig.json b/bskyogcard/tsconfig.json new file mode 100644 index 0000000000..a5c3beecb1 --- /dev/null +++ b/bskyogcard/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "esModuleInterop": true, + "moduleResolution": "NodeNext", + "jsx": "react-jsx", + "outDir": "dist" + }, + "include": ["./src/index.ts", "./src/bin.ts"] +} diff --git a/bskyogcard/yarn.lock b/bskyogcard/yarn.lock new file mode 100644 index 0000000000..0403efb84e --- /dev/null +++ b/bskyogcard/yarn.lock @@ -0,0 +1,1113 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@atproto/api@0.12.19-next.0": + version "0.12.19-next.0" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.19-next.0.tgz#9592476cbdba8482d0fd8d65e20275c95d6d5fd4" + integrity sha512-wyWr4uIabTgDTBY99y3QyrFxcIx1Mh4DkURgSv8sd/b+w0lfrZAJh0Gg9BXdg/iIjcf/M2lCTL04r0vASfkMVg== + dependencies: + "@atproto/common-web" "^0.3.0" + "@atproto/lexicon" "^0.4.0" + "@atproto/syntax" "^0.3.0" + "@atproto/xrpc" "^0.5.0" + multiformats "^9.9.0" + tlds "^1.234.0" + +"@atproto/common-web@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.3.0.tgz#36da8c2c31d8cf8a140c3c8f03223319bf4430bb" + integrity sha512-67VnV6JJyX+ZWyjV7xFQMypAgDmjVaR9ZCuU/QW+mqlqI7fex2uL4Fv+7/jHadgzhuJHVd6OHOvNn0wR5WZYtA== + dependencies: + graphemer "^1.4.0" + multiformats "^9.9.0" + uint8arrays "3.0.0" + zod "^3.21.4" + +"@atproto/common@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.4.0.tgz#d77696c7eb545426df727837d9ee333b429fe7ef" + integrity sha512-yOXuPlCjT/OK9j+neIGYn9wkxx/AlxQSucysAF0xgwu0Ji8jAtKBf9Jv6R5ObYAjAD/kVUvEYumle+Yq/R9/7g== + dependencies: + "@atproto/common-web" "^0.3.0" + "@ipld/dag-cbor" "^7.0.3" + cbor-x "^1.5.1" + iso-datestring-validator "^2.2.2" + multiformats "^9.9.0" + pino "^8.15.0" + +"@atproto/lexicon@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.0.tgz#63e8829945d80c25524882caa8ed27b1151cc576" + integrity sha512-RvCBKdSI4M8qWm5uTNz1z3R2yIvIhmOsMuleOj8YR6BwRD+QbtUBy3l+xQ7iXf4M5fdfJFxaUNa6Ty0iRwdKqQ== + dependencies: + "@atproto/common-web" "^0.3.0" + "@atproto/syntax" "^0.3.0" + iso-datestring-validator "^2.2.2" + multiformats "^9.9.0" + zod "^3.21.4" + +"@atproto/syntax@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.3.0.tgz#fafa2dbea9add37253005cb663e7373e05e618b3" + integrity sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA== + +"@atproto/xrpc@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.5.0.tgz#dacbfd8f7b13f0ab5bd56f8fdd4b460e132a6032" + integrity sha512-swu+wyOLvYW4l3n+VAuJbHcPcES+tin2Lsrp8Bw5aIXIICiuFn1YMFlwK9JwVUzTH21Py1s1nHEjr4CJeElJog== + dependencies: + "@atproto/lexicon" "^0.4.0" + zod "^3.21.4" + +"@cbor-extract/cbor-extract-darwin-arm64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz#8d65cb861a99622e1b4a268e2d522d2ec6137338" + integrity sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w== + +"@cbor-extract/cbor-extract-darwin-x64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz#9fbec199c888c5ec485a1839f4fad0485ab6c40a" + integrity sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w== + +"@cbor-extract/cbor-extract-linux-arm64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz#bf77e0db4a1d2200a5aa072e02210d5043e953ae" + integrity sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ== + +"@cbor-extract/cbor-extract-linux-arm@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz#491335037eb8533ed8e21b139c59f6df04e39709" + integrity sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q== + +"@cbor-extract/cbor-extract-linux-x64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz#672574485ccd24759bf8fb8eab9dbca517d35b97" + integrity sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw== + +"@cbor-extract/cbor-extract-win32-x64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz#4b3f07af047f984c082de34b116e765cb9af975f" + integrity sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w== + +"@ipld/dag-cbor@^7.0.3": + version "7.0.3" + resolved "https://registry.yarnpkg.com/@ipld/dag-cbor/-/dag-cbor-7.0.3.tgz#aa31b28afb11a807c3d627828a344e5521ac4a1e" + integrity sha512-1VVh2huHsuohdXC1bGJNE8WR72slZ9XE2T3wbBBq31dm7ZBatmKLLxrB+XAqafxfRFjv08RZmj/W/ZqaM13AuA== + dependencies: + cborg "^1.6.0" + multiformats "^9.5.4" + +"@resvg/resvg-js-android-arm-eabi@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz#e761e0b688127db64879f455178c92468a9aeabe" + integrity sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA== + +"@resvg/resvg-js-android-arm64@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz#b8cb564d7f6b3f37d9b43129f5dc5fe171e249e4" + integrity sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ== + +"@resvg/resvg-js-darwin-arm64@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz#49bd3faeda5c49f53302d970e6e79d006de18e7d" + integrity sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A== + +"@resvg/resvg-js-darwin-x64@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz#e1344173aa27bfb4d880ab576d1acf1c1648faca" + integrity sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw== + +"@resvg/resvg-js-linux-arm-gnueabihf@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz#34c445eba45efd68f6130b2ab426d76a7424253d" + integrity sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw== + +"@resvg/resvg-js-linux-arm64-gnu@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz#30da47087dd8153182198b94fe9f8d994890dae5" + integrity sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg== + +"@resvg/resvg-js-linux-arm64-musl@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz#5d75b8ff5c83103729c1ca3779987302753c50d4" + integrity sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg== + +"@resvg/resvg-js-linux-x64-gnu@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz#411abedfaee5edc57cbb7701736cecba522e26f3" + integrity sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw== + +"@resvg/resvg-js-linux-x64-musl@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz#fe4984038f0372f279e3ff570b72934dd7eb2a5c" + integrity sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ== + +"@resvg/resvg-js-win32-arm64-msvc@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz#d3a053cf7ff687087a2106330c0fdaae706254d1" + integrity sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ== + +"@resvg/resvg-js-win32-ia32-msvc@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz#7cdda1ce29ef7209e28191d917fa5bef0624a4ad" + integrity sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w== + +"@resvg/resvg-js-win32-x64-msvc@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz#cb0ad04525d65f3def4c8d346157a57976d5b388" + integrity sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ== + +"@resvg/resvg-js@^2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js/-/resvg-js-2.6.2.tgz#3e92a907d88d879256c585347c5b21a7f3bb5b46" + integrity sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q== + optionalDependencies: + "@resvg/resvg-js-android-arm-eabi" "2.6.2" + "@resvg/resvg-js-android-arm64" "2.6.2" + "@resvg/resvg-js-darwin-arm64" "2.6.2" + "@resvg/resvg-js-darwin-x64" "2.6.2" + "@resvg/resvg-js-linux-arm-gnueabihf" "2.6.2" + "@resvg/resvg-js-linux-arm64-gnu" "2.6.2" + "@resvg/resvg-js-linux-arm64-musl" "2.6.2" + "@resvg/resvg-js-linux-x64-gnu" "2.6.2" + "@resvg/resvg-js-linux-x64-musl" "2.6.2" + "@resvg/resvg-js-win32-arm64-msvc" "2.6.2" + "@resvg/resvg-js-win32-ia32-msvc" "2.6.2" + "@resvg/resvg-js-win32-x64-msvc" "2.6.2" + +"@shuding/opentype.js@1.4.0-beta.0": + version "1.4.0-beta.0" + resolved "https://registry.yarnpkg.com/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz#5d1e7e9e056f546aad41df1c5043f8f85d39e24b" + integrity sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA== + dependencies: + fflate "^0.7.3" + string.prototype.codepointat "^0.2.1" + +"@types/node@^20.14.3": + version "20.14.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.3.tgz#7a9a5d009b0861e7f337166dc435dbfd758db92d" + integrity sha512-Nuzqa6WAxeGnve6SXqiPAM9rA++VQs+iLZ1DDd56y0gdvygSZlQvZuvdFPR3yLqkVxPu4WrO02iDEyH1g+wazw== + dependencies: + undici-types "~5.26.4" + +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +atomic-sleep@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" + integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== + +base64-js@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.8.tgz#1101e9544f4a76b1bc3b26d452ca96d7a35e7978" + integrity sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +body-parser@1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +boolean@^3.1.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b" + integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw== + +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +camelize@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.1.tgz#89b7e16884056331a35d6b5ad064332c91daa6c3" + integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ== + +cbor-extract@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cbor-extract/-/cbor-extract-2.2.0.tgz#cee78e630cbeae3918d1e2e58e0cebaf3a3be840" + integrity sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA== + dependencies: + node-gyp-build-optional-packages "5.1.1" + optionalDependencies: + "@cbor-extract/cbor-extract-darwin-arm64" "2.2.0" + "@cbor-extract/cbor-extract-darwin-x64" "2.2.0" + "@cbor-extract/cbor-extract-linux-arm" "2.2.0" + "@cbor-extract/cbor-extract-linux-arm64" "2.2.0" + "@cbor-extract/cbor-extract-linux-x64" "2.2.0" + "@cbor-extract/cbor-extract-win32-x64" "2.2.0" + +cbor-x@^1.5.1: + version "1.5.9" + resolved "https://registry.yarnpkg.com/cbor-x/-/cbor-x-1.5.9.tgz#ed6b2afcd7884bdd697674bfb7332c1473a13ecf" + integrity sha512-OEI5rEu3MeR0WWNUXuIGkxmbXVhABP+VtgAXzm48c9ulkrsvxshjjk94XSOGphyAKeNGLPfAxxzEtgQ6rEVpYQ== + optionalDependencies: + cbor-extract "^2.2.0" + +cborg@^1.6.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/cborg/-/cborg-1.10.2.tgz#83cd581b55b3574c816f82696307c7512db759a1" + integrity sha512-b3tFPA9pUr2zCUiCfRd2+wok2/LBSNUMKOuRRok+WlvvAgEt/PlbgPTsZUcwCOs53IJvLgTp0eotwtosE6njug== + +color-name@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== + +css-background-parser@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/css-background-parser/-/css-background-parser-0.1.0.tgz#48a17f7fe6d4d4f1bca3177ddf16c5617950741b" + integrity sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA== + +css-box-shadow@1.0.0-3: + version "1.0.0-3" + resolved "https://registry.yarnpkg.com/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz#9eaeb7140947bf5d649fc49a19e4bbaa5f602713" + integrity sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg== + +css-color-keywords@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" + integrity sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg== + +css-to-react-native@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.2.0.tgz#cdd8099f71024e149e4f6fe17a7d46ecd55f1e32" + integrity sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ== + dependencies: + camelize "^1.0.0" + css-color-keywords "^1.0.0" + postcss-value-parser "^4.0.2" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +delay@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" + integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-libc@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +emoji-regex@^10.2.1: + version "10.3.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.3.0.tgz#76998b9268409eb3dae3de989254d456e70cfe23" + integrity sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +escape-html@^1.0.3, escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +express@^4.19.2: + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.2" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.6.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +fast-printf@^1.6.9: + version "1.6.9" + resolved "https://registry.yarnpkg.com/fast-printf/-/fast-printf-1.6.9.tgz#212f56570d2dc8ccdd057ee93d50dd414d07d676" + integrity sha512-FChq8hbz65WMj4rstcQsFB0O7Cy++nmbNfLYnD9cYv2cRn8EG6k/MGn9kO/tjO66t09DLDugj3yL+V2o6Qftrg== + dependencies: + boolean "^3.1.4" + +fast-redact@^3.1.1: + version "3.5.0" + resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.5.0.tgz#e9ea02f7e57d0cd8438180083e93077e496285e4" + integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A== + +fflate@^0.7.3: + version "0.7.4" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.7.4.tgz#61587e5d958fdabb5a9368a302c25363f4f69f50" + integrity sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw== + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +hex-rgb@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/hex-rgb/-/hex-rgb-4.3.0.tgz#af5e974e83bb2fefe44d55182b004ec818c07776" + integrity sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-terminator@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/http-terminator/-/http-terminator-3.2.0.tgz#bc158d2694b733ca4fbf22a35065a81a609fb3e9" + integrity sha512-JLjck1EzPaWjsmIf8bziM3p9fgR1Y3JoUKAkyYEbZmFrIvJM6I8vVJfBGWlEtV9IWOvzNnaTtjuwZeBY2kwB4g== + dependencies: + delay "^5.0.0" + p-wait-for "^3.2.0" + roarr "^7.0.4" + type-fest "^2.3.3" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +iso-datestring-validator@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz#2daa80d2900b7a954f9f731d42f96ee0c19a6895" + integrity sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA== + +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +linebreak@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/linebreak/-/linebreak-1.1.0.tgz#831cf378d98bced381d8ab118f852bd50d81e46b" + integrity sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ== + dependencies: + base64-js "0.0.8" + unicode-trie "^2.0.0" + +loose-envify@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multiformats@^9.4.2, multiformats@^9.5.4, multiformats@^9.9.0: + version "9.9.0" + resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37" + integrity sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +node-gyp-build-optional-packages@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz#52b143b9dd77b7669073cbfe39e3f4118bfc603c" + integrity sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw== + dependencies: + detect-libc "^2.0.1" + +object-inspect@^1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== + +on-exit-leak-free@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8" + integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== + +p-timeout@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" + integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== + dependencies: + p-finally "^1.0.0" + +p-wait-for@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/p-wait-for/-/p-wait-for-3.2.0.tgz#640429bcabf3b0dd9f492c31539c5718cb6a3f1f" + integrity sha512-wpgERjNkLrBiFmkMEjuZJEWKKDrNfHCKA1OhyN1wg1FrLkULbviEy6py1AyJUgZ72YWFbZ38FIpnqvVqAlDUwA== + dependencies: + p-timeout "^3.0.0" + +pako@^0.2.5: + version "0.2.9" + resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" + integrity sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA== + +parse-css-color@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/parse-css-color/-/parse-css-color-0.2.1.tgz#b687a583f2e42e66ffdfce80a570706966e807c9" + integrity sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg== + dependencies: + color-name "^1.1.4" + hex-rgb "^4.1.0" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +pino-abstract-transport@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz#97f9f2631931e242da531b5c66d3079c12c9d1b5" + integrity sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q== + dependencies: + readable-stream "^4.0.0" + split2 "^4.0.0" + +pino-std-serializers@^6.0.0: + version "6.2.2" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz#d9a9b5f2b9a402486a5fc4db0a737570a860aab3" + integrity sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA== + +pino-std-serializers@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz#7c625038b13718dbbd84ab446bd673dc52259e3b" + integrity sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA== + +pino@^8.15.0: + version "8.21.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-8.21.0.tgz#e1207f3675a2722940d62da79a7a55a98409f00d" + integrity sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q== + dependencies: + atomic-sleep "^1.0.0" + fast-redact "^3.1.1" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^1.2.0" + pino-std-serializers "^6.0.0" + process-warning "^3.0.0" + quick-format-unescaped "^4.0.3" + real-require "^0.2.0" + safe-stable-stringify "^2.3.1" + sonic-boom "^3.7.0" + thread-stream "^2.6.0" + +pino@^9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-9.2.0.tgz#e77a9516f3a3e5550d9b76d9f65ac6118ef02bdd" + integrity sha512-g3/hpwfujK5a4oVbaefoJxezLzsDgLcNJeITvC6yrfwYeT9la+edCK42j5QpEQSQCZgTKapXvnQIdgZwvRaZug== + dependencies: + atomic-sleep "^1.0.0" + fast-redact "^3.1.1" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^1.2.0" + pino-std-serializers "^7.0.0" + process-warning "^3.0.0" + quick-format-unescaped "^4.0.3" + real-require "^0.2.0" + safe-stable-stringify "^2.3.1" + sonic-boom "^4.0.1" + thread-stream "^3.0.0" + +postcss-value-parser@^4.0.2, postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +process-warning@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-3.0.0.tgz#96e5b88884187a1dce6f5c3166d611132058710b" + integrity sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +quick-format-unescaped@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" + integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +react@^18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" + integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== + dependencies: + loose-envify "^1.1.0" + +readable-stream@^4.0.0: + version "4.5.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" + integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + +real-require@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78" + integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== + +roarr@^7.0.4: + version "7.21.1" + resolved "https://registry.yarnpkg.com/roarr/-/roarr-7.21.1.tgz#fd6452ca822a65f736c35e5372f04ee9f2ca3851" + integrity sha512-3niqt5bXFY1InKU8HKWqqYTYjtrBaxBMnXELXCXUYgtNYGUtZM5rB46HIC430AyacL95iEniGf7RgqsesykLmQ== + dependencies: + fast-printf "^1.6.9" + safe-stable-stringify "^2.4.3" + semver-compare "^1.0.0" + +safe-buffer@5.2.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-stable-stringify@^2.3.1, safe-stable-stringify@^2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" + integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +satori@^0.10.13: + version "0.10.13" + resolved "https://registry.yarnpkg.com/satori/-/satori-0.10.13.tgz#658a9920f55268d2002819387a80a0b6d4bdc262" + integrity sha512-klCwkVYMQ/ZN5inJLHzrUmGwoRfsdP7idB5hfpJ1jfiJk1ErDitK8Hkc6Kll1+Ox2WtqEuGecSZLnmup3CGzvQ== + dependencies: + "@shuding/opentype.js" "1.4.0-beta.0" + css-background-parser "^0.1.0" + css-box-shadow "1.0.0-3" + css-to-react-native "^3.0.0" + emoji-regex "^10.2.1" + escape-html "^1.0.3" + linebreak "^1.1.0" + parse-css-color "^0.2.1" + postcss-value-parser "^4.2.0" + yoga-wasm-web "^0.3.3" + +semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +side-channel@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +sonic-boom@^3.7.0: + version "3.8.1" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.8.1.tgz#d5ba8c4e26d6176c9a1d14d549d9ff579a163422" + integrity sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg== + dependencies: + atomic-sleep "^1.0.0" + +sonic-boom@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-4.0.1.tgz#515b7cef2c9290cb362c4536388ddeece07aed30" + integrity sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ== + dependencies: + atomic-sleep "^1.0.0" + +split2@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +string.prototype.codepointat@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz#004ad44c8afc727527b108cd462b4d971cd469bc" + integrity sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg== + +string_decoder@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +thread-stream@^2.6.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.7.0.tgz#d8a8e1b3fd538a6cca8ce69dbe5d3d097b601e11" + integrity sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw== + dependencies: + real-require "^0.2.0" + +thread-stream@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-3.1.0.tgz#4b2ef252a7c215064507d4ef70c05a5e2d34c4f1" + integrity sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A== + dependencies: + real-require "^0.2.0" + +tiny-inflate@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4" + integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw== + +tlds@^1.234.0: + version "1.252.0" + resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.252.0.tgz#71d9617f4ef4cc7347843bee72428e71b8b0f419" + integrity sha512-GA16+8HXvqtfEnw/DTcwB0UU354QE1n3+wh08oFjr6Znl7ZLAeUgYzCcK+/CCrOyE0vnHR8/pu3XXG3vDijXpQ== + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +type-fest@^2.3.3: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@^5.4.5: + version "5.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" + integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== + +uint8arrays@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.0.0.tgz#260869efb8422418b6f04e3fac73a3908175c63b" + integrity sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA== + dependencies: + multiformats "^9.4.2" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +unicode-trie@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-trie/-/unicode-trie-2.0.0.tgz#8fd8845696e2e14a8b67d78fa9e0dd2cad62fec8" + integrity sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ== + dependencies: + pako "^0.2.5" + tiny-inflate "^1.0.0" + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +yoga-wasm-web@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz#eb8e9fcb18e5e651994732f19a220cb885d932ba" + integrity sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA== + +zod@^3.21.4: + version "3.23.8" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" + integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== From 5d98b4b06c272d1a3a0b91f4f509c6e733c89d4f Mon Sep 17 00:00:00 2001 From: dan Date: Fri, 21 Jun 2024 00:54:30 +0300 Subject: [PATCH 65/81] Wait for AppView when posting (#4584) --- src/view/com/composer/Composer.tsx | 34 ++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 80bce5351c..9e2f77d4df 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -24,12 +24,18 @@ import Animated, { } from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {LinearGradient} from 'expo-linear-gradient' +import { + AppBskyFeedDefs, + AppBskyFeedGetPostThread, + BskyAgent, +} from '@atproto/api' import {RichText} from '@atproto/api' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {observer} from 'mobx-react-lite' +import {until} from '#/lib/async/until' import { createGIFDescription, parseAltFromGIFDescription, @@ -299,6 +305,17 @@ export const ComposePost = observer(function ComposePost({ langs: toPostLanguages(langPrefs.postLanguage), }) ).uri + try { + await whenAppViewReady(agent, postUri, res => { + const thread = res.data.thread + return AppBskyFeedDefs.isThreadViewPost(thread) + }) + } catch (waitErr: any) { + logger.error(waitErr, { + message: `Waiting for app view failed`, + }) + // Keep going because the post *was* published. + } } catch (e: any) { logger.error(e, { message: `Composer: create post failed`, @@ -756,6 +773,23 @@ function useKeyboardVerticalOffset() { return top + 10 } +async function whenAppViewReady( + agent: BskyAgent, + uri: string, + fn: (res: AppBskyFeedGetPostThread.Response) => boolean, +) { + await until( + 5, // 5 tries + 1e3, // 1s delay between tries + fn, + () => + agent.app.bsky.feed.getPostThread({ + uri, + depth: 0, + }), + ) +} + const styles = StyleSheet.create({ topbarInner: { flexDirection: 'row', From 4d8537bcd46866c9c613cdb8a1ff9ae77ed52dfa Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 20 Jun 2024 15:01:58 -0700 Subject: [PATCH 66/81] center pill text in label pill (#4579) * center pill text * undo --- src/view/com/profile/ProfileCard.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx index 2b0790002d..a3cd5ca1b9 100644 --- a/src/view/com/profile/ProfileCard.tsx +++ b/src/view/com/profile/ProfileCard.tsx @@ -328,6 +328,7 @@ const styles = StyleSheet.create({ borderRadius: 4, paddingHorizontal: 6, paddingVertical: 2, + justifyContent: 'center', }, btn: { paddingVertical: 7, From 4bba59790a04d9c708dd3cbecf96fdab7f306d94 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 20 Jun 2024 17:06:57 -0500 Subject: [PATCH 67/81] Add a11y context (#4586) * Add a11y context * Feedback --- src/App.native.tsx | 45 +++++++++++++++++--------------- src/App.web.tsx | 41 +++++++++++++++-------------- src/state/a11y.tsx | 65 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 40 deletions(-) create mode 100644 src/state/a11y.tsx diff --git a/src/App.native.tsx b/src/App.native.tsx index 18461fdd05..4c73d87525 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -24,6 +24,7 @@ import { import {s} from '#/lib/styles' import {ThemeProvider} from '#/lib/ThemeContext' import {logger} from '#/logger' +import {Provider as A11yProvider} from '#/state/a11y' import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' import {Provider as DialogStateProvider} from '#/state/dialogs' import {Provider as InvitesStateProvider} from '#/state/invites' @@ -152,27 +153,29 @@ function App() { * that is set up in the InnerApp component above. */ return ( - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + ) } diff --git a/src/App.web.tsx b/src/App.web.tsx index 6af3c7d6fb..00939c9eb4 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -13,6 +13,7 @@ import {QueryProvider} from '#/lib/react-query' import {Provider as StatsigProvider} from '#/lib/statsig/statsig' import {ThemeProvider} from '#/lib/ThemeContext' import {logger} from '#/logger' +import {Provider as A11yProvider} from '#/state/a11y' import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' import {Provider as DialogStateProvider} from '#/state/dialogs' import {Provider as InvitesStateProvider} from '#/state/invites' @@ -135,25 +136,27 @@ function App() { * that is set up in the InnerApp component above. */ return ( - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + ) } diff --git a/src/state/a11y.tsx b/src/state/a11y.tsx new file mode 100644 index 0000000000..aefcfd1ec4 --- /dev/null +++ b/src/state/a11y.tsx @@ -0,0 +1,65 @@ +import React from 'react' +import {AccessibilityInfo} from 'react-native' +import {isReducedMotion} from 'react-native-reanimated' + +import {isWeb} from '#/platform/detection' + +const Context = React.createContext({ + reduceMotionEnabled: false, + screenReaderEnabled: false, +}) + +export function useA11y() { + return React.useContext(Context) +} + +export function Provider({children}: React.PropsWithChildren<{}>) { + const [reduceMotionEnabled, setReduceMotionEnabled] = React.useState(() => + isReducedMotion(), + ) + const [screenReaderEnabled, setScreenReaderEnabled] = React.useState(false) + + React.useEffect(() => { + const reduceMotionChangedSubscription = AccessibilityInfo.addEventListener( + 'reduceMotionChanged', + enabled => { + setReduceMotionEnabled(enabled) + }, + ) + const screenReaderChangedSubscription = AccessibilityInfo.addEventListener( + 'screenReaderChanged', + enabled => { + setScreenReaderEnabled(enabled) + }, + ) + + ;(async () => { + const [_reduceMotionEnabled, _screenReaderEnabled] = await Promise.all([ + AccessibilityInfo.isReduceMotionEnabled(), + AccessibilityInfo.isScreenReaderEnabled(), + ]) + setReduceMotionEnabled(_reduceMotionEnabled) + setScreenReaderEnabled(_screenReaderEnabled) + })() + + return () => { + reduceMotionChangedSubscription.remove() + screenReaderChangedSubscription.remove() + } + }, []) + + const ctx = React.useMemo(() => { + return { + reduceMotionEnabled, + /** + * Always returns true on web. For now, we're using this for mobile a11y, + * so we reset to false on web. + * + * @see https://github.com/necolas/react-native-web/discussions/2072 + */ + screenReaderEnabled: isWeb ? false : screenReaderEnabled, + } + }, [reduceMotionEnabled, screenReaderEnabled]) + + return {children} +} From 4c48a1f14b317a76e55c008aeeb834ebb8f416d0 Mon Sep 17 00:00:00 2001 From: dan Date: Fri, 21 Jun 2024 01:47:56 +0300 Subject: [PATCH 68/81] [Session] Logging (#4476) * Add session logging (console.log) * Hook it up for real * Send type separately --- src/state/session/index.tsx | 42 +++++++++-- src/state/session/logging.ts | 137 +++++++++++++++++++++++++++++++++++ src/state/session/reducer.ts | 5 +- 3 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 src/state/session/logging.ts diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index 314945bcf9..3aac19025d 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -19,6 +19,7 @@ import { import {getInitialState, reducer} from './reducer' export {isSignupQueued} from './util' +import {addSessionDebugLog} from './logging' export type {SessionAccount} from '#/state/session/types' import {SessionApiContext, SessionStateContext} from '#/state/session/types' @@ -40,9 +41,11 @@ const ApiContext = React.createContext({ export function Provider({children}: React.PropsWithChildren<{}>) { const cancelPendingTask = useOneTaskAtATime() - const [state, dispatch] = React.useReducer(reducer, null, () => - getInitialState(persisted.get('session').accounts), - ) + const [state, dispatch] = React.useReducer(reducer, null, () => { + const initialState = getInitialState(persisted.get('session').accounts) + addSessionDebugLog({type: 'reducer:init', state: initialState}) + return initialState + }) const onAgentSessionChange = React.useCallback( (agent: BskyAgent, accountDid: string, sessionEvent: AtpSessionEvent) => { @@ -63,6 +66,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const createAccount = React.useCallback( async params => { + addSessionDebugLog({type: 'method:start', method: 'createAccount'}) const signal = cancelPendingTask() track('Try Create Account') logEvent('account:create:begin', {}) @@ -81,12 +85,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) { }) track('Create Account') logEvent('account:create:success', {}) + addSessionDebugLog({type: 'method:end', method: 'createAccount', account}) }, [onAgentSessionChange, cancelPendingTask], ) const login = React.useCallback( async (params, logContext) => { + addSessionDebugLog({type: 'method:start', method: 'login'}) const signal = cancelPendingTask() const {agent, account} = await createAgentAndLogin( params, @@ -103,23 +109,31 @@ export function Provider({children}: React.PropsWithChildren<{}>) { }) track('Sign In', {resumedSession: false}) logEvent('account:loggedIn', {logContext, withPassword: true}) + addSessionDebugLog({type: 'method:end', method: 'login', account}) }, [onAgentSessionChange, cancelPendingTask], ) const logout = React.useCallback( logContext => { + addSessionDebugLog({type: 'method:start', method: 'logout'}) cancelPendingTask() dispatch({ type: 'logged-out', }) logEvent('account:loggedOut', {logContext}) + addSessionDebugLog({type: 'method:end', method: 'logout'}) }, [cancelPendingTask], ) const resumeSession = React.useCallback( async storedAccount => { + addSessionDebugLog({ + type: 'method:start', + method: 'resumeSession', + account: storedAccount, + }) const signal = cancelPendingTask() const {agent, account} = await createAgentAndResume( storedAccount, @@ -134,17 +148,24 @@ export function Provider({children}: React.PropsWithChildren<{}>) { newAgent: agent, newAccount: account, }) + addSessionDebugLog({type: 'method:end', method: 'resumeSession', account}) }, [onAgentSessionChange, cancelPendingTask], ) const removeAccount = React.useCallback( account => { + addSessionDebugLog({ + type: 'method:start', + method: 'removeAccount', + account, + }) cancelPendingTask() dispatch({ type: 'removed-account', accountDid: account.did, }) + addSessionDebugLog({type: 'method:end', method: 'removeAccount', account}) }, [cancelPendingTask], ) @@ -152,18 +173,21 @@ export function Provider({children}: React.PropsWithChildren<{}>) { React.useEffect(() => { if (state.needsPersist) { state.needsPersist = false - persisted.write('session', { + const persistedData = { accounts: state.accounts, currentAccount: state.accounts.find( a => a.did === state.currentAgentState.did, ), - }) + } + addSessionDebugLog({type: 'persisted:broadcast', data: persistedData}) + persisted.write('session', persistedData) } }, [state]) React.useEffect(() => { return persisted.onUpdate(() => { const synced = persisted.get('session') + addSessionDebugLog({type: 'persisted:receive', data: synced}) dispatch({ type: 'synced-accounts', syncedAccounts: synced.accounts, @@ -177,7 +201,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) { resumeSession(syncedAccount) } else { const agent = state.currentAgentState.agent as BskyAgent + const prevSession = agent.session agent.session = sessionAccountToSession(syncedAccount) + addSessionDebugLog({ + type: 'agent:patch', + agent, + prevSession, + nextSession: agent.session, + }) } } }) @@ -215,6 +246,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { // Read the previous value and immediately advance the pointer. const prevAgent = currentAgentRef.current currentAgentRef.current = agent + addSessionDebugLog({type: 'agent:switch', prevAgent, nextAgent: agent}) // We never reuse agents so let's fully neutralize the previous one. // This ensures it won't try to consume any refresh tokens. prevAgent.session = undefined diff --git a/src/state/session/logging.ts b/src/state/session/logging.ts new file mode 100644 index 0000000000..16aa66fe72 --- /dev/null +++ b/src/state/session/logging.ts @@ -0,0 +1,137 @@ +import {AtpSessionData} from '@atproto/api' +import {sha256} from 'js-sha256' +import {Statsig} from 'statsig-react-native-expo' + +import {Schema} from '../persisted' +import {Action, State} from './reducer' +import {SessionAccount} from './types' + +type Reducer = (state: State, action: Action) => State + +type Log = + | { + type: 'reducer:init' + state: State + } + | { + type: 'reducer:call' + action: Action + prevState: State + nextState: State + } + | { + type: 'method:start' + method: + | 'createAccount' + | 'login' + | 'logout' + | 'resumeSession' + | 'removeAccount' + account?: SessionAccount + } + | { + type: 'method:end' + method: + | 'createAccount' + | 'login' + | 'logout' + | 'resumeSession' + | 'removeAccount' + account?: SessionAccount + } + | { + type: 'persisted:broadcast' + data: Schema['session'] + } + | { + type: 'persisted:receive' + data: Schema['session'] + } + | { + type: 'agent:switch' + prevAgent: object + nextAgent: object + } + | { + type: 'agent:patch' + agent: object + prevSession: AtpSessionData | undefined + nextSession: AtpSessionData + } + +export function wrapSessionReducerForLogging(reducer: Reducer): Reducer { + return function loggingWrapper(prevState: State, action: Action): State { + const nextState = reducer(prevState, action) + addSessionDebugLog({type: 'reducer:call', prevState, action, nextState}) + return nextState + } +} + +let nextMessageIndex = 0 +const MAX_SLICE_LENGTH = 1000 + +export function addSessionDebugLog(log: Log) { + try { + if (!Statsig.initializeCalled() || !Statsig.getStableID()) { + // Drop these logs for now. + return + } + if (!Statsig.checkGate('debug_session')) { + return + } + const messageIndex = nextMessageIndex++ + const {type, ...content} = log + let payload = JSON.stringify(content, replacer) + + let nextSliceIndex = 0 + while (payload.length > 0) { + const sliceIndex = nextSliceIndex++ + const slice = payload.slice(0, MAX_SLICE_LENGTH) + payload = payload.slice(MAX_SLICE_LENGTH) + Statsig.logEvent('session:debug', null, { + realmId, + messageIndex: String(messageIndex), + messageType: type, + sliceIndex: String(sliceIndex), + slice, + }) + } + } catch (e) { + console.error(e) + } +} + +let agentIds = new WeakMap() +let realmId = Math.random().toString(36).slice(2) +let nextAgentId = 1 + +function getAgentId(agent: object) { + let id = agentIds.get(agent) + if (id === undefined) { + id = realmId + '::' + nextAgentId++ + agentIds.set(agent, id) + } + return id +} + +function replacer(key: string, value: unknown) { + if (typeof value === 'object' && value != null && 'api' in value) { + return getAgentId(value) + } + if ( + key === 'service' || + key === 'email' || + key === 'emailConfirmed' || + key === 'emailAuthFactor' || + key === 'pdsUrl' + ) { + return undefined + } + if ( + typeof value === 'string' && + (key === 'refreshJwt' || key === 'accessJwt') + ) { + return sha256(value) + } + return value +} diff --git a/src/state/session/reducer.ts b/src/state/session/reducer.ts index 7f30809353..0a537b42c6 100644 --- a/src/state/session/reducer.ts +++ b/src/state/session/reducer.ts @@ -1,6 +1,7 @@ import {AtpSessionEvent} from '@atproto/api' import {createPublicAgent} from './agent' +import {wrapSessionReducerForLogging} from './logging' import {SessionAccount} from './types' // A hack so that the reducer can't read anything from the agent. @@ -64,7 +65,7 @@ export function getInitialState(persistedAccounts: SessionAccount[]): State { } } -export function reducer(state: State, action: Action): State { +let reducer = (state: State, action: Action): State => { switch (action.type) { case 'received-agent-event': { const {agent, accountDid, refreshedAccount, sessionEvent} = action @@ -166,3 +167,5 @@ export function reducer(state: State, action: Action): State { } } } +reducer = wrapSessionReducerForLogging(reducer) +export {reducer} From f344032ed8fde2ecbfc1a71f12373eb013fb4fd7 Mon Sep 17 00:00:00 2001 From: Marco Maroni <166719395+marcomaroni-github@users.noreply.github.com> Date: Fri, 21 Jun 2024 00:48:58 +0200 Subject: [PATCH 69/81] Italian translation fix (#4527) * Update messages.po Italian translation fix (feeds->feed) * Italian localization update Italian localization update (DM) * Update src/locale/locales/it/messages.po Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update src/locale/locales/it/messages.po Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> --- src/locale/locales/it/messages.po | 60 +++++++++++++++---------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/locale/locales/it/messages.po b/src/locale/locales/it/messages.po index e2c2bdb3ac..59660ca2e5 100644 --- a/src/locale/locales/it/messages.po +++ b/src/locale/locales/it/messages.po @@ -165,7 +165,7 @@ msgstr "" #~ msgstr "<0>{following} <1>following" #~ msgid "<0>Choose your<1>Recommended<2>Feeds" -#~ msgstr "<0>Scegli i tuoi<1>feeds<2>consigliati" +#~ msgstr "<0>Scegli i tuoi<1>feed/1><2>consigliati" #~ msgid "<0>Follow some<1>Recommended<2>Users" #~ msgstr "<0>Segui alcuni<1>utenti<2>consigliati" @@ -356,7 +356,7 @@ msgstr "Aggiunto alla lista" #: src/view/com/feeds/FeedSourceCard.tsx:126 msgid "Added to my feeds" -msgstr "Aggiunto ai miei feeds" +msgstr "Aggiunto ai miei feed" #: src/view/screens/PreferencesFollowingFeed.tsx:172 msgid "Adjust the number of likes a reply must have to be shown in your feed." @@ -395,7 +395,7 @@ msgstr "" #: src/screens/Messages/Settings.tsx:62 #: src/screens/Messages/Settings.tsx:65 msgid "Allow new messages from" -msgstr "" +msgstr "Consenti nuovi messaggi da" #: src/screens/Login/ForgotPasswordForm.tsx:178 #: src/view/com/modals/ChangePassword.tsx:171 @@ -936,12 +936,12 @@ msgstr "Conversazione silenziata" #: src/screens/Messages/List/index.tsx:88 #: src/view/screens/Settings/index.tsx:638 msgid "Chat settings" -msgstr "" +msgstr "Impostazioni messaggi" #: src/screens/Messages/Settings.tsx:59 #: src/view/screens/Settings/index.tsx:647 msgid "Chat Settings" -msgstr "" +msgstr "Impostazioni messaggi" #: src/components/dms/ConvoMenu.tsx:84 msgid "Chat unmuted" @@ -1043,7 +1043,7 @@ msgstr "" #: src/screens/Feeds/NoFollowingFeed.tsx:46 #~ msgid "Click here to add one." -#~ msgstr "" +#~ msgstr "Clicca qui per aggiungerne uno." #: src/components/TagMenu/index.web.tsx:138 msgid "Click here to open tag menu for {tag}" @@ -1665,14 +1665,14 @@ msgstr "Scoraggia le app dal mostrare il mio account agli utenti disconnessi" #: src/view/com/posts/FollowingEmptyState.tsx:70 #: src/view/com/posts/FollowingEndOfFeed.tsx:71 msgid "Discover new custom feeds" -msgstr "Scopri nuovi feeds personalizzati" +msgstr "Scopri nuovi feed personalizzati" #~ msgid "Discover new feeds" -#~ msgstr "Scopri nuovi feeds" +#~ msgstr "Scopri nuovi feed" #: src/view/screens/Feeds.tsx:794 msgid "Discover New Feeds" -msgstr "Scopri nuovi feeds" +msgstr "Scopri nuovi feed" #: src/view/com/modals/EditProfile.tsx:193 msgid "Display name" @@ -1831,7 +1831,7 @@ msgstr "Modifica l'elenco di moderazione" #: src/view/screens/Feeds.tsx:469 #: src/view/screens/SavedFeeds.tsx:93 msgid "Edit My Feeds" -msgstr "Modifica i miei feeds" +msgstr "Modifica i miei feed" #: src/view/com/modals/EditProfile.tsx:153 msgid "Edit my profile" @@ -1850,7 +1850,7 @@ msgstr "Modifica il Profilo" #: src/view/com/home/HomeHeaderLayout.web.tsx:76 #: src/view/screens/Feeds.tsx:416 #~ msgid "Edit Saved Feeds" -#~ msgstr "Modifica i feeds memorizzati" +#~ msgstr "Modifica i feed memorizzati" #: src/view/com/modals/CreateOrEditList.tsx:234 msgid "Edit User List" @@ -1927,7 +1927,7 @@ msgstr "Attiva il contenuto per adulti" #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:78 #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:79 #~ msgid "Enable adult content in your feeds" -#~ msgstr "Abilita i contenuti per adulti nei tuoi feeds" +#~ msgstr "Abilita i contenuti per adulti nei tuoi feed" #: src/components/dialogs/EmbedConsent.tsx:82 #: src/components/dialogs/EmbedConsent.tsx:89 @@ -2202,7 +2202,7 @@ msgstr "Commenti" #: src/view/shell/Drawer.tsx:493 #: src/view/shell/Drawer.tsx:494 msgid "Feeds" -msgstr "Feeds" +msgstr "Feed" #~ msgid "Feeds are created by users to curate content. Choose some feeds that you find interesting." #~ msgstr "I feed vengono creati dagli utenti per curare i contenuti. Scegli alcuni feed che ritieni interessanti." @@ -2213,7 +2213,7 @@ msgstr "I feed sono algoritmi personalizzati che gli utenti creano con un minimo #: src/screens/Onboarding/StepTopicalFeeds.tsx:80 #~ msgid "Feeds can be topical as well!" -#~ msgstr "I feeds possono anche avere tematiche!" +#~ msgstr "I feed possono anche avere tematiche!" #: src/view/com/modals/ChangeHandle.tsx:475 msgid "File Contents" @@ -3214,7 +3214,7 @@ msgstr "Il messaggio è troppo lungo" #: src/screens/Messages/List/index.tsx:321 msgid "Message settings" -msgstr "Impostazione messaggio" +msgstr "Impostazioni messaggio" #: src/Navigation.tsx:504 #: src/screens/Messages/List/index.tsx:164 @@ -3412,7 +3412,7 @@ msgstr "Il mio Compleanno" #: src/view/screens/Feeds.tsx:768 msgid "My Feeds" -msgstr "I miei Feeds" +msgstr "I miei Feed" #: src/view/shell/desktop/LeftNav.tsx:84 msgid "My Profile" @@ -3424,7 +3424,7 @@ msgstr "I miei feed salvati" #: src/view/screens/Settings/index.tsx:622 msgid "My Saved Feeds" -msgstr "I miei Feeds Salvati" +msgstr "I miei Feed Salvati" #~ msgid "my-server.com" #~ msgstr "my-server.com" @@ -3862,7 +3862,7 @@ msgstr "Apre la fotocamera sul dispositivo" #: src/view/screens/Settings/index.tsx:639 msgid "Opens chat settings" -msgstr "" +msgstr "Apre impostazioni messaggi" #: src/view/com/composer/Prompt.tsx:27 msgid "Opens composer" @@ -4112,7 +4112,7 @@ msgstr "Fissa su Home" #: src/view/screens/SavedFeeds.tsx:103 msgid "Pinned Feeds" -msgstr "Feeds Fissi" +msgstr "Feed Fissi" #: src/view/screens/ProfileList.tsx:289 msgid "Pinned to your feeds" @@ -4380,7 +4380,7 @@ msgstr "Elenchi pubblici e condivisibili di utenti da disattivare o bloccare in #: src/view/screens/Lists.tsx:66 msgid "Public, shareable lists which can drive feeds." -msgstr "Liste pubbliche e condivisibili che possono impulsare i feeds." +msgstr "Liste pubbliche e condivisibili che possono impulsare i feed." #: src/view/com/composer/Composer.tsx:462 msgid "Publish post" @@ -4431,7 +4431,7 @@ msgid "Recent Searches" msgstr "Ricerche recenti" #~ msgid "Recommended Feeds" -#~ msgstr "Feeds consigliati" +#~ msgstr "Feed consigliati" #~ msgid "Recommended Users" #~ msgstr "Utenti consigliati" @@ -4454,7 +4454,7 @@ msgid "Remove" msgstr "Rimuovi" #~ msgid "Remove {0} from my feeds?" -#~ msgstr "Rimuovere {0} dai miei feeds?" +#~ msgstr "Rimuovere {0} dai miei feed?" #: src/view/com/util/AccountDropdownBtn.tsx:22 msgid "Remove account" @@ -4524,14 +4524,14 @@ msgid "Remove repost" msgstr "Rimuovi la ripubblicazione" #~ msgid "Remove this feed from my feeds?" -#~ msgstr "Rimuovere questo feed dai miei feeds?" +#~ msgstr "Rimuovere questo feed dai miei feed?" #: src/view/com/posts/FeedErrorMessage.tsx:210 msgid "Remove this feed from your saved feeds" msgstr "Rimuovi questo feed dai feed salvati" #~ msgid "Remove this feed from your saved feeds?" -#~ msgstr "Elimina questo feed dai feeds salvati?" +#~ msgstr "Elimina questo feed dai feed salvati?" #: src/view/com/modals/ListAddRemoveUsers.tsx:199 #: src/view/com/modals/UserAddRemoveLists.tsx:165 @@ -4540,7 +4540,7 @@ msgstr "Elimina dalla lista" #: src/view/com/feeds/FeedSourceCard.tsx:139 msgid "Removed from my feeds" -msgstr "Rimuovere dai miei feeds" +msgstr "Rimuovere dai miei feed" #: src/view/com/posts/FeedShutdownMsg.tsx:44 #: src/view/screens/ProfileFeed.tsx:191 @@ -5047,7 +5047,7 @@ msgstr "Seleziona il servizio che ospita i tuoi dati." #: src/screens/Onboarding/StepTopicalFeeds.tsx:100 #~ msgid "Select topical feeds to follow from the list below" -#~ msgstr "Seleziona i feeds con temi da seguire dal seguente elenco" +#~ msgstr "Seleziona i feed con temi da seguire dal seguente elenco" #: src/screens/Onboarding/StepModeration/index.tsx:63 #~ msgid "Select what you want to see (or not see), and we’ll handle the rest." @@ -6154,7 +6154,7 @@ msgid "This will delete {0} from your muted words. You can always add it back la msgstr "Questo eliminerà {0} dalle parole disattivate. Puoi sempre aggiungerla nuovamente in seguito." #~ msgid "This will hide this post from your feeds." -#~ msgstr "Questo nasconderà il post dai tuoi feeds." +#~ msgstr "Questo nasconderà il post dai tuoi feed." #: src/view/screens/Settings/index.tsx:594 msgid "Thread preferences" @@ -6783,7 +6783,7 @@ msgstr "Che lingue sono utilizzate in questo post?" #: src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx:77 msgid "Which languages would you like to see in your algorithmic feeds?" -msgstr "Quali lingue vorresti vedere negli algoritmi dei tuoi feeds?" +msgstr "Quali lingue vorresti vedere negli algoritmi dei tuoi feed?" #: src/components/dms/MessagesNUX.tsx:110 #: src/components/dms/MessagesNUX.tsx:124 @@ -6901,7 +6901,7 @@ msgstr "Puoi modificarlo in qualsiasi momento." #: src/screens/Messages/Settings.tsx:111 msgid "You can continue ongoing conversations regardless of which setting you choose." -msgstr "" +msgstr "Puoi proseguire le conversazioni in corso indipendentemente da quale settaggio scegli." #: src/screens/Login/index.tsx:158 #: src/screens/Login/PasswordUpdatedForm.tsx:33 @@ -6982,7 +6982,7 @@ msgstr "Non hai ancora nessuna conversazione. Avviane una!" #: src/view/com/feeds/ProfileFeedgens.tsx:141 msgid "You have no feeds." -msgstr "Non hai feeds." +msgstr "Non hai feed." #: src/view/com/lists/MyLists.tsx:90 #: src/view/com/lists/ProfileLists.tsx:145 From 8ff862798fdf94ac7d7abbb0111d7f9c6938c7b3 Mon Sep 17 00:00:00 2001 From: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> Date: Fri, 21 Jun 2024 07:49:50 +0900 Subject: [PATCH 70/81] Update Japanese translation (#4477) * Update Japanese translation * Update Japanese translation * Fix translation ref. https://github.com/bluesky-social/social-app/pull/4477#pullrequestreview-2114478514 * Update Japanese translation * Fix translation * Updated Japanese translation * Updated translations --- src/locale/locales/ja/messages.po | 201 ++++++++++++++++++++++-------- 1 file changed, 152 insertions(+), 49 deletions(-) diff --git a/src/locale/locales/ja/messages.po b/src/locale/locales/ja/messages.po index e8dc12ce24..bb41c63317 100644 --- a/src/locale/locales/ja/messages.po +++ b/src/locale/locales/ja/messages.po @@ -8,7 +8,7 @@ msgstr "" "Language: ja\n" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2024-06-05 11:06+0900\n" +"PO-Revision-Date: 2024-06-19 11:10+0900\n" "Last-Translator: tkusano\n" "Language-Team: Hima-Zinn, tkusano, dolciss, oboenikui, noritada, middlingphys, hibiki, reindex-ot, haoyayoi, vyv03354\n" "Plural-Forms: \n" @@ -37,10 +37,6 @@ msgstr "{0, plural, other {#個のラベルがこのコンテンツに適用さ msgid "{0, plural, one {# repost} other {# reposts}}" msgstr "{0, plural, other {#回のリポスト}}" -#: src/components/KnownFollowers.tsx:179 -msgid "{0, plural, one {and # other} other {and # others}}" -msgstr "" - #: src/components/ProfileHoverCard/index.web.tsx:376 #: src/screens/Profile/Header/Metrics.tsx:23 msgid "{0, plural, one {follower} other {followers}}" @@ -87,6 +83,26 @@ msgstr "{0}のアバター" msgid "{count, plural, one {Liked by # user} other {Liked by # users}}" msgstr "{count, plural, other {#人のユーザーがいいね}}" +#: src/lib/hooks/useTimeAgo.ts:69 +msgid "{diff, plural, one {day} other {days}}" +msgstr "{diff, plural, other {日}}" + +#: src/lib/hooks/useTimeAgo.ts:64 +msgid "{diff, plural, one {hour} other {hours}}" +msgstr "{diff, plural, other {時間}}" + +#: src/lib/hooks/useTimeAgo.ts:59 +msgid "{diff, plural, one {minute} other {minutes}}" +msgstr "{diff, plural, other {分}}" + +#: src/lib/hooks/useTimeAgo.ts:75 +msgid "{diff, plural, one {month} other {months}}" +msgstr "{diff, plural, other {ヶ月}}" + +#: src/lib/hooks/useTimeAgo.ts:54 +msgid "{diffSeconds, plural, one {second} other {seconds}}" +msgstr "{diffSeconds, plural, other {秒}}" + #: src/screens/SignupQueued.tsx:207 msgid "{estimatedTimeHrs, plural, one {hour} other {hours}}" msgstr "{estimatedTimeHrs, plural, other {時間}}" @@ -114,6 +130,10 @@ msgstr "{likeCount, plural, other {#人のユーザーがいいね}}" msgid "{numUnreadNotifications} unread" msgstr "{numUnreadNotifications}件の未読" +#: src/components/NewskieDialog.tsx:75 +msgid "{profileName} joined Bluesky {0} ago" +msgstr "{profileName}はBlueskyに{0}前に参加しました" + #: src/view/screens/PreferencesFollowingFeed.tsx:67 msgid "{value, plural, =0 {Show all replies} one {Show replies with at least # like} other {Show replies with at least # likes}}" msgstr "{value, plural, =0 {すべての返信を表示} other {#個以上のいいねがついた返信を表示}}" @@ -270,6 +290,10 @@ msgstr "フォローしているユーザーのみのデフォルトのフィー msgid "Add the following DNS record to your domain:" msgstr "次のDNSレコードをドメインに追加してください:" +#: src/components/FeedCard.tsx:173 +msgid "Add this feed to your feeds" +msgstr "このフィードをあなたのフィードに追加する" + #: src/view/com/profile/ProfileMenu.tsx:265 #: src/view/com/profile/ProfileMenu.tsx:268 msgid "Add to Lists" @@ -469,6 +493,10 @@ msgstr "この会話から退出しますか?あなたのメッセージはあ msgid "Are you sure you want to remove {0} from your feeds?" msgstr "あなたのフィードから{0}を削除してもよろしいですか?" +#: src/components/FeedCard.tsx:190 +msgid "Are you sure you want to remove this from your feeds?" +msgstr "本当にこのフィードをあなたのフィードから削除したいですか?" + #: src/view/com/composer/Composer.tsx:630 msgid "Are you sure you'd like to discard this draft?" msgstr "本当にこの下書きを破棄しますか?" @@ -1092,7 +1120,7 @@ msgstr "{0}として続行(現在サインイン中)" #: src/view/com/post-thread/PostThreadLoadMore.tsx:52 msgid "Continue thread..." -msgstr "" +msgstr "スレッドの続き…" #: src/screens/Onboarding/StepInterests/index.tsx:250 #: src/screens/Onboarding/StepProfile/index.tsx:266 @@ -1419,6 +1447,10 @@ msgstr "アプリがログアウトしたユーザーに自分のアカウント msgid "Discover new custom feeds" msgstr "新しいカスタムフィードを見つける" +#: src/view/screens/Search/Explore.tsx:378 +msgid "Discover new feeds" +msgstr "新しいフィードを探す" + #: src/view/screens/Feeds.tsx:794 msgid "Discover New Feeds" msgstr "新しいフィードを探す" @@ -1534,16 +1566,16 @@ msgstr "例:返信として広告を繰り返し送ってくるユーザー。 msgid "Each code works once. You'll receive more invite codes periodically." msgstr "それぞれのコードは一回限り有効です。定期的に追加の招待コードをお送りします。" -#: src/view/screens/Feeds.tsx:400 -#: src/view/screens/Feeds.tsx:471 -msgid "Edit" -msgstr "" - #: src/view/com/lists/ListMembers.tsx:149 msgctxt "action" msgid "Edit" msgstr "編集" +#: src/view/screens/Feeds.tsx:400 +#: src/view/screens/Feeds.tsx:471 +msgid "Edit" +msgstr "編集" + #: src/view/com/util/UserAvatar.tsx:312 #: src/view/com/util/UserBanner.tsx:92 msgid "Edit avatar" @@ -1583,11 +1615,6 @@ msgstr "プロフィールを編集" msgid "Edit Profile" msgstr "プロフィールを編集" -#: src/view/com/home/HomeHeaderLayout.web.tsx:76 -#: src/view/screens/Feeds.tsx:416 -#~ msgid "Edit Saved Feeds" -#~ msgstr "保存されたフィードを編集" - #: src/view/com/modals/CreateOrEditList.tsx:234 msgid "Edit User List" msgstr "ユーザーリストを編集" @@ -1754,6 +1781,10 @@ msgstr "全員" msgid "Everybody can reply" msgstr "誰でも返信可能" +#: src/view/com/threadgate/WhoCanReply.tsx:129 +msgid "Everybody can reply." +msgstr "誰でも返信可能です。" + #: src/components/dms/MessagesNUX.tsx:131 #: src/components/dms/MessagesNUX.tsx:134 #: src/screens/Messages/Settings.tsx:75 @@ -1857,6 +1888,11 @@ msgstr "メッセージの削除に失敗しました" msgid "Failed to delete post, please try again" msgstr "投稿の削除に失敗しました。もう一度お試しください。" +#: src/view/screens/Search/Explore.tsx:414 +#: src/view/screens/Search/Explore.tsx:438 +msgid "Failed to load feeds preferences" +msgstr "フィードの設定の読み込みに失敗しました" + #: src/components/dialogs/GifSelect.ios.tsx:196 #: src/components/dialogs/GifSelect.tsx:212 msgid "Failed to load GIFs" @@ -1866,6 +1902,15 @@ msgstr "GIFの読み込みに失敗しました" msgid "Failed to load past messages" msgstr "過去のメッセージの読み込みに失敗しました" +#: src/view/screens/Search/Explore.tsx:407 +#: src/view/screens/Search/Explore.tsx:431 +msgid "Failed to load suggested feeds" +msgstr "おすすめのフィードの読み込みに失敗しました" + +#: src/view/screens/Search/Explore.tsx:367 +msgid "Failed to load suggested follows" +msgstr "おすすめのフォローの読み込みに失敗しました" + #: src/view/com/lightbox/Lightbox.tsx:84 msgid "Failed to save image: {0}" msgstr "画像の保存に失敗しました:{0}" @@ -1879,6 +1924,14 @@ msgstr "送信に失敗" msgid "Failed to submit appeal, please try again." msgstr "異議申し立ての送信に失敗しました。再度試してください。" +#: src/view/com/util/forms/PostDropdownBtn.tsx:180 +msgid "Failed to toggle thread mute, please try again" +msgstr "スレッドのミュートの切り替えに失敗しました。再度試してください" + +#: src/components/FeedCard.tsx:153 +msgid "Failed to update feeds" +msgstr "フィードの更新に失敗しました" + #: src/components/dms/MessagesNUX.tsx:60 #: src/screens/Messages/Settings.tsx:35 msgid "Failed to update settings" @@ -1914,6 +1967,10 @@ msgstr "フィード" msgid "Feeds are custom algorithms that users build with a little coding expertise. <0/> for more information." msgstr "フィードはユーザーがプログラミングの専門知識を持って構築するカスタムアルゴリズムです。詳細については、<0/>を参照してください。" +#: src/components/FeedCard.tsx:150 +msgid "Feeds updated!" +msgstr "フィードを更新しました!" + #: src/view/com/modals/ChangeHandle.tsx:475 msgid "File Contents" msgstr "ファイルのコンテンツ" @@ -1996,14 +2053,30 @@ msgstr "アカウントをフォロー" msgid "Follow Back" msgstr "フォローバック" -#: src/components/KnownFollowers.tsx:169 -msgid "Followed by" -msgstr "" +#: src/view/screens/Search/Explore.tsx:332 +msgid "Follow more accounts to get connected to your interests and build your network." +msgstr "もっとたくさんのアカウントをフォローして、興味あることにつながり、ネットワークを広げましょう。" #: src/view/com/profile/ProfileCard.tsx:227 msgid "Followed by {0}" msgstr "{0}がフォロー中" +#: src/components/KnownFollowers.tsx:192 +msgid "Followed by <0>{0}" +msgstr "<0>{0}がフォロー中" + +#: src/components/KnownFollowers.tsx:209 +msgid "Followed by <0>{0} and {1, plural, one {# other} other {# others}}" +msgstr "<0>{0}および{1, plural, other {他#人}}がフォロー中" + +#: src/components/KnownFollowers.tsx:181 +msgid "Followed by <0>{0} and <1>{1}" +msgstr "<0>{0}と<1>{1}がフォロー中" + +#: src/components/KnownFollowers.tsx:168 +msgid "Followed by <0>{0}, <1>{1}, and {2, plural, one {# other} other {# others}}" +msgstr "<0>{0}、<1>{1}および{2, plural, other {他#人}}がフォロー中" + #: src/view/com/modals/Threadgate.tsx:99 msgid "Followed users" msgstr "自分がフォローしているユーザー" @@ -2023,12 +2096,12 @@ msgstr "フォロワー" #: src/Navigation.tsx:177 msgid "Followers of @{0} that you know" -msgstr "" +msgstr "あなたが知っている@{0}のフォロワー" #: src/screens/Profile/KnownFollowers.tsx:108 #: src/screens/Profile/KnownFollowers.tsx:118 msgid "Followers you know" -msgstr "" +msgstr "あなたが知っているフォロワー" #: src/components/ProfileHoverCard/index.web.tsx:411 #: src/components/ProfileHoverCard/index.web.tsx:422 @@ -2118,6 +2191,10 @@ msgstr "始める" msgid "Get Started" msgstr "開始" +#: src/view/com/util/images/ImageHorzList.tsx:35 +msgid "GIF" +msgstr "GIF" + #: src/screens/Onboarding/StepProfile/index.tsx:225 msgid "Give your profile a face" msgstr "プロフィールに顔をつける" @@ -2658,6 +2735,18 @@ msgstr "リスト" msgid "Lists blocking this user:" msgstr "このユーザーをブロックしているリスト:" +#: src/view/screens/Search/Explore.tsx:128 +msgid "Load more" +msgstr "さらに読み込む" + +#: src/view/screens/Search/Explore.tsx:216 +msgid "Load more suggested feeds" +msgstr "おすすめのフィードをさらに読み込む" + +#: src/view/screens/Search/Explore.tsx:214 +msgid "Load more suggested follows" +msgstr "おすすめのフォローをさらに読み込む" + #: src/view/screens/Notifications.tsx:184 msgid "Load new notifications" msgstr "最新の通知を読み込む" @@ -3062,6 +3151,10 @@ msgctxt "action" msgid "New Post" msgstr "新しい投稿" +#: src/components/NewskieDialog.tsx:68 +msgid "New user info dialog" +msgstr "新しいユーザー情報ダイアログ" + #: src/view/com/modals/CreateOrEditList.tsx:236 msgid "New User List" msgstr "新しいユーザーリスト" @@ -3142,7 +3235,7 @@ msgstr "誰からも受け取らない" #: src/screens/Profile/Sections/Feed.tsx:59 msgid "No posts yet." -msgstr "" +msgstr "まだ投稿がありません。" #: src/view/com/composer/text-input/mobile/Autocomplete.tsx:101 #: src/view/com/composer/text-input/web/Autocomplete.tsx:195 @@ -3236,6 +3329,10 @@ msgstr "通知音" msgid "Notifications" msgstr "通知" +#: src/lib/hooks/useTimeAgo.ts:51 +msgid "now" +msgstr "今" + #: src/components/dms/MessageItem.tsx:175 msgid "Now" msgstr "今" @@ -3274,6 +3371,10 @@ msgstr "OK" msgid "Oldest replies first" msgstr "古い順に返信を表示" +#: src/lib/hooks/useTimeAgo.ts:81 +msgid "on {str}" +msgstr "{str}" + #: src/view/screens/Settings/index.tsx:256 msgid "Onboarding reset" msgstr "オンボーディングのリセット" @@ -3449,11 +3550,6 @@ msgstr "モデレーションの設定を開く" msgid "Opens password reset form" msgstr "パスワードリセットのフォームを開く" -#: src/view/com/home/HomeHeaderLayout.web.tsx:77 -#: src/view/screens/Feeds.tsx:417 -#~ msgid "Opens screen to edit Saved Feeds" -#~ msgstr "保存されたフィードの編集画面を開く" - #: src/view/screens/Settings/index.tsx:617 msgid "Opens screen with all saved feeds" msgstr "保存されたすべてのフィードで画面を開く" @@ -3777,7 +3873,7 @@ msgstr "再実行する" #: src/components/KnownFollowers.tsx:111 msgid "Press to view followers of this account that you also follow" -msgstr "" +msgstr "あなたもフォローしているこのアカウントのフォロワーを見る" #: src/view/com/lightbox/Lightbox.web.tsx:150 msgid "Previous image" @@ -3975,7 +4071,7 @@ msgstr "リストから削除されました" #: src/view/com/feeds/FeedSourceCard.tsx:139 msgid "Removed from my feeds" -msgstr "フィードから削除しました" +msgstr "マイフィードから削除しました" #: src/view/com/posts/FeedShutdownMsg.tsx:44 #: src/view/screens/ProfileFeed.tsx:191 @@ -4000,9 +4096,9 @@ msgstr "Discoverで置き換える" msgid "Replies" msgstr "返信" -#: src/view/com/threadgate/WhoCanReply.tsx:98 -msgid "Replies to this thread are disabled" -msgstr "このスレッドへの返信はできません" +#: src/view/com/threadgate/WhoCanReply.tsx:131 +msgid "Replies to this thread are disabled." +msgstr "このスレッドへの返信はできません。" #: src/view/com/composer/Composer.tsx:475 msgctxt "action" @@ -4019,6 +4115,11 @@ msgctxt "description" msgid "Reply to <0><1/>" msgstr "<0><1/>に返信" +#: src/view/com/posts/FeedItem.tsx:437 +msgctxt "description" +msgid "Reply to a blocked post" +msgstr "ブロックした投稿への返信" + #: src/components/dms/MessageMenu.tsx:132 #: src/components/dms/MessagesListBlockedFooter.tsx:77 #: src/components/dms/MessagesListBlockedFooter.tsx:84 @@ -4926,9 +5027,9 @@ msgstr "このラベラーを登録" msgid "Subscribe to this list" msgstr "このリストに登録" -#: src/view/screens/Search/Search.tsx:425 -msgid "Suggested Follows" -msgstr "おすすめのフォロー" +#: src/view/screens/Search/Explore.tsx:330 +msgid "Suggested accounts" +msgstr "おすすめのアカウント" #: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:65 msgid "Suggested for you" @@ -5077,7 +5178,7 @@ msgstr "サービス規約は移動しました" #: src/screens/Settings/components/DeactivateAccountDialog.tsx:86 msgid "There is no time limit for account deactivation, come back any time." -msgstr "アカウントの無効化に期限はありません。いつでも戻ってこれます。" +msgstr "アカウントの無効化に期限はありません。いつでも戻ってこられます。" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:115 #: src/view/screens/ProfileFeed.tsx:541 @@ -5217,7 +5318,7 @@ msgstr "このコンテンツはBlueskyのアカウントがないと閲覧で #: src/screens/Messages/List/ChatListItem.tsx:213 msgid "This conversation is with a deleted or a deactivated account. Press for options." -msgstr "" +msgstr "削除あるいは無効化されたアカウントとの会話です。押すと選択肢が表示されます。" #: src/view/screens/Settings/ExportCarDialog.tsx:93 msgid "This feature is in beta. You can read more about repository exports in <0>this blogpost." @@ -5227,12 +5328,6 @@ msgstr "この機能はベータ版です。リポジトリのエクスポート msgid "This feed is currently receiving high traffic and is temporarily unavailable. Please try again later." msgstr "現在このフィードにはアクセスが集中しており、一時的にご利用いただけません。時間をおいてもう一度お試しください。" -#: src/screens/Profile/Sections/Feed.tsx:59 -#: src/view/screens/ProfileFeed.tsx:471 -#: src/view/screens/ProfileList.tsx:729 -#~ msgid "This feed is empty!" -#~ msgstr "このフィードは空です!" - #: src/view/com/posts/CustomFeedEmptyState.tsx:37 msgid "This feed is empty! You may need to follow more users or tune your language settings." msgstr "このフィードは空です!もっと多くのユーザーをフォローするか、言語の設定を調整する必要があるかもしれません。" @@ -5240,7 +5335,7 @@ msgstr "このフィードは空です!もっと多くのユーザーをフォ #: src/view/screens/ProfileFeed.tsx:471 #: src/view/screens/ProfileList.tsx:729 msgid "This feed is empty." -msgstr "" +msgstr "このフィードは空です。" #: src/view/com/posts/FeedShutdownMsg.tsx:97 msgid "This feed is no longer online. We are showing <0>Discover instead." @@ -5336,6 +5431,10 @@ msgstr "このユーザーはブロックした<0>{0}リストに含まれ msgid "This user is included in the <0>{0} list which you have muted." msgstr "このユーザーはミュートした<0>{0}リストに含まれています。" +#: src/components/NewskieDialog.tsx:50 +msgid "This user is new here. Press for more info about when they joined." +msgstr "新しいユーザーです。ここを押すといつ参加したかの情報が表示されます。" + #: src/view/com/profile/ProfileFollows.tsx:87 msgid "This user isn't following anyone." msgstr "このユーザーは誰もフォローしていません。" @@ -5762,6 +5861,10 @@ msgstr "{0}のアバターを表示" msgid "View {0}'s profile" msgstr "{0}のプロフィールを表示" +#: src/components/ProfileHoverCard/index.web.tsx:417 +msgid "View blocked user's profile" +msgstr "ブロック中のユーザーのプロフィールを表示" + #: src/view/screens/Log.tsx:52 msgid "View debug entry" msgstr "デバッグエントリーを表示" @@ -5804,7 +5907,7 @@ msgstr "このフィードにいいねしたユーザーを見る" #: src/view/com/home/HomeHeaderLayout.web.tsx:78 #: src/view/com/home/HomeHeaderLayoutMobile.tsx:84 msgid "View your feeds and explore more" -msgstr "" +msgstr "フィードを表示し、さらにフィードを探す" #: src/view/com/modals/LinkWarning.tsx:89 #: src/view/com/modals/LinkWarning.tsx:95 @@ -5891,7 +5994,7 @@ msgstr "大変申し訳ありませんが、検索を完了できませんでし #: src/view/com/composer/Composer.tsx:318 msgid "We're sorry! The post you are replying to has been deleted." -msgstr "" +msgstr "大変申し訳ありません!返信しようとしている投稿は削除されました。" #: src/components/Lists.tsx:212 #: src/view/screens/NotFound.tsx:48 @@ -5899,8 +6002,8 @@ msgid "We're sorry! We can't find the page you were looking for." msgstr "大変申し訳ありません!お探しのページは見つかりません。" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:330 -msgid "We're sorry! You can only subscribe to ten labelers, and you've reached your limit of ten." -msgstr "大変申し訳ありません!ラベラーは10までしか登録できず、すでに上限に達しています。" +msgid "We're sorry! You can only subscribe to twenty labelers, and you've reached your limit of twenty." +msgstr "大変申し訳ありません!ラベラーは20までしか登録できず、すでに上限に達しています。" #: src/screens/Deactivated.tsx:128 msgid "Welcome back!" @@ -6047,7 +6150,7 @@ msgstr "あなたはまだだれもフォロワーがいません。" #: src/screens/Profile/KnownFollowers.tsx:99 msgid "You don't follow any users who follow @{name}." -msgstr "" +msgstr "@{name}をフォローしているユーザーを誰もフォローしていません。" #: src/view/com/modals/InviteCodes.tsx:67 msgid "You don't have any invite codes yet! We'll send you some when you've been on Bluesky for a little longer." From ba21fddd7897513fef663b826094878ad0ff1556 Mon Sep 17 00:00:00 2001 From: Minseo Lee Date: Fri, 21 Jun 2024 07:50:18 +0900 Subject: [PATCH 71/81] Update Korean localization (#4513) * Update messages.po * Update messages.po * Update messages.po * Update messages.po * Update messages.po * Update messages.po * Update messages.po * Update messages.po * Update messages.po * Update messages.po --- src/locale/locales/ko/messages.po | 767 ++++++++++++++++++------------ 1 file changed, 456 insertions(+), 311 deletions(-) diff --git a/src/locale/locales/ko/messages.po b/src/locale/locales/ko/messages.po index ea3088188c..5f36a7eb7f 100644 --- a/src/locale/locales/ko/messages.po +++ b/src/locale/locales/ko/messages.po @@ -8,7 +8,7 @@ msgstr "" "Language: ko\n" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2024-06-08 15:32+0900\n" +"PO-Revision-Date: 2024-06-19 10:55+0900\n" "Last-Translator: quiple\n" "Language-Team: quiple, lens0021, HaruChanHeart, hazzzi, heartade\n" "Plural-Forms: \n" @@ -21,7 +21,7 @@ msgstr "(임베드 콘텐츠 포함)" msgid "(no email)" msgstr "(이메일 없음)" -#: src/view/com/notifications/FeedItem.tsx:261 +#: src/view/com/notifications/FeedItem.tsx:263 msgid "{0, plural, one {{formattedCount} other} other {{formattedCount} others}}" msgstr "외 {0, plural, other {{formattedCount}}}명" @@ -33,32 +33,29 @@ msgstr "이 계정에 {0, plural, other {#}}개의 라벨이 지정됨" msgid "{0, plural, one {# label has been placed on this content} other {# labels have been placed on this content}}" msgstr "이 콘텐츠에 {0, plural, other {#}}개의 라벨이 지정됨" -#: src/view/com/util/post-ctrls/RepostButton.tsx:65 +#: src/view/com/util/post-ctrls/RepostButton.tsx:66 msgid "{0, plural, one {# repost} other {# reposts}}" msgstr "{0, plural, other {#}}개" -#: src/components/KnownFollowers.tsx:179 -msgid "{0, plural, one {and # other} other {and # others}}" -msgstr "" - -#: src/components/ProfileHoverCard/index.web.tsx:376 +#: src/components/ProfileHoverCard/index.web.tsx:398 #: src/screens/Profile/Header/Metrics.tsx:23 msgid "{0, plural, one {follower} other {followers}}" msgstr "팔로워" -#: src/components/ProfileHoverCard/index.web.tsx:380 +#: src/components/ProfileHoverCard/index.web.tsx:402 #: src/screens/Profile/Header/Metrics.tsx:27 msgid "{0, plural, one {following} other {following}}" msgstr "팔로우 중" -#: src/view/com/util/post-ctrls/PostCtrls.tsx:252 +#: src/view/com/util/post-ctrls/PostCtrls.tsx:255 msgid "{0, plural, one {Like (# like)} other {Like (# likes)}}" msgstr "좋아요 ({0, plural, other {#}}개)" -#: src/view/com/post-thread/PostThreadItem.tsx:380 +#: src/view/com/post-thread/PostThreadItem.tsx:382 msgid "{0, plural, one {like} other {likes}}" msgstr "좋아요" +#: src/components/FeedCard.tsx:111 #: src/view/com/feeds/FeedSourceCard.tsx:301 msgid "{0, plural, one {Liked by # user} other {Liked by # users}}" msgstr "{0, plural, other {#}}명의 사용자가 좋아함" @@ -67,19 +64,19 @@ msgstr "{0, plural, other {#}}명의 사용자가 좋아함" msgid "{0, plural, one {post} other {posts}}" msgstr "게시물" -#: src/view/com/util/post-ctrls/PostCtrls.tsx:210 +#: src/view/com/util/post-ctrls/PostCtrls.tsx:213 msgid "{0, plural, one {Reply (# reply)} other {Reply (# replies)}}" msgstr "답글 ({0, plural, other {#}}개)" -#: src/view/com/post-thread/PostThreadItem.tsx:360 +#: src/view/com/post-thread/PostThreadItem.tsx:362 msgid "{0, plural, one {repost} other {reposts}}" msgstr "재게시" -#: src/view/com/util/post-ctrls/PostCtrls.tsx:248 +#: src/view/com/util/post-ctrls/PostCtrls.tsx:251 msgid "{0, plural, one {Unlike (# like)} other {Unlike (# likes)}}" msgstr "좋아요 취소 ({0, plural, other {#}}개)" -#: src/view/com/util/UserAvatar.tsx:406 +#: src/view/com/util/UserAvatar.tsx:419 msgid "{0}'s avatar" msgstr "{0} 님의 아바타" @@ -87,6 +84,26 @@ msgstr "{0} 님의 아바타" msgid "{count, plural, one {Liked by # user} other {Liked by # users}}" msgstr "{count, plural, other {#}}명의 사용자가 좋아함" +#: src/lib/hooks/useTimeAgo.ts:69 +msgid "{diff, plural, one {day} other {days}}" +msgstr "일" + +#: src/lib/hooks/useTimeAgo.ts:64 +msgid "{diff, plural, one {hour} other {hours}}" +msgstr "시간" + +#: src/lib/hooks/useTimeAgo.ts:59 +msgid "{diff, plural, one {minute} other {minutes}}" +msgstr "분" + +#: src/lib/hooks/useTimeAgo.ts:75 +msgid "{diff, plural, one {month} other {months}}" +msgstr "개월" + +#: src/lib/hooks/useTimeAgo.ts:54 +msgid "{diffSeconds, plural, one {second} other {seconds}}" +msgstr "초" + #: src/screens/SignupQueued.tsx:207 msgid "{estimatedTimeHrs, plural, one {hour} other {hours}}" msgstr "시간" @@ -95,7 +112,7 @@ msgstr "시간" msgid "{estimatedTimeMins, plural, one {minute} other {minutes}}" msgstr "분" -#: src/components/ProfileHoverCard/index.web.tsx:457 +#: src/components/ProfileHoverCard/index.web.tsx:503 #: src/screens/Profile/Header/Metrics.tsx:50 msgid "{following} following" msgstr "{following} 팔로우 중" @@ -114,11 +131,15 @@ msgstr "{likeCount, plural, other {#}}명의 사용자가 좋아함" msgid "{numUnreadNotifications} unread" msgstr "{numUnreadNotifications}개 읽지 않음" +#: src/components/NewskieDialog.tsx:75 +msgid "{profileName} joined Bluesky {0} ago" +msgstr "{profileName} 님은 {0} 전에 Bluesky에 가입했습니다." + #: src/view/screens/PreferencesFollowingFeed.tsx:67 msgid "{value, plural, =0 {Show all replies} one {Show replies with at least # like} other {Show replies with at least # likes}}" msgstr "{value, plural, =0 {모든 답글 표시} other {좋아요가 #개 이상인 답글 표시}}" -#: src/view/com/threadgate/WhoCanReply.tsx:159 +#: src/view/com/threadgate/WhoCanReply.tsx:290 msgid "<0/> members" msgstr "<0/>의 멤버" @@ -134,7 +155,7 @@ msgstr "<0>{0} 팔로우 중" msgid "<0>Not Applicable. This warning is only available for posts with media attached." msgstr "<0>해당 없음. 이 경고는 미디어가 첨부된 게시물에만 사용할 수 있습니다." -#: src/screens/Profile/Header/Handle.tsx:43 +#: src/screens/Profile/Header/Handle.tsx:50 msgid "⚠Invalid Handle" msgstr "⚠잘못된 핸들" @@ -143,7 +164,7 @@ msgid "2FA Confirmation" msgstr "2단계 인증" #: src/view/com/util/ViewHeader.tsx:93 -#: src/view/screens/Search/Search.tsx:715 +#: src/view/screens/Search/Search.tsx:684 msgid "Access navigation links and settings" msgstr "탐색 링크 및 설정으로 이동합니다" @@ -161,7 +182,7 @@ msgid "Accessibility settings" msgstr "접근성 설정" #: src/Navigation.tsx:296 -#: src/view/screens/AccessibilitySettings.tsx:63 +#: src/view/screens/AccessibilitySettings.tsx:69 msgid "Accessibility Settings" msgstr "접근성 설정" @@ -171,15 +192,15 @@ msgstr "접근성 설정" msgid "Account" msgstr "계정" -#: src/view/com/profile/ProfileMenu.tsx:142 +#: src/view/com/profile/ProfileMenu.tsx:145 msgid "Account blocked" msgstr "계정 차단됨" -#: src/view/com/profile/ProfileMenu.tsx:156 +#: src/view/com/profile/ProfileMenu.tsx:159 msgid "Account followed" msgstr "계정 팔로우함" -#: src/view/com/profile/ProfileMenu.tsx:116 +#: src/view/com/profile/ProfileMenu.tsx:119 msgid "Account muted" msgstr "계정 뮤트됨" @@ -200,16 +221,16 @@ msgstr "계정 옵션" msgid "Account removed from quick access" msgstr "빠른 액세스에서 계정 제거" -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:135 -#: src/view/com/profile/ProfileMenu.tsx:131 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:139 +#: src/view/com/profile/ProfileMenu.tsx:134 msgid "Account unblocked" msgstr "계정 차단 해제됨" -#: src/view/com/profile/ProfileMenu.tsx:169 +#: src/view/com/profile/ProfileMenu.tsx:172 msgid "Account unfollowed" msgstr "계정 언팔로우함" -#: src/view/com/profile/ProfileMenu.tsx:105 +#: src/view/com/profile/ProfileMenu.tsx:108 msgid "Account unmuted" msgstr "계정 언뮤트됨" @@ -270,8 +291,12 @@ msgstr "내가 팔로우하는 사람의 기본 피드만 추가하기" msgid "Add the following DNS record to your domain:" msgstr "도메인에 다음 DNS 레코드를 추가하세요:" -#: src/view/com/profile/ProfileMenu.tsx:265 +#: src/components/FeedCard.tsx:180 +msgid "Add this feed to your feeds" +msgstr "이 피드를 내 피드에 추가하기" + #: src/view/com/profile/ProfileMenu.tsx:268 +#: src/view/com/profile/ProfileMenu.tsx:271 msgid "Add to Lists" msgstr "리스트에 추가" @@ -306,7 +331,7 @@ msgstr "성인 콘텐츠가 비활성화되어 있습니다." msgid "Advanced" msgstr "고급" -#: src/view/screens/Feeds.tsx:771 +#: src/view/screens/Feeds.tsx:737 msgid "All the feeds you've saved, right in one place." msgstr "저장한 모든 피드를 한 곳에서 확인하세요." @@ -331,17 +356,17 @@ msgstr "이미 @{0}(으)로 로그인했습니다" #: src/view/com/composer/GifAltText.tsx:93 #: src/view/com/composer/photos/Gallery.tsx:144 -#: src/view/com/util/post-embeds/GifEmbed.tsx:173 +#: src/view/com/util/post-embeds/GifEmbed.tsx:177 msgid "ALT" msgstr "ALT" #: src/view/com/composer/GifAltText.tsx:144 #: src/view/com/modals/EditImage.tsx:316 -#: src/view/screens/AccessibilitySettings.tsx:77 +#: src/view/screens/AccessibilitySettings.tsx:83 msgid "Alt text" msgstr "대체 텍스트" -#: src/view/com/util/post-embeds/GifEmbed.tsx:179 +#: src/view/com/util/post-embeds/GifEmbed.tsx:183 msgid "Alt Text" msgstr "대체 텍스트" @@ -379,9 +404,8 @@ msgstr "문제가 발생했습니다. 다시 시도해 주세요." msgid "an unknown error occurred" msgstr "알 수 없는 오류가 발생했습니다" -#: src/components/KnownFollowers.tsx:187 -#: src/view/com/notifications/FeedItem.tsx:258 -#: src/view/com/threadgate/WhoCanReply.tsx:180 +#: src/view/com/notifications/FeedItem.tsx:260 +#: src/view/com/threadgate/WhoCanReply.tsx:311 msgid "and" msgstr "및" @@ -389,7 +413,7 @@ msgstr "및" msgid "Animals" msgstr "동물" -#: src/view/com/util/post-embeds/GifEmbed.tsx:148 +#: src/view/com/util/post-embeds/GifEmbed.tsx:149 msgid "Animated GIF" msgstr "움직이는 GIF" @@ -469,7 +493,11 @@ msgstr "정말 이 대화에서 나가시겠습니까? 나에게 보이는 메 msgid "Are you sure you want to remove {0} from your feeds?" msgstr "피드에서 {0}을(를) 제거하시겠습니까?" -#: src/view/com/composer/Composer.tsx:630 +#: src/components/FeedCard.tsx:197 +msgid "Are you sure you want to remove this from your feeds?" +msgstr "내 피드에서 이 피드를 삭제하시겠습니까?" + +#: src/view/com/composer/Composer.tsx:632 msgid "Are you sure you'd like to discard this draft?" msgstr "이 초안을 삭제하시겠습니까?" @@ -524,8 +552,8 @@ msgstr "생년월일" msgid "Birthday:" msgstr "생년월일:" -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:309 -#: src/view/com/profile/ProfileMenu.tsx:363 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:314 +#: src/view/com/profile/ProfileMenu.tsx:366 msgid "Block" msgstr "차단" @@ -534,12 +562,12 @@ msgstr "차단" msgid "Block account" msgstr "계정 차단" -#: src/view/com/profile/ProfileMenu.tsx:302 -#: src/view/com/profile/ProfileMenu.tsx:309 +#: src/view/com/profile/ProfileMenu.tsx:305 +#: src/view/com/profile/ProfileMenu.tsx:312 msgid "Block Account" msgstr "계정 차단" -#: src/view/com/profile/ProfileMenu.tsx:346 +#: src/view/com/profile/ProfileMenu.tsx:349 msgid "Block Account?" msgstr "계정을 차단하시겠습니까?" @@ -569,7 +597,7 @@ msgstr "차단한 계정" msgid "Blocked Accounts" msgstr "차단한 계정" -#: src/view/com/profile/ProfileMenu.tsx:358 +#: src/view/com/profile/ProfileMenu.tsx:361 msgid "Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you." msgstr "차단한 계정은 내 스레드에 답글을 달거나 나를 멘션하거나 기타 다른 방식으로 나와 상호작용할 수 없습니다." @@ -577,7 +605,7 @@ msgstr "차단한 계정은 내 스레드에 답글을 달거나 나를 멘션 msgid "Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you. You will not see their content and they will be prevented from seeing yours." msgstr "차단한 계정은 내 스레드에 답글을 달거나 나를 멘션하거나 기타 다른 방식으로 나와 상호작용할 수 없습니다. 차단한 계정의 콘텐츠를 볼 수 없으며 해당 계정도 내 콘텐츠를 볼 수 없게 됩니다." -#: src/view/com/post-thread/PostThread.tsx:363 +#: src/view/com/post-thread/PostThread.tsx:367 msgid "Blocked post." msgstr "차단된 게시물." @@ -589,7 +617,7 @@ msgstr "차단하더라도 이 라벨러가 내 계정에 라벨을 붙이는 msgid "Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you." msgstr "차단 목록은 공개됩니다. 차단한 계정은 내 스레드에 답글을 달거나 나를 멘션하거나 기타 다른 방식으로 나와 상호작용할 수 없습니다." -#: src/view/com/profile/ProfileMenu.tsx:355 +#: src/view/com/profile/ProfileMenu.tsx:358 msgid "Blocking will not prevent labels from being applied on your account, but it will stop this account from replying in your threads or interacting with you." msgstr "차단하더라도 내 계정에 라벨이 붙는 것은 막지 못하지만, 이 계정이 내 스레드에 답글을 달거나 나와 상호작용하는 것은 중지됩니다." @@ -664,8 +692,8 @@ msgstr "글자, 숫자, 공백, 대시, 밑줄만 포함할 수 있습니다. #: src/components/Prompt.tsx:121 #: src/components/TagMenu/index.tsx:268 #: src/screens/Deactivated.tsx:161 -#: src/view/com/composer/Composer.tsx:432 -#: src/view/com/composer/Composer.tsx:438 +#: src/view/com/composer/Composer.tsx:434 +#: src/view/com/composer/Composer.tsx:440 #: src/view/com/modals/ChangeEmail.tsx:213 #: src/view/com/modals/ChangeEmail.tsx:215 #: src/view/com/modals/ChangeHandle.tsx:148 @@ -681,8 +709,8 @@ msgstr "글자, 숫자, 공백, 대시, 밑줄만 포함할 수 있습니다. #: src/view/com/modals/LinkWarning.tsx:107 #: src/view/com/modals/VerifyEmail.tsx:255 #: src/view/com/modals/VerifyEmail.tsx:261 -#: src/view/com/util/post-ctrls/RepostButton.tsx:138 -#: src/view/screens/Search/Search.tsx:735 +#: src/view/com/util/post-ctrls/RepostButton.tsx:139 +#: src/view/screens/Search/Search.tsx:704 #: src/view/shell/desktop/Search.tsx:218 msgid "Cancel" msgstr "취소" @@ -711,7 +739,7 @@ msgstr "이미지 자르기 취소" msgid "Cancel profile editing" msgstr "프로필 편집 취소" -#: src/view/com/util/post-ctrls/RepostButton.tsx:132 +#: src/view/com/util/post-ctrls/RepostButton.tsx:133 msgid "Cancel quote post" msgstr "게시물 인용 취소" @@ -807,7 +835,7 @@ msgstr "이메일에서 로그인 코드를 확인한 후 여기에 입력하세 msgid "Check your inbox for an email with the confirmation code to enter below:" msgstr "받은 편지함에서 아래에 입력하는 인증 코드가 포함된 이메일이 있는지 확인하세요:" -#: src/view/com/modals/Threadgate.tsx:73 +#: src/view/com/modals/Threadgate.tsx:75 msgid "Choose \"Everybody\" or \"Nobody\"" msgstr "\"모두\" 또는 \"없음\"을 선택하세요." @@ -844,7 +872,7 @@ msgid "Clear all storage data (restart after this)" msgstr "모든 스토리지 데이터 지우기 (이후 다시 시작)" #: src/view/com/util/forms/SearchInput.tsx:88 -#: src/view/screens/Search/Search.tsx:861 +#: src/view/screens/Search/Search.tsx:824 msgid "Clear search query" msgstr "검색어 지우기" @@ -889,7 +917,7 @@ msgstr "다그닥 🐴 다그닥 🐴" #: src/components/dms/dialogs/SearchablePeopleList.tsx:261 #: src/view/com/modals/ChangePassword.tsx:268 #: src/view/com/modals/ChangePassword.tsx:271 -#: src/view/com/util/post-embeds/GifEmbed.tsx:185 +#: src/view/com/util/post-embeds/GifEmbed.tsx:189 msgid "Close" msgstr "닫기" @@ -944,7 +972,7 @@ msgstr "하단 탐색 막대를 닫습니다" msgid "Closes password update alert" msgstr "비밀번호 변경 알림을 닫습니다" -#: src/view/com/composer/Composer.tsx:434 +#: src/view/com/composer/Composer.tsx:436 msgid "Closes post composer and discards post draft" msgstr "게시물 작성 상자를 닫고 게시물 초안을 삭제합니다" @@ -952,11 +980,11 @@ msgstr "게시물 작성 상자를 닫고 게시물 초안을 삭제합니다" msgid "Closes viewer for header image" msgstr "헤더 이미지 뷰어를 닫습니다" -#: src/view/com/notifications/FeedItem.tsx:205 +#: src/view/com/notifications/FeedItem.tsx:207 msgid "Collapse list of users" msgstr "사용자 목록 접기" -#: src/view/com/notifications/FeedItem.tsx:341 +#: src/view/com/notifications/FeedItem.tsx:343 msgid "Collapses list of users for a given notification" msgstr "이 알림에 대한 사용자 목록을 축소합니다" @@ -981,7 +1009,7 @@ msgstr "온보딩 완료 후 계정 사용 시작" msgid "Complete the challenge" msgstr "챌린지 완료하기" -#: src/view/com/composer/Composer.tsx:551 +#: src/view/com/composer/Composer.tsx:553 msgid "Compose posts up to {MAX_GRAPHEME_LENGTH} characters in length" msgstr "최대 {MAX_GRAPHEME_LENGTH}자 길이까지 글을 작성할 수 있습니다" @@ -997,8 +1025,8 @@ msgstr "{name} 카테고리에 대한 콘텐츠 필터링 설정을 구성합니 msgid "Configured in <0>moderation settings." msgstr "<0>검토 설정에서 설정합니다." -#: src/components/Prompt.tsx:159 #: src/components/Prompt.tsx:162 +#: src/components/Prompt.tsx:165 #: src/view/com/modals/SelfLabel.tsx:155 #: src/view/com/modals/VerifyEmail.tsx:239 #: src/view/com/modals/VerifyEmail.tsx:241 @@ -1092,7 +1120,7 @@ msgstr "{0}(으)로 계속하기 (현재 로그인)" #: src/view/com/post-thread/PostThreadLoadMore.tsx:52 msgid "Continue thread..." -msgstr "" +msgstr "스레드 더 보기..." #: src/screens/Onboarding/StepInterests/index.tsx:250 #: src/screens/Onboarding/StepProfile/index.tsx:266 @@ -1121,7 +1149,7 @@ msgstr "빌드 버전 클립보드에 복사됨" #: src/view/com/modals/AddAppPasswords.tsx:80 #: src/view/com/modals/ChangeHandle.tsx:320 #: src/view/com/modals/InviteCodes.tsx:153 -#: src/view/com/util/forms/PostDropdownBtn.tsx:182 +#: src/view/com/util/forms/PostDropdownBtn.tsx:189 msgid "Copied to clipboard" msgstr "클립보드에 복사됨" @@ -1150,8 +1178,8 @@ msgstr "코드 복사" msgid "Copy link to list" msgstr "리스트 링크 복사" -#: src/view/com/util/forms/PostDropdownBtn.tsx:297 -#: src/view/com/util/forms/PostDropdownBtn.tsx:306 +#: src/view/com/util/forms/PostDropdownBtn.tsx:307 +#: src/view/com/util/forms/PostDropdownBtn.tsx:316 msgid "Copy link to post" msgstr "게시물 링크 복사" @@ -1160,8 +1188,8 @@ msgstr "게시물 링크 복사" msgid "Copy message text" msgstr "메시지 텍스트 복사" -#: src/view/com/util/forms/PostDropdownBtn.tsx:275 -#: src/view/com/util/forms/PostDropdownBtn.tsx:277 +#: src/view/com/util/forms/PostDropdownBtn.tsx:285 +#: src/view/com/util/forms/PostDropdownBtn.tsx:287 msgid "Copy post text" msgstr "게시물 텍스트 복사" @@ -1238,7 +1266,8 @@ msgstr "사용자 지정" msgid "Custom domain" msgstr "사용자 지정 도메인" -#: src/view/screens/Feeds.tsx:797 +#: src/view/screens/Feeds.tsx:763 +#: src/view/screens/Search/Explore.tsx:383 msgid "Custom feeds built by the community bring you new experiences and help you find the content you love." msgstr "커뮤니티에서 구축한 맞춤 피드는 새로운 경험을 제공하고 좋아하는 콘텐츠를 찾을 수 있도록 도와줍니다." @@ -1281,7 +1310,7 @@ msgid "Debug panel" msgstr "디버그 패널" #: src/components/dms/MessageMenu.tsx:151 -#: src/view/com/util/forms/PostDropdownBtn.tsx:423 +#: src/view/com/util/forms/PostDropdownBtn.tsx:433 #: src/view/screens/AppPasswords.tsx:285 #: src/view/screens/ProfileList.tsx:667 msgid "Delete" @@ -1332,8 +1361,8 @@ msgstr "내 계정 삭제" msgid "Delete My Account…" msgstr "내 계정 삭제…" -#: src/view/com/util/forms/PostDropdownBtn.tsx:404 -#: src/view/com/util/forms/PostDropdownBtn.tsx:406 +#: src/view/com/util/forms/PostDropdownBtn.tsx:414 +#: src/view/com/util/forms/PostDropdownBtn.tsx:416 msgid "Delete post" msgstr "게시물 삭제" @@ -1341,7 +1370,7 @@ msgstr "게시물 삭제" msgid "Delete this list?" msgstr "이 리스트를 삭제하시겠습니까?" -#: src/view/com/util/forms/PostDropdownBtn.tsx:418 +#: src/view/com/util/forms/PostDropdownBtn.tsx:428 msgid "Delete this post?" msgstr "이 게시물을 삭제하시겠습니까?" @@ -1349,7 +1378,7 @@ msgstr "이 게시물을 삭제하시겠습니까?" msgid "Deleted" msgstr "삭제됨" -#: src/view/com/post-thread/PostThread.tsx:349 +#: src/view/com/post-thread/PostThread.tsx:353 msgid "Deleted post." msgstr "삭제된 게시물." @@ -1380,7 +1409,7 @@ msgstr "어둑함" msgid "Direct messages are here!" msgstr "다이렉트 메시지가 생겼습니다!" -#: src/view/screens/AccessibilitySettings.tsx:94 +#: src/view/screens/AccessibilitySettings.tsx:107 msgid "Disable autoplay for GIFs" msgstr "GIF 자동 재생 끄기" @@ -1388,7 +1417,7 @@ msgstr "GIF 자동 재생 끄기" msgid "Disable Email 2FA" msgstr "이메일 2단계 인증 끄기" -#: src/view/screens/AccessibilitySettings.tsx:108 +#: src/view/screens/AccessibilitySettings.tsx:121 msgid "Disable haptic feedback" msgstr "햅틱 피드백 끄기" @@ -1401,11 +1430,11 @@ msgstr "햅틱 피드백 끄기" msgid "Disabled" msgstr "사용 안 함" -#: src/view/com/composer/Composer.tsx:632 +#: src/view/com/composer/Composer.tsx:634 msgid "Discard" msgstr "삭제" -#: src/view/com/composer/Composer.tsx:629 +#: src/view/com/composer/Composer.tsx:631 msgid "Discard draft?" msgstr "초안 삭제" @@ -1419,10 +1448,18 @@ msgstr "앱이 로그아웃한 사용자에게 내 계정을 표시하지 않도 msgid "Discover new custom feeds" msgstr "새로운 맞춤 피드 찾아보기" -#: src/view/screens/Feeds.tsx:794 +#: src/view/screens/Search/Explore.tsx:381 +msgid "Discover new feeds" +msgstr "새 피드 발견하기" + +#: src/view/screens/Feeds.tsx:760 msgid "Discover New Feeds" msgstr "새 피드 발견하기" +#: src/view/screens/AccessibilitySettings.tsx:95 +msgid "Display larger alt text badges" +msgstr "더 큰 대체 텍스트 배지 표시" + #: src/view/com/modals/EditProfile.tsx:193 msgid "Display name" msgstr "표시 이름" @@ -1453,8 +1490,8 @@ msgstr "도메인을 확인했습니다." #: src/components/dialogs/BirthDateSettings.tsx:119 #: src/components/dialogs/BirthDateSettings.tsx:125 -#: src/components/forms/DateField/index.tsx:74 -#: src/components/forms/DateField/index.tsx:80 +#: src/components/forms/DateField/index.tsx:77 +#: src/components/forms/DateField/index.tsx:83 #: src/screens/Onboarding/StepProfile/index.tsx:322 #: src/screens/Onboarding/StepProfile/index.tsx:325 #: src/view/com/auth/server-input/index.tsx:169 @@ -1472,8 +1509,8 @@ msgstr "완료" #: src/view/com/modals/EditImage.tsx:334 #: src/view/com/modals/ListAddRemoveUsers.tsx:144 #: src/view/com/modals/SelfLabel.tsx:158 -#: src/view/com/modals/Threadgate.tsx:130 #: src/view/com/modals/Threadgate.tsx:133 +#: src/view/com/modals/Threadgate.tsx:136 #: src/view/com/modals/UserAddRemoveLists.tsx:108 #: src/view/com/modals/UserAddRemoveLists.tsx:111 #: src/view/screens/PreferencesThreads.tsx:162 @@ -1490,7 +1527,7 @@ msgstr "완료{extraText}" msgid "Download CAR file" msgstr "CAR 파일 다운로드" -#: src/view/com/composer/text-input/TextInput.web.tsx:261 +#: src/view/com/composer/text-input/TextInput.web.tsx:272 msgid "Drop to add images" msgstr "드롭하여 이미지 추가" @@ -1534,17 +1571,17 @@ msgstr "예: 반복적으로 광고 답글을 다는 계정." msgid "Each code works once. You'll receive more invite codes periodically." msgstr "각 코드는 한 번만 사용할 수 있습니다. 주기적으로 더 많은 초대 코드를 받게 됩니다." -#: src/view/screens/Feeds.tsx:400 -#: src/view/screens/Feeds.tsx:471 -msgid "Edit" -msgstr "" - #: src/view/com/lists/ListMembers.tsx:149 msgctxt "action" msgid "Edit" msgstr "편집" -#: src/view/com/util/UserAvatar.tsx:312 +#: src/view/screens/Feeds.tsx:370 +#: src/view/screens/Feeds.tsx:441 +msgid "Edit" +msgstr "편집" + +#: src/view/com/util/UserAvatar.tsx:325 #: src/view/com/util/UserBanner.tsx:92 msgid "Edit avatar" msgstr "아바타 편집" @@ -1563,8 +1600,8 @@ msgid "Edit Moderation List" msgstr "검토 리스트 편집" #: src/Navigation.tsx:269 -#: src/view/screens/Feeds.tsx:398 -#: src/view/screens/Feeds.tsx:469 +#: src/view/screens/Feeds.tsx:368 +#: src/view/screens/Feeds.tsx:439 #: src/view/screens/SavedFeeds.tsx:93 msgid "Edit My Feeds" msgstr "내 피드 편집" @@ -1574,24 +1611,24 @@ msgid "Edit my profile" msgstr "내 프로필 편집" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:181 -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:175 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:179 msgid "Edit profile" msgstr "프로필 편집" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:184 -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:178 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:182 msgid "Edit Profile" msgstr "프로필 편집" -#: src/view/com/home/HomeHeaderLayout.web.tsx:76 -#: src/view/screens/Feeds.tsx:416 -#~ msgid "Edit Saved Feeds" -#~ msgstr "저장한 피드 편집" - #: src/view/com/modals/CreateOrEditList.tsx:234 msgid "Edit User List" msgstr "사용자 리스트 편집" +#: src/view/com/threadgate/WhoCanReply.tsx:73 +#: src/view/com/threadgate/WhoCanReply.tsx:130 +msgid "Edit who can reply" +msgstr "답글을 달 수 있는 사람 편집" + #: src/view/com/modals/EditProfile.tsx:194 msgid "Edit your display name" msgstr "내 표시 이름 편집" @@ -1639,8 +1676,8 @@ msgid "Embed HTML code" msgstr "임베드 HTML 코드" #: src/components/dialogs/Embed.tsx:97 -#: src/view/com/util/forms/PostDropdownBtn.tsx:314 -#: src/view/com/util/forms/PostDropdownBtn.tsx:316 +#: src/view/com/util/forms/PostDropdownBtn.tsx:324 +#: src/view/com/util/forms/PostDropdownBtn.tsx:326 msgid "Embed post" msgstr "게시물 임베드" @@ -1746,11 +1783,14 @@ msgstr "캡차 응답을 수신하는 동안 오류가 발생했습니다." msgid "Error:" msgstr "오류:" -#: src/view/com/modals/Threadgate.tsx:77 +#: src/view/com/modals/Threadgate.tsx:79 msgid "Everybody" msgstr "모두" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:46 +#: src/view/com/threadgate/WhoCanReply.tsx:64 +#: src/view/com/threadgate/WhoCanReply.tsx:121 +#: src/view/com/threadgate/WhoCanReply.tsx:235 msgid "Everybody can reply" msgstr "누구나 답글을 달 수 있음" @@ -1794,7 +1834,7 @@ msgstr "검색어 입력을 종료합니다" msgid "Expand alt text" msgstr "대체 텍스트 확장" -#: src/view/com/notifications/FeedItem.tsx:206 +#: src/view/com/notifications/FeedItem.tsx:208 msgid "Expand list of users" msgstr "사용자 목록 펼치기" @@ -1853,10 +1893,15 @@ msgstr "리스트를 만들지 못했습니다. 인터넷 연결을 확인한 msgid "Failed to delete message" msgstr "메시지를 삭제하지 못했습니다" -#: src/view/com/util/forms/PostDropdownBtn.tsx:149 +#: src/view/com/util/forms/PostDropdownBtn.tsx:152 msgid "Failed to delete post, please try again" msgstr "게시물을 삭제하지 못했습니다. 다시 시도해 주세요" +#: src/view/screens/Search/Explore.tsx:417 +#: src/view/screens/Search/Explore.tsx:441 +msgid "Failed to load feeds preferences" +msgstr "피드 환경설정을 불러오지 못했습니다" + #: src/components/dialogs/GifSelect.ios.tsx:196 #: src/components/dialogs/GifSelect.tsx:212 msgid "Failed to load GIFs" @@ -1866,6 +1911,15 @@ msgstr "GIF를 불러오지 못했습니다" msgid "Failed to load past messages" msgstr "지난 메시지를 불러오지 못했습니다" +#: src/view/screens/Search/Explore.tsx:410 +#: src/view/screens/Search/Explore.tsx:434 +msgid "Failed to load suggested feeds" +msgstr "추천 피드를 불러오지 못했습니다" + +#: src/view/screens/Search/Explore.tsx:370 +msgid "Failed to load suggested follows" +msgstr "추천 팔로우를 불러오지 못했습니다" + #: src/view/com/lightbox/Lightbox.tsx:84 msgid "Failed to save image: {0}" msgstr "이미지를 저장하지 못함: {0}" @@ -1877,7 +1931,15 @@ msgstr "전송 실패" #: src/components/moderation/LabelsOnMeDialog.tsx:223 #: src/screens/Messages/Conversation/ChatDisabled.tsx:87 msgid "Failed to submit appeal, please try again." -msgstr "이의신청을 제출하지 못했습니다. 다시 시도하세요." +msgstr "이의신청을 제출하지 못했습니다. 다시 시도해 주세요." + +#: src/view/com/util/forms/PostDropdownBtn.tsx:180 +msgid "Failed to toggle thread mute, please try again" +msgstr "스레드 뮤트를 전환하지 못했습니다. 다시 시도해 주세요" + +#: src/components/FeedCard.tsx:160 +msgid "Failed to update feeds" +msgstr "피드를 업데이트하지 못했습니다" #: src/components/dms/MessagesNUX.tsx:60 #: src/screens/Messages/Settings.tsx:35 @@ -1888,11 +1950,12 @@ msgstr "설정을 업데이트하지 못했습니다" msgid "Feed" msgstr "피드" +#: src/components/FeedCard.tsx:91 #: src/view/com/feeds/FeedSourceCard.tsx:251 msgid "Feed by {0}" msgstr "{0} 님의 피드" -#: src/view/screens/Feeds.tsx:709 +#: src/view/screens/Feeds.tsx:675 msgid "Feed offline" msgstr "피드 오프라인" @@ -1901,9 +1964,10 @@ msgstr "피드 오프라인" msgid "Feedback" msgstr "피드백" -#: src/view/screens/Feeds.tsx:463 -#: src/view/screens/Feeds.tsx:570 +#: src/view/screens/Feeds.tsx:433 +#: src/view/screens/Feeds.tsx:536 #: src/view/screens/Profile.tsx:197 +#: src/view/screens/Search/Search.tsx:375 #: src/view/shell/desktop/LeftNav.tsx:367 #: src/view/shell/Drawer.tsx:493 #: src/view/shell/Drawer.tsx:494 @@ -1914,6 +1978,10 @@ msgstr "피드" msgid "Feeds are custom algorithms that users build with a little coding expertise. <0/> for more information." msgstr "피드는 사용자가 약간의 코딩 전문 지식만으로 구축할 수 있는 맞춤 알고리즘입니다. <0/>에서 자세한 내용을 확인하세요." +#: src/components/FeedCard.tsx:157 +msgid "Feeds updated!" +msgstr "피드 업데이트됨" + #: src/view/com/modals/ChangeHandle.tsx:475 msgid "File Contents" msgstr "파일 콘텐츠" @@ -1936,7 +2004,7 @@ msgstr "마무리 중" msgid "Find accounts to follow" msgstr "팔로우할 계정 찾아보기" -#: src/view/screens/Search/Search.tsx:470 +#: src/view/screens/Search/Search.tsx:439 msgid "Find posts and users on Bluesky" msgstr "Bluesky에서 게시물 및 사용자 찾기" @@ -1965,9 +2033,9 @@ msgstr "가로로 뒤집기" msgid "Flip vertically" msgstr "세로로 뒤집기" -#: src/components/ProfileHoverCard/index.web.tsx:412 -#: src/components/ProfileHoverCard/index.web.tsx:423 -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:248 +#: src/components/ProfileHoverCard/index.web.tsx:446 +#: src/components/ProfileHoverCard/index.web.tsx:457 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:252 #: src/view/com/post-thread/PostThreadFollowBtn.tsx:146 #: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:247 msgid "Follow" @@ -1978,7 +2046,7 @@ msgctxt "action" msgid "Follow" msgstr "팔로우" -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:234 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:238 #: src/view/com/post-thread/PostThreadFollowBtn.tsx:128 msgid "Follow {0}" msgstr "{0} 님을 팔로우" @@ -1987,8 +2055,8 @@ msgstr "{0} 님을 팔로우" msgid "Follow {name}" msgstr "{name} 님을 팔로우" -#: src/view/com/profile/ProfileMenu.tsx:244 -#: src/view/com/profile/ProfileMenu.tsx:255 +#: src/view/com/profile/ProfileMenu.tsx:247 +#: src/view/com/profile/ProfileMenu.tsx:258 msgid "Follow Account" msgstr "계정 팔로우" @@ -1996,15 +2064,31 @@ msgstr "계정 팔로우" msgid "Follow Back" msgstr "맞팔로우" -#: src/components/KnownFollowers.tsx:169 -msgid "Followed by" -msgstr "" +#: src/view/screens/Search/Explore.tsx:333 +msgid "Follow more accounts to get connected to your interests and build your network." +msgstr "더 많은 계정을 팔로우하고 관심 분야를 연결하여 네트워크를 구축하세요." #: src/view/com/profile/ProfileCard.tsx:227 msgid "Followed by {0}" msgstr "{0} 님이 팔로우함" -#: src/view/com/modals/Threadgate.tsx:99 +#: src/components/KnownFollowers.tsx:223 +msgid "Followed by <0>{0}" +msgstr "<0>{0} 님이 팔로우함" + +#: src/components/KnownFollowers.tsx:209 +msgid "Followed by <0>{0} and {1, plural, one {# other} other {# others}}" +msgstr "<0>{0} 님 외 {1, plural, other {#}}명이 팔로우함" + +#: src/components/KnownFollowers.tsx:196 +msgid "Followed by <0>{0} and <1>{1}" +msgstr "<0>{0} 님과 <1>{1} 님이 팔로우함" + +#: src/components/KnownFollowers.tsx:178 +msgid "Followed by <0>{0}, <1>{1}, and {2, plural, one {# other} other {# others}}" +msgstr "<0>{0} 님, <1>{1} 님 외 {2, plural, other {#}}명이 팔로우함" + +#: src/view/com/modals/Threadgate.tsx:101 msgid "Followed users" msgstr "팔로우한 사용자" @@ -2012,7 +2096,7 @@ msgstr "팔로우한 사용자" msgid "Followed users only" msgstr "팔로우한 사용자만" -#: src/view/com/notifications/FeedItem.tsx:173 +#: src/view/com/notifications/FeedItem.tsx:175 msgid "followed you" msgstr "이(가) 나를 팔로우했습니다" @@ -2023,25 +2107,25 @@ msgstr "팔로워" #: src/Navigation.tsx:177 msgid "Followers of @{0} that you know" -msgstr "" +msgstr "내가 아는 @{0} 님의 팔로워" #: src/screens/Profile/KnownFollowers.tsx:108 #: src/screens/Profile/KnownFollowers.tsx:118 msgid "Followers you know" -msgstr "" +msgstr "내가 아는 팔로워" -#: src/components/ProfileHoverCard/index.web.tsx:411 -#: src/components/ProfileHoverCard/index.web.tsx:422 -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:246 +#: src/components/ProfileHoverCard/index.web.tsx:445 +#: src/components/ProfileHoverCard/index.web.tsx:456 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:250 #: src/view/com/post-thread/PostThreadFollowBtn.tsx:149 #: src/view/com/profile/ProfileFollows.tsx:104 -#: src/view/screens/Feeds.tsx:656 +#: src/view/screens/Feeds.tsx:622 #: src/view/screens/ProfileFollows.tsx:25 #: src/view/screens/SavedFeeds.tsx:415 msgid "Following" msgstr "팔로우 중" -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:94 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:98 msgid "Following {0}" msgstr "{0} 님을 팔로우했습니다" @@ -2059,7 +2143,7 @@ msgstr "팔로우 중 피드 설정" msgid "Following Feed Preferences" msgstr "팔로우 중 피드 설정" -#: src/screens/Profile/Header/Handle.tsx:24 +#: src/screens/Profile/Header/Handle.tsx:31 msgid "Follows you" msgstr "나를 팔로우함" @@ -2100,7 +2184,7 @@ msgstr "잦은 원치 않는 콘텐츠 게시" msgid "From @{sanitizedAuthor}" msgstr "@{sanitizedAuthor} 님의 태그" -#: src/view/com/posts/FeedItem.tsx:232 +#: src/view/com/posts/FeedItem.tsx:236 msgctxt "from-feed" msgid "From <0/>" msgstr "<0/>에서" @@ -2118,6 +2202,10 @@ msgstr "시작하기" msgid "Get Started" msgstr "시작하기" +#: src/view/com/util/images/ImageHorzList.tsx:35 +msgid "GIF" +msgstr "GIF" + #: src/screens/Onboarding/StepProfile/index.tsx:225 msgid "Give your profile a face" msgstr "프로필에 얼굴 달기" @@ -2188,7 +2276,7 @@ msgstr "그래픽 미디어" msgid "Handle" msgstr "핸들" -#: src/view/screens/AccessibilitySettings.tsx:103 +#: src/view/screens/AccessibilitySettings.tsx:116 msgid "Haptics" msgstr "햅틱" @@ -2223,35 +2311,35 @@ msgstr "앱 비밀번호입니다." #: src/components/moderation/ContentHider.tsx:116 #: src/components/moderation/LabelPreference.tsx:134 -#: src/components/moderation/PostHider.tsx:121 +#: src/components/moderation/PostHider.tsx:122 #: src/lib/moderation/useLabelBehaviorDescription.ts:15 #: src/lib/moderation/useLabelBehaviorDescription.ts:20 #: src/lib/moderation/useLabelBehaviorDescription.ts:25 #: src/lib/moderation/useLabelBehaviorDescription.ts:30 -#: src/view/com/util/forms/PostDropdownBtn.tsx:432 +#: src/view/com/util/forms/PostDropdownBtn.tsx:442 msgid "Hide" msgstr "숨기기" -#: src/view/com/notifications/FeedItem.tsx:348 +#: src/view/com/notifications/FeedItem.tsx:350 msgctxt "action" msgid "Hide" msgstr "숨기기" -#: src/view/com/util/forms/PostDropdownBtn.tsx:377 -#: src/view/com/util/forms/PostDropdownBtn.tsx:379 +#: src/view/com/util/forms/PostDropdownBtn.tsx:387 +#: src/view/com/util/forms/PostDropdownBtn.tsx:389 msgid "Hide post" msgstr "게시물 숨기기" #: src/components/moderation/ContentHider.tsx:68 -#: src/components/moderation/PostHider.tsx:78 +#: src/components/moderation/PostHider.tsx:79 msgid "Hide the content" msgstr "콘텐츠 숨기기" -#: src/view/com/util/forms/PostDropdownBtn.tsx:429 +#: src/view/com/util/forms/PostDropdownBtn.tsx:439 msgid "Hide this post?" msgstr "이 게시물을 숨기시겠습니까?" -#: src/view/com/notifications/FeedItem.tsx:339 +#: src/view/com/notifications/FeedItem.tsx:341 msgid "Hide user list" msgstr "사용자 리스트 숨기기" @@ -2341,7 +2429,7 @@ msgstr "해당 국가의 법률에 따라 아직 성인이 아닌 경우, 부모 msgid "If you delete this list, you won't be able to recover it." msgstr "이 리스트를 삭제하면 다시 복구할 수 없습니다." -#: src/view/com/util/forms/PostDropdownBtn.tsx:420 +#: src/view/com/util/forms/PostDropdownBtn.tsx:430 msgid "If you remove this post, you won't be able to recover it." msgstr "이 게시물을 삭제하면 다시 복구할 수 없습니다." @@ -2357,7 +2445,7 @@ msgstr "핸들이나 이메일을 변경하려는 경우 비활성화하기 전 msgid "Illegal and Urgent" msgstr "불법 및 긴급 사항" -#: src/view/com/util/images/Gallery.tsx:39 +#: src/view/com/util/images/Gallery.tsx:42 msgid "Image" msgstr "이미지" @@ -2426,7 +2514,7 @@ msgstr "다이렉트 메시지 소개" msgid "Invalid 2FA confirmation code." msgstr "잘못된 2단계 인증 코드입니다." -#: src/view/com/post-thread/PostThreadItem.tsx:235 +#: src/view/com/post-thread/PostThreadItem.tsx:236 msgid "Invalid or unsupported post record" msgstr "유효하지 않거나 지원되지 않는 게시물 기록" @@ -2442,7 +2530,7 @@ msgstr "친구 초대하기" msgid "Invite code" msgstr "초대 코드" -#: src/screens/Signup/state.ts:272 +#: src/screens/Signup/state.ts:275 msgid "Invite code not accepted. Check that you input it correctly and try again." msgstr "초대 코드가 올바르지 않습니다. 코드를 올바르게 입력했는지 확인한 후 다시 시도하세요." @@ -2504,7 +2592,7 @@ msgid "Languages" msgstr "언어" #: src/screens/Hashtag.tsx:99 -#: src/view/screens/Search/Search.tsx:377 +#: src/view/screens/Search/Search.tsx:359 msgid "Latest" msgstr "최신" @@ -2517,7 +2605,7 @@ msgstr "더 알아보기" msgid "Learn more about the moderation applied to this content." msgstr "이 콘텐츠에 적용된 검토 설정에 대해 자세히 알아보세요." -#: src/components/moderation/PostHider.tsx:99 +#: src/components/moderation/PostHider.tsx:100 #: src/components/moderation/ScreenHider.tsx:125 msgid "Learn more about this warning" msgstr "이 경고에 대해 더 알아보기" @@ -2593,11 +2681,11 @@ msgstr "좋아요 표시한 사용자" msgid "Liked By" msgstr "좋아요 표시한 사용자" -#: src/view/com/notifications/FeedItem.tsx:176 +#: src/view/com/notifications/FeedItem.tsx:178 msgid "liked your custom feed" msgstr "이(가) 내 맞춤 피드를 좋아합니다" -#: src/view/com/notifications/FeedItem.tsx:168 +#: src/view/com/notifications/FeedItem.tsx:170 msgid "liked your post" msgstr "이(가) 내 게시물을 좋아합니다" @@ -2605,7 +2693,7 @@ msgstr "이(가) 내 게시물을 좋아합니다" msgid "Likes" msgstr "좋아요" -#: src/view/com/post-thread/PostThreadItem.tsx:196 +#: src/view/com/post-thread/PostThreadItem.tsx:197 msgid "Likes on this post" msgstr "이 게시물을 좋아요 표시합니다" @@ -2658,6 +2746,18 @@ msgstr "리스트" msgid "Lists blocking this user:" msgstr "이 사용자를 차단한 리스트:" +#: src/view/screens/Search/Explore.tsx:130 +msgid "Load more" +msgstr "더 불러오기" + +#: src/view/screens/Search/Explore.tsx:218 +msgid "Load more suggested feeds" +msgstr "추천 피드 더 불러오기" + +#: src/view/screens/Search/Explore.tsx:216 +msgid "Load more suggested follows" +msgstr "추천 팔로우 더 불러오기" + #: src/view/screens/Notifications.tsx:184 msgid "Load new notifications" msgstr "새 알림 불러오기" @@ -2730,21 +2830,21 @@ msgstr "뮤트한 단어 및 태그 관리" msgid "Mark as read" msgstr "읽음으로 표시" -#: src/view/screens/AccessibilitySettings.tsx:89 +#: src/view/screens/AccessibilitySettings.tsx:102 #: src/view/screens/Profile.tsx:195 msgid "Media" msgstr "미디어" -#: src/view/com/threadgate/WhoCanReply.tsx:139 +#: src/view/com/threadgate/WhoCanReply.tsx:270 msgid "mentioned users" msgstr "멘션한 사용자" -#: src/view/com/modals/Threadgate.tsx:94 +#: src/view/com/modals/Threadgate.tsx:96 msgid "Mentioned users" msgstr "멘션한 사용자" #: src/view/com/util/ViewHeader.tsx:91 -#: src/view/screens/Search/Search.tsx:714 +#: src/view/screens/Search/Search.tsx:683 msgid "Menu" msgstr "메뉴" @@ -2844,7 +2944,7 @@ msgstr "검토 도구" msgid "Moderator has chosen to set a general warning on the content." msgstr "검토자가 콘텐츠에 일반 경고를 설정했습니다." -#: src/view/com/post-thread/PostThreadItem.tsx:566 +#: src/view/com/post-thread/PostThreadItem.tsx:567 msgid "More" msgstr "더 보기" @@ -2868,8 +2968,8 @@ msgstr "뮤트" msgid "Mute {truncatedTag}" msgstr "{truncatedTag} 뮤트" -#: src/view/com/profile/ProfileMenu.tsx:281 -#: src/view/com/profile/ProfileMenu.tsx:288 +#: src/view/com/profile/ProfileMenu.tsx:284 +#: src/view/com/profile/ProfileMenu.tsx:291 msgid "Mute Account" msgstr "계정 뮤트" @@ -2910,13 +3010,13 @@ msgstr "게시물 글 및 태그에서 이 단어 뮤트하기" msgid "Mute this word in tags only" msgstr "태그에서만 이 단어 뮤트하기" -#: src/view/com/util/forms/PostDropdownBtn.tsx:352 -#: src/view/com/util/forms/PostDropdownBtn.tsx:358 +#: src/view/com/util/forms/PostDropdownBtn.tsx:362 +#: src/view/com/util/forms/PostDropdownBtn.tsx:368 msgid "Mute thread" msgstr "스레드 뮤트" -#: src/view/com/util/forms/PostDropdownBtn.tsx:368 -#: src/view/com/util/forms/PostDropdownBtn.tsx:370 +#: src/view/com/util/forms/PostDropdownBtn.tsx:378 +#: src/view/com/util/forms/PostDropdownBtn.tsx:380 msgid "Mute words & tags" msgstr "단어 및 태그 뮤트" @@ -2954,7 +3054,7 @@ msgstr "뮤트 목록은 비공개입니다. 뮤트한 계정은 나와 상호 msgid "My Birthday" msgstr "내 생년월일" -#: src/view/screens/Feeds.tsx:768 +#: src/view/screens/Feeds.tsx:734 msgid "My Feeds" msgstr "내 피드" @@ -3047,7 +3147,7 @@ msgctxt "action" msgid "New post" msgstr "새 게시물" -#: src/view/screens/Feeds.tsx:600 +#: src/view/screens/Feeds.tsx:566 #: src/view/screens/Notifications.tsx:193 #: src/view/screens/Profile.tsx:464 #: src/view/screens/ProfileFeed.tsx:426 @@ -3062,6 +3162,10 @@ msgctxt "action" msgid "New Post" msgstr "새 게시물" +#: src/components/NewskieDialog.tsx:68 +msgid "New user info dialog" +msgstr "새 사용자 정보 대화 상자" + #: src/view/com/modals/CreateOrEditList.tsx:236 msgid "New User List" msgstr "새 사용자 리스트" @@ -3113,7 +3217,7 @@ msgstr "DNS 패널 없음" msgid "No featured GIFs found. There may be an issue with Tenor." msgstr "인기 GIF를 찾을 수 없습니다. Tenor에 문제가 있을 수 있습니다." -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:116 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:120 msgid "No longer following {0}" msgstr "더 이상 {0} 님을 팔로우하지 않음" @@ -3157,13 +3261,14 @@ msgstr "결과 없음" msgid "No results found" msgstr "결과를 찾을 수 없음" -#: src/view/screens/Feeds.tsx:530 +#: src/view/screens/Feeds.tsx:497 msgid "No results found for \"{query}\"" msgstr "\"{query}\"에 대한 결과를 찾을 수 없습니다" #: src/view/com/modals/ListAddRemoveUsers.tsx:127 -#: src/view/screens/Search/Search.tsx:297 -#: src/view/screens/Search/Search.tsx:336 +#: src/view/screens/Search/Search.tsx:233 +#: src/view/screens/Search/Search.tsx:272 +#: src/view/screens/Search/Search.tsx:318 msgid "No results found for {query}" msgstr "{query}에 대한 결과를 찾을 수 없습니다" @@ -3177,7 +3282,7 @@ msgstr "\"{search}\"에 대한 검색 결과를 찾을 수 없습니다." msgid "No thanks" msgstr "사용하지 않음" -#: src/view/com/modals/Threadgate.tsx:83 +#: src/view/com/modals/Threadgate.tsx:85 msgid "Nobody" msgstr "없음" @@ -3204,9 +3309,9 @@ msgstr "찾을 수 없음" msgid "Not right now" msgstr "나중에 하기" -#: src/view/com/profile/ProfileMenu.tsx:370 -#: src/view/com/util/forms/PostDropdownBtn.tsx:446 -#: src/view/com/util/post-ctrls/PostCtrls.tsx:308 +#: src/view/com/profile/ProfileMenu.tsx:373 +#: src/view/com/util/forms/PostDropdownBtn.tsx:456 +#: src/view/com/util/post-ctrls/PostCtrls.tsx:311 msgid "Note about sharing" msgstr "공유 관련 참고 사항" @@ -3236,6 +3341,10 @@ msgstr "알림음" msgid "Notifications" msgstr "알림" +#: src/lib/hooks/useTimeAgo.ts:51 +msgid "now" +msgstr "지금" + #: src/components/dms/MessageItem.tsx:175 msgid "Now" msgstr "지금" @@ -3274,11 +3383,15 @@ msgstr "확인" msgid "Oldest replies first" msgstr "오래된 순" +#: src/lib/hooks/useTimeAgo.ts:81 +msgid "on {str}" +msgstr "" + #: src/view/screens/Settings/index.tsx:256 msgid "Onboarding reset" msgstr "온보딩 재설정" -#: src/view/com/composer/Composer.tsx:503 +#: src/view/com/composer/Composer.tsx:505 msgid "One or more images is missing alt text." msgstr "하나 이상의 이미지에 대체 텍스트가 누락되었습니다." @@ -3286,9 +3399,9 @@ msgstr "하나 이상의 이미지에 대체 텍스트가 누락되었습니다. msgid "Only .jpg and .png files are supported" msgstr ".jpg 및 .png 파일만 지원합니다" -#: src/view/com/threadgate/WhoCanReply.tsx:100 -msgid "Only {0} can reply." -msgstr "{0}만 답글을 달 수 있습니다." +#: src/view/com/threadgate/WhoCanReply.tsx:239 +msgid "Only {0} can reply" +msgstr "{0}만 답글을 달 수 있음" #: src/screens/Signup/StepHandle.tsx:98 msgid "Only contains letters, numbers, and hyphens" @@ -3321,8 +3434,8 @@ msgstr "아바타 생성기 열기" msgid "Open conversation options" msgstr "대화 옵션 열기" -#: src/view/com/composer/Composer.tsx:613 -#: src/view/com/composer/Composer.tsx:614 +#: src/view/com/composer/Composer.tsx:615 +#: src/view/com/composer/Composer.tsx:616 msgid "Open emoji picker" msgstr "이모티콘 선택기 열기" @@ -3346,7 +3459,7 @@ msgstr "뮤트한 단어 및 태그 설정 열기" msgid "Open navigation" msgstr "내비게이션 열기" -#: src/view/com/util/forms/PostDropdownBtn.tsx:237 +#: src/view/com/util/forms/PostDropdownBtn.tsx:247 msgid "Open post options menu" msgstr "게시물 옵션 메뉴 열기" @@ -3367,7 +3480,7 @@ msgstr "{numItems}번째 옵션을 엽니다" msgid "Opens accessibility settings" msgstr "접근성 설정을 엽니다" -#: src/view/screens/Log.tsx:54 +#: src/view/screens/Log.tsx:58 msgid "Opens additional details for a debug entry" msgstr "디버그 항목에 대한 추가 세부 정보를 엽니다" @@ -3449,11 +3562,6 @@ msgstr "검토 설정을 엽니다" msgid "Opens password reset form" msgstr "비밀번호 재설정 양식을 엽니다" -#: src/view/com/home/HomeHeaderLayout.web.tsx:77 -#: src/view/screens/Feeds.tsx:417 -#~ msgid "Opens screen to edit Saved Feeds" -#~ msgstr "저장한 피드를 편집할 수 있는 화면을 엽니다" - #: src/view/screens/Settings/index.tsx:617 msgid "Opens screen with all saved feeds" msgstr "모든 저장한 피드 화면을 엽니다" @@ -3483,8 +3591,8 @@ msgstr "시스템 로그 페이지를 엽니다" msgid "Opens the threads preferences" msgstr "스레드 설정을 엽니다" -#: src/view/com/notifications/FeedItem.tsx:427 -#: src/view/com/util/UserAvatar.tsx:409 +#: src/view/com/notifications/FeedItem.tsx:429 +#: src/view/com/util/UserAvatar.tsx:422 msgid "Opens this profile" msgstr "이 프로필을 엽니다" @@ -3497,7 +3605,7 @@ msgstr "{numItems}개 중 {0}번째 옵션" msgid "Optionally provide additional information below:" msgstr "선택 사항으로 아래에 추가 정보를 입력하세요:" -#: src/view/com/modals/Threadgate.tsx:90 +#: src/view/com/modals/Threadgate.tsx:92 msgid "Or combine these options:" msgstr "또는 다음 옵션을 결합하세요:" @@ -3553,11 +3661,11 @@ msgstr "비밀번호 변경됨" msgid "Password updated!" msgstr "비밀번호 변경됨" -#: src/view/com/util/post-embeds/GifEmbed.tsx:36 +#: src/view/com/util/post-embeds/GifEmbed.tsx:37 msgid "Pause" msgstr "일시 정지" -#: src/view/screens/Search/Search.tsx:387 +#: src/view/screens/Search/Search.tsx:369 msgid "People" msgstr "사람들" @@ -3602,7 +3710,7 @@ msgstr "고정한 피드" msgid "Pinned to your feeds" msgstr "내 피드에 고정됨" -#: src/view/com/util/post-embeds/GifEmbed.tsx:36 +#: src/view/com/util/post-embeds/GifEmbed.tsx:37 msgid "Play" msgstr "재생" @@ -3610,7 +3718,7 @@ msgstr "재생" msgid "Play {0}" msgstr "{0} 재생" -#: src/view/com/util/post-embeds/GifEmbed.tsx:35 +#: src/view/com/util/post-embeds/GifEmbed.tsx:36 msgid "Play or pause the GIF" msgstr "GIP를 재생하거나 일시 정지합니다" @@ -3688,13 +3796,13 @@ msgstr "정치" msgid "Porn" msgstr "음란물" -#: src/view/com/composer/Composer.tsx:477 -#: src/view/com/composer/Composer.tsx:485 +#: src/view/com/composer/Composer.tsx:479 +#: src/view/com/composer/Composer.tsx:487 msgctxt "action" msgid "Post" msgstr "게시하기" -#: src/view/com/post-thread/PostThread.tsx:430 +#: src/view/com/post-thread/PostThread.tsx:434 msgctxt "description" msgid "Post" msgstr "게시물" @@ -3709,7 +3817,7 @@ msgstr "{0} 님의 게시물" msgid "Post by @{0}" msgstr "@{0} 님의 게시물" -#: src/view/com/util/forms/PostDropdownBtn.tsx:129 +#: src/view/com/util/forms/PostDropdownBtn.tsx:132 msgid "Post deleted" msgstr "게시물 삭제됨" @@ -3775,9 +3883,9 @@ msgstr "호스팅 제공자를 변경하려면 누릅니다" msgid "Press to retry" msgstr "다시 시도하려면 누르기" -#: src/components/KnownFollowers.tsx:111 +#: src/components/KnownFollowers.tsx:116 msgid "Press to view followers of this account that you also follow" -msgstr "" +msgstr "내가 팔로우하는 이 계정의 팔로워를 보려면 누르세요" #: src/view/com/lightbox/Lightbox.web.tsx:150 msgid "Previous image" @@ -3845,18 +3953,18 @@ msgstr "일괄 뮤트하거나 차단할 수 있는 공개적이고 공유 가 msgid "Public, shareable lists which can drive feeds." msgstr "피드를 탐색할 수 있는 공개적이고 공유 가능한 목록입니다." -#: src/view/com/composer/Composer.tsx:462 +#: src/view/com/composer/Composer.tsx:464 msgid "Publish post" msgstr "게시물 게시하기" -#: src/view/com/composer/Composer.tsx:462 +#: src/view/com/composer/Composer.tsx:464 msgid "Publish reply" msgstr "답글 게시하기" -#: src/view/com/util/post-ctrls/RepostButton.tsx:115 -#: src/view/com/util/post-ctrls/RepostButton.tsx:127 -#: src/view/com/util/post-ctrls/RepostButton.web.tsx:78 -#: src/view/com/util/post-ctrls/RepostButton.web.tsx:81 +#: src/view/com/util/post-ctrls/RepostButton.tsx:116 +#: src/view/com/util/post-ctrls/RepostButton.tsx:128 +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:79 +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:82 msgid "Quote post" msgstr "게시물 인용" @@ -3876,7 +3984,7 @@ msgstr "계정 재활성화" msgid "Reason:" msgstr "이유:" -#: src/view/screens/Search/Search.tsx:970 +#: src/view/screens/Search/Search.tsx:933 msgid "Recent Searches" msgstr "최근 검색" @@ -3889,6 +3997,7 @@ msgid "Reload conversations" msgstr "대화 다시 불러오기" #: src/components/dialogs/MutedWords.tsx:286 +#: src/components/FeedCard.tsx:200 #: src/view/com/feeds/FeedSourceCard.tsx:317 #: src/view/com/modals/ListAddRemoveUsers.tsx:268 #: src/view/com/modals/SelfLabel.tsx:84 @@ -3901,7 +4010,7 @@ msgstr "제거" msgid "Remove account" msgstr "계정 제거" -#: src/view/com/util/UserAvatar.tsx:371 +#: src/view/com/util/UserAvatar.tsx:384 msgid "Remove Avatar" msgstr "아바타 제거" @@ -3931,6 +4040,7 @@ msgstr "피드를 제거하시겠습니까?" msgid "Remove from my feeds" msgstr "내 피드에서 제거" +#: src/components/FeedCard.tsx:195 #: src/view/com/feeds/FeedSourceCard.tsx:312 msgid "Remove from my feeds?" msgstr "내 피드에서 제거하시겠습니까?" @@ -3947,11 +4057,11 @@ msgstr "이미지 미리보기 제거" msgid "Remove mute word from your list" msgstr "목록에서 뮤트한 단어 제거" -#: src/view/screens/Search/Search.tsx:1011 +#: src/view/screens/Search/Search.tsx:974 msgid "Remove profile" msgstr "프로필 제거" -#: src/view/screens/Search/Search.tsx:1013 +#: src/view/screens/Search/Search.tsx:976 msgid "Remove profile from search history" msgstr "검색 기록에서 프로필을 제거합니다" @@ -3959,8 +4069,8 @@ msgstr "검색 기록에서 프로필을 제거합니다" msgid "Remove quote" msgstr "인용 제거" -#: src/view/com/util/post-ctrls/RepostButton.tsx:92 -#: src/view/com/util/post-ctrls/RepostButton.tsx:108 +#: src/view/com/util/post-ctrls/RepostButton.tsx:93 +#: src/view/com/util/post-ctrls/RepostButton.tsx:109 msgid "Remove repost" msgstr "재게시를 취소합니다" @@ -4000,11 +4110,19 @@ msgstr "Discover로 교체" msgid "Replies" msgstr "답글" -#: src/view/com/threadgate/WhoCanReply.tsx:98 -msgid "Replies to this thread are disabled" -msgstr "이 스레드에 대한 답글이 비활성화됩니다." +#: src/view/com/threadgate/WhoCanReply.tsx:66 +msgid "Replies disabled" +msgstr "답글 비활성화됨" -#: src/view/com/composer/Composer.tsx:475 +#: src/view/com/threadgate/WhoCanReply.tsx:123 +msgid "Replies on this thread are disabled" +msgstr "이 스레드에 대한 답글이 비활성화됨" + +#: src/view/com/threadgate/WhoCanReply.tsx:237 +msgid "Replies to this thread are disabled" +msgstr "이 스레드에 대한 답글이 비활성화됨" + +#: src/view/com/composer/Composer.tsx:477 msgctxt "action" msgid "Reply" msgstr "답글" @@ -4014,19 +4132,24 @@ msgid "Reply Filters" msgstr "답글 필터" #: src/view/com/post/Post.tsx:190 -#: src/view/com/posts/FeedItem.tsx:427 +#: src/view/com/posts/FeedItem.tsx:439 msgctxt "description" msgid "Reply to <0><1/>" msgstr "<0><1/> 님에게 보내는 답글" +#: src/view/com/posts/FeedItem.tsx:437 +msgctxt "description" +msgid "Reply to a blocked post" +msgstr "차단된 게시물에 보내는 답글" + #: src/components/dms/MessageMenu.tsx:132 #: src/components/dms/MessagesListBlockedFooter.tsx:77 #: src/components/dms/MessagesListBlockedFooter.tsx:84 msgid "Report" msgstr "신고" -#: src/view/com/profile/ProfileMenu.tsx:321 #: src/view/com/profile/ProfileMenu.tsx:324 +#: src/view/com/profile/ProfileMenu.tsx:327 msgid "Report Account" msgstr "계정 신고" @@ -4053,8 +4176,8 @@ msgstr "리스트 신고" msgid "Report message" msgstr "메시지 신고" -#: src/view/com/util/forms/PostDropdownBtn.tsx:394 -#: src/view/com/util/forms/PostDropdownBtn.tsx:396 +#: src/view/com/util/forms/PostDropdownBtn.tsx:404 +#: src/view/com/util/forms/PostDropdownBtn.tsx:406 msgid "Report post" msgstr "게시물 신고" @@ -4084,21 +4207,21 @@ msgstr "이 게시물 신고하기" msgid "Report this user" msgstr "이 사용자 신고하기" -#: src/view/com/util/post-ctrls/RepostButton.tsx:64 -#: src/view/com/util/post-ctrls/RepostButton.tsx:93 -#: src/view/com/util/post-ctrls/RepostButton.tsx:109 +#: src/view/com/util/post-ctrls/RepostButton.tsx:65 +#: src/view/com/util/post-ctrls/RepostButton.tsx:94 +#: src/view/com/util/post-ctrls/RepostButton.tsx:110 msgctxt "action" msgid "Repost" msgstr "재게시" -#: src/view/com/util/post-ctrls/RepostButton.web.tsx:69 -#: src/view/com/util/post-ctrls/RepostButton.web.tsx:73 +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:70 +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:74 msgid "Repost" msgstr "재게시" -#: src/view/com/util/post-ctrls/RepostButton.tsx:85 -#: src/view/com/util/post-ctrls/RepostButton.web.tsx:46 -#: src/view/com/util/post-ctrls/RepostButton.web.tsx:92 +#: src/view/com/util/post-ctrls/RepostButton.tsx:86 +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:47 +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:93 msgid "Repost or quote post" msgstr "재게시 또는 게시물 인용" @@ -4106,19 +4229,19 @@ msgstr "재게시 또는 게시물 인용" msgid "Reposted By" msgstr "재게시한 사용자" -#: src/view/com/posts/FeedItem.tsx:250 +#: src/view/com/posts/FeedItem.tsx:254 msgid "Reposted by {0}" msgstr "{0} 님이 재게시함" -#: src/view/com/posts/FeedItem.tsx:265 +#: src/view/com/posts/FeedItem.tsx:269 msgid "Reposted by <0><1/>" msgstr "<0><1/> 님이 재게시함" -#: src/view/com/notifications/FeedItem.tsx:170 +#: src/view/com/notifications/FeedItem.tsx:172 msgid "reposted your post" msgstr "이(가) 내 게시물을 재게시했습니다" -#: src/view/com/post-thread/PostThreadItem.tsx:201 +#: src/view/com/post-thread/PostThreadItem.tsx:202 msgid "Reposts of this post" msgstr "이 게시물의 재게시" @@ -4132,7 +4255,7 @@ msgstr "변경 요청" msgid "Request Code" msgstr "코드 요청" -#: src/view/screens/AccessibilitySettings.tsx:82 +#: src/view/screens/AccessibilitySettings.tsx:88 msgid "Require alt text before posting" msgstr "게시하기 전 대체 텍스트 필수" @@ -4282,6 +4405,7 @@ msgid "Saves image crop settings" msgstr "이미지 자르기 설정을 저장합니다" #: src/components/dms/ChatEmptyPill.tsx:33 +#: src/components/NewskieDialog.tsx:72 msgid "Say hello!" msgstr "인사해 보세요!" @@ -4299,9 +4423,9 @@ msgstr "맨 위로 스크롤" #: src/view/com/modals/ListAddRemoveUsers.tsx:75 #: src/view/com/util/forms/SearchInput.tsx:67 #: src/view/com/util/forms/SearchInput.tsx:79 -#: src/view/screens/Search/Search.tsx:452 -#: src/view/screens/Search/Search.tsx:822 -#: src/view/screens/Search/Search.tsx:850 +#: src/view/screens/Search/Search.tsx:421 +#: src/view/screens/Search/Search.tsx:791 +#: src/view/screens/Search/Search.tsx:813 #: src/view/shell/bottom-bar/BottomBar.tsx:179 #: src/view/shell/desktop/LeftNav.tsx:343 #: src/view/shell/desktop/Search.tsx:194 @@ -4315,7 +4439,7 @@ msgstr "검색" msgid "Search for \"{query}\"" msgstr "\"{query}\"에 대한 검색 결과" -#: src/view/screens/Search/Search.tsx:906 +#: src/view/screens/Search/Search.tsx:869 msgid "Search for \"{searchText}\"" msgstr "\"{searchText}\"에 대한 검색 결과" @@ -4496,8 +4620,8 @@ msgstr "{0} 님에게 신고 보내기" msgid "Send verification email" msgstr "인증 이메일 보내기" -#: src/view/com/util/forms/PostDropdownBtn.tsx:286 -#: src/view/com/util/forms/PostDropdownBtn.tsx:289 +#: src/view/com/util/forms/PostDropdownBtn.tsx:296 +#: src/view/com/util/forms/PostDropdownBtn.tsx:299 msgid "Send via direct message" msgstr "다이렉트 메시지로 보내기" @@ -4602,11 +4726,11 @@ msgctxt "action" msgid "Share" msgstr "공유" -#: src/view/com/profile/ProfileMenu.tsx:217 -#: src/view/com/profile/ProfileMenu.tsx:226 -#: src/view/com/util/forms/PostDropdownBtn.tsx:297 -#: src/view/com/util/forms/PostDropdownBtn.tsx:306 -#: src/view/com/util/post-ctrls/PostCtrls.tsx:297 +#: src/view/com/profile/ProfileMenu.tsx:220 +#: src/view/com/profile/ProfileMenu.tsx:229 +#: src/view/com/util/forms/PostDropdownBtn.tsx:307 +#: src/view/com/util/forms/PostDropdownBtn.tsx:316 +#: src/view/com/util/post-ctrls/PostCtrls.tsx:300 #: src/view/screens/ProfileList.tsx:428 msgid "Share" msgstr "공유" @@ -4619,9 +4743,9 @@ msgstr "멋진 이야기를 전하세요!" msgid "Share a fun fact!" msgstr "재미있는 사실을 전하세요!" -#: src/view/com/profile/ProfileMenu.tsx:375 -#: src/view/com/util/forms/PostDropdownBtn.tsx:451 -#: src/view/com/util/post-ctrls/PostCtrls.tsx:313 +#: src/view/com/profile/ProfileMenu.tsx:378 +#: src/view/com/util/forms/PostDropdownBtn.tsx:461 +#: src/view/com/util/post-ctrls/PostCtrls.tsx:316 msgid "Share anyway" msgstr "무시하고 공유" @@ -4645,12 +4769,12 @@ msgstr "연결된 웹사이트를 공유합니다" #: src/components/moderation/ContentHider.tsx:116 #: src/components/moderation/LabelPreference.tsx:136 -#: src/components/moderation/PostHider.tsx:121 +#: src/components/moderation/PostHider.tsx:122 #: src/view/screens/Settings/index.tsx:381 msgid "Show" msgstr "표시" -#: src/view/com/util/post-embeds/GifEmbed.tsx:167 +#: src/view/com/util/post-embeds/GifEmbed.tsx:169 msgid "Show alt text" msgstr "대체 텍스트 표시" @@ -4668,7 +4792,7 @@ msgstr "배지 표시" msgid "Show badge and filter from feeds" msgstr "배지 표시 및 피드에서 필터링" -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:211 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:215 msgid "Show follows similar to {0}" msgstr "{0} 님과 비슷한 팔로우 표시" @@ -4676,19 +4800,19 @@ msgstr "{0} 님과 비슷한 팔로우 표시" msgid "Show hidden replies" msgstr "숨겨진 답글 표시" -#: src/view/com/util/forms/PostDropdownBtn.tsx:336 -#: src/view/com/util/forms/PostDropdownBtn.tsx:338 +#: src/view/com/util/forms/PostDropdownBtn.tsx:346 +#: src/view/com/util/forms/PostDropdownBtn.tsx:348 msgid "Show less like this" msgstr "이런 항목 덜 보기" -#: src/view/com/post-thread/PostThreadItem.tsx:532 +#: src/view/com/post-thread/PostThreadItem.tsx:533 #: src/view/com/post/Post.tsx:227 -#: src/view/com/posts/FeedItem.tsx:392 +#: src/view/com/posts/FeedItem.tsx:396 msgid "Show More" msgstr "더 보기" -#: src/view/com/util/forms/PostDropdownBtn.tsx:328 -#: src/view/com/util/forms/PostDropdownBtn.tsx:330 +#: src/view/com/util/forms/PostDropdownBtn.tsx:338 +#: src/view/com/util/forms/PostDropdownBtn.tsx:340 msgid "Show more like this" msgstr "이런 항목 더 보기" @@ -4717,7 +4841,7 @@ msgid "Show Reposts" msgstr "재게시 표시" #: src/components/moderation/ContentHider.tsx:69 -#: src/components/moderation/PostHider.tsx:78 +#: src/components/moderation/PostHider.tsx:79 msgid "Show the content" msgstr "콘텐츠 표시" @@ -4818,8 +4942,10 @@ msgid "Software Dev" msgstr "소프트웨어 개발" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49 +#: src/view/com/threadgate/WhoCanReply.tsx:67 +#: src/view/com/threadgate/WhoCanReply.tsx:124 msgid "Some people can reply" -msgstr "몇몇 사람들이 답글을 달 수 있음" +msgstr "일부 사람들이 답글을 달 수 있음" #: src/screens/Messages/Conversation/index.tsx:106 msgid "Something went wrong" @@ -4836,7 +4962,7 @@ msgstr "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요" msgid "Something went wrong, please try again." msgstr "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." -#: src/App.native.tsx:85 +#: src/App.native.tsx:92 #: src/App.web.tsx:74 msgid "Sorry! Your session expired. Please log in again." msgstr "죄송합니다. 세션이 만료되었습니다. 다시 로그인해 주세요." @@ -4926,9 +5052,9 @@ msgstr "이 라벨러 구독하기" msgid "Subscribe to this list" msgstr "이 리스트 구독하기" -#: src/view/screens/Search/Search.tsx:425 -msgid "Suggested Follows" -msgstr "팔로우 추천" +#: src/view/screens/Search/Explore.tsx:331 +msgid "Suggested accounts" +msgstr "추천 계정" #: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:65 msgid "Suggested for you" @@ -5029,8 +5155,8 @@ msgstr "텍스트 파일 내용:" msgid "That handle is already taken." msgstr "이 핸들은 이미 사용 중입니다." -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:305 -#: src/view/com/profile/ProfileMenu.tsx:351 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:310 +#: src/view/com/profile/ProfileMenu.tsx:354 msgid "The account will be able to interact with you after unblocking." msgstr "차단을 해제하면 이 계정이 나와 상호작용할 수 있게 됩니다." @@ -5139,17 +5265,17 @@ msgstr "신고를 전송하는 동안 문제가 발생했습니다. 인터넷 msgid "There was an issue with fetching your app passwords" msgstr "앱 비밀번호를 가져오는 동안 문제가 발생했습니다" -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:103 -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:125 -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:139 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:107 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:129 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:143 #: src/view/com/post-thread/PostThreadFollowBtn.tsx:99 #: src/view/com/post-thread/PostThreadFollowBtn.tsx:111 -#: src/view/com/profile/ProfileMenu.tsx:109 -#: src/view/com/profile/ProfileMenu.tsx:120 -#: src/view/com/profile/ProfileMenu.tsx:135 -#: src/view/com/profile/ProfileMenu.tsx:146 -#: src/view/com/profile/ProfileMenu.tsx:160 -#: src/view/com/profile/ProfileMenu.tsx:173 +#: src/view/com/profile/ProfileMenu.tsx:112 +#: src/view/com/profile/ProfileMenu.tsx:123 +#: src/view/com/profile/ProfileMenu.tsx:138 +#: src/view/com/profile/ProfileMenu.tsx:149 +#: src/view/com/profile/ProfileMenu.tsx:163 +#: src/view/com/profile/ProfileMenu.tsx:176 msgid "There was an issue! {0}" msgstr "문제가 발생했습니다! {0}" @@ -5284,16 +5410,16 @@ msgstr "이 이름은 이미 사용 중입니다" msgid "This post has been deleted." msgstr "이 게시물은 삭제되었습니다." -#: src/view/com/util/forms/PostDropdownBtn.tsx:448 -#: src/view/com/util/post-ctrls/PostCtrls.tsx:310 +#: src/view/com/util/forms/PostDropdownBtn.tsx:458 +#: src/view/com/util/post-ctrls/PostCtrls.tsx:313 msgid "This post is only visible to logged-in users. It won't be visible to people who aren't logged in." msgstr "이 게시물은 로그인한 사용자에게만 표시됩니다. 로그인하지 않은 사용자에게는 표시되지 않습니다." -#: src/view/com/util/forms/PostDropdownBtn.tsx:430 +#: src/view/com/util/forms/PostDropdownBtn.tsx:440 msgid "This post will be hidden from feeds." msgstr "이 게시물을 피드에서 숨깁니다." -#: src/view/com/profile/ProfileMenu.tsx:372 +#: src/view/com/profile/ProfileMenu.tsx:375 msgid "This profile is only visible to logged-in users. It won't be visible to people who aren't logged in." msgstr "이 프로필은 로그인한 사용자에게만 표시됩니다. 로그인하지 않은 사용자에게는 표시되지 않습니다." @@ -5330,6 +5456,10 @@ msgstr "이 사용자는 내가 차단한 <0>{0} 리스트에 포함되어 msgid "This user is included in the <0>{0} list which you have muted." msgstr "이 사용자는 내가 뮤트한 <0>{0} 리스트에 포함되어 있습니다." +#: src/components/NewskieDialog.tsx:50 +msgid "This user is new here. Press for more info about when they joined." +msgstr "이 사용자는 새로 가입했습니다. 언제 가입했는지 자세한 정보를 보려면 누르세요." + #: src/view/com/profile/ProfileFollows.tsx:87 msgid "This user isn't following anyone." msgstr "이 사용자는 아무도 팔로우하지 않았습니다." @@ -5380,7 +5510,7 @@ msgid "Toggle to enable or disable adult content" msgstr "성인 콘텐츠 활성화 또는 비활성화 전환" #: src/screens/Hashtag.tsx:88 -#: src/view/screens/Search/Search.tsx:367 +#: src/view/screens/Search/Search.tsx:349 msgid "Top" msgstr "인기" @@ -5390,10 +5520,10 @@ msgstr "변형" #: src/components/dms/MessageMenu.tsx:103 #: src/components/dms/MessageMenu.tsx:105 -#: src/view/com/post-thread/PostThreadItem.tsx:674 -#: src/view/com/post-thread/PostThreadItem.tsx:676 -#: src/view/com/util/forms/PostDropdownBtn.tsx:267 -#: src/view/com/util/forms/PostDropdownBtn.tsx:269 +#: src/view/com/post-thread/PostThreadItem.tsx:681 +#: src/view/com/post-thread/PostThreadItem.tsx:683 +#: src/view/com/util/forms/PostDropdownBtn.tsx:277 +#: src/view/com/util/forms/PostDropdownBtn.tsx:279 msgid "Translate" msgstr "번역" @@ -5435,14 +5565,14 @@ msgstr "서비스에 연결할 수 없습니다. 인터넷 연결을 확인하 #: src/components/dms/MessagesListBlockedFooter.tsx:96 #: src/components/dms/MessagesListBlockedFooter.tsx:104 #: src/components/dms/MessagesListBlockedFooter.tsx:111 -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:188 -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:309 -#: src/view/com/profile/ProfileMenu.tsx:363 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:192 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:314 +#: src/view/com/profile/ProfileMenu.tsx:366 #: src/view/screens/ProfileList.tsx:626 msgid "Unblock" msgstr "차단 해제" -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:193 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:197 msgctxt "action" msgid "Unblock" msgstr "차단 해제" @@ -5452,19 +5582,19 @@ msgstr "차단 해제" msgid "Unblock account" msgstr "계정 차단 해제" -#: src/view/com/profile/ProfileMenu.tsx:301 -#: src/view/com/profile/ProfileMenu.tsx:307 +#: src/view/com/profile/ProfileMenu.tsx:304 +#: src/view/com/profile/ProfileMenu.tsx:310 msgid "Unblock Account" msgstr "계정 차단 해제" -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:303 -#: src/view/com/profile/ProfileMenu.tsx:345 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:308 +#: src/view/com/profile/ProfileMenu.tsx:348 msgid "Unblock Account?" msgstr "계정을 차단 해제하시겠습니까?" -#: src/view/com/util/post-ctrls/RepostButton.tsx:63 -#: src/view/com/util/post-ctrls/RepostButton.web.tsx:69 -#: src/view/com/util/post-ctrls/RepostButton.web.tsx:73 +#: src/view/com/util/post-ctrls/RepostButton.tsx:64 +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:70 +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:74 msgid "Undo repost" msgstr "재게시 취소" @@ -5477,12 +5607,12 @@ msgstr "언팔로우" msgid "Unfollow" msgstr "언팔로우" -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:233 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:237 msgid "Unfollow {0}" msgstr "{0} 님을 언팔로우" -#: src/view/com/profile/ProfileMenu.tsx:243 -#: src/view/com/profile/ProfileMenu.tsx:253 +#: src/view/com/profile/ProfileMenu.tsx:246 +#: src/view/com/profile/ProfileMenu.tsx:256 msgid "Unfollow Account" msgstr "계정 언팔로우" @@ -5499,8 +5629,8 @@ msgstr "언뮤트" msgid "Unmute {truncatedTag}" msgstr "{truncatedTag} 언뮤트" -#: src/view/com/profile/ProfileMenu.tsx:280 -#: src/view/com/profile/ProfileMenu.tsx:286 +#: src/view/com/profile/ProfileMenu.tsx:283 +#: src/view/com/profile/ProfileMenu.tsx:289 msgid "Unmute Account" msgstr "계정 언뮤트" @@ -5512,8 +5642,8 @@ msgstr "모든 {tag} 게시물 언뮤트" msgid "Unmute conversation" msgstr "알림 언뮤트" -#: src/view/com/util/forms/PostDropdownBtn.tsx:352 -#: src/view/com/util/forms/PostDropdownBtn.tsx:357 +#: src/view/com/util/forms/PostDropdownBtn.tsx:362 +#: src/view/com/util/forms/PostDropdownBtn.tsx:367 msgid "Unmute thread" msgstr "스레드 언뮤트" @@ -5567,20 +5697,20 @@ msgstr "대신 사진 업로드하기" msgid "Upload a text file to:" msgstr "텍스트 파일 업로드 경로:" -#: src/view/com/util/UserAvatar.tsx:339 -#: src/view/com/util/UserAvatar.tsx:342 +#: src/view/com/util/UserAvatar.tsx:352 +#: src/view/com/util/UserAvatar.tsx:355 #: src/view/com/util/UserBanner.tsx:123 #: src/view/com/util/UserBanner.tsx:126 msgid "Upload from Camera" msgstr "카메라에서 업로드" -#: src/view/com/util/UserAvatar.tsx:356 +#: src/view/com/util/UserAvatar.tsx:369 #: src/view/com/util/UserBanner.tsx:140 msgid "Upload from Files" msgstr "파일에서 업로드" -#: src/view/com/util/UserAvatar.tsx:350 -#: src/view/com/util/UserAvatar.tsx:354 +#: src/view/com/util/UserAvatar.tsx:363 +#: src/view/com/util/UserAvatar.tsx:367 #: src/view/com/util/UserBanner.tsx:134 #: src/view/com/util/UserBanner.tsx:138 msgid "Upload from Library" @@ -5688,7 +5818,7 @@ msgstr "사용자 이름 또는 이메일 주소" msgid "Users" msgstr "사용자" -#: src/view/com/threadgate/WhoCanReply.tsx:143 +#: src/view/com/threadgate/WhoCanReply.tsx:274 msgid "users followed by <0/>" msgstr "<0/> 님이 팔로우한 사용자" @@ -5699,7 +5829,7 @@ msgstr "<0/> 님이 팔로우한 사용자" msgid "Users I follow" msgstr "내가 팔로우하는 사용자" -#: src/view/com/modals/Threadgate.tsx:107 +#: src/view/com/modals/Threadgate.tsx:109 msgid "Users in \"{0}\"" msgstr "\"{0}\"에 있는 사용자" @@ -5752,11 +5882,15 @@ msgstr "비디오 게임" msgid "View {0}'s avatar" msgstr "{0} 님의 아바타를 봅니다" -#: src/view/com/notifications/FeedItem.tsx:213 +#: src/view/com/notifications/FeedItem.tsx:215 msgid "View {0}'s profile" msgstr "{0} 님의 프로필 보기" -#: src/view/screens/Log.tsx:52 +#: src/components/ProfileHoverCard/index.web.tsx:430 +msgid "View blocked user's profile" +msgstr "차단한 사용자의 프로필 보기" + +#: src/view/screens/Log.tsx:56 msgid "View debug entry" msgstr "디버그 항목 보기" @@ -5768,7 +5902,7 @@ msgstr "세부 정보 보기" msgid "View details for reporting a copyright violation" msgstr "저작권 위반 신고에 대한 세부 정보 보기" -#: src/view/com/posts/FeedSlice.tsx:120 +#: src/view/com/posts/FeedSlice.tsx:124 msgid "View full thread" msgstr "전체 스레드 보기" @@ -5776,8 +5910,9 @@ msgstr "전체 스레드 보기" msgid "View information about these labels" msgstr "이 라벨에 대한 정보 보기" -#: src/components/ProfileHoverCard/index.web.tsx:396 -#: src/components/ProfileHoverCard/index.web.tsx:429 +#: src/components/ProfileHoverCard/index.web.tsx:418 +#: src/components/ProfileHoverCard/index.web.tsx:436 +#: src/components/ProfileHoverCard/index.web.tsx:463 #: src/view/com/posts/AviFollowButton.tsx:58 #: src/view/com/posts/FeedErrorMessage.tsx:174 msgid "View profile" @@ -5795,10 +5930,10 @@ msgstr "{0} 님이 제공하는 라벨링 서비스 보기" msgid "View users who like this feed" msgstr "이 피드를 좋아하는 사용자 보기" -#: src/view/com/home/HomeHeaderLayout.web.tsx:78 +#: src/view/com/home/HomeHeaderLayout.web.tsx:79 #: src/view/com/home/HomeHeaderLayoutMobile.tsx:84 msgid "View your feeds and explore more" -msgstr "" +msgstr "내 피드를 보거나 새 피드를 탐색합니다" #: src/view/com/modals/LinkWarning.tsx:89 #: src/view/com/modals/LinkWarning.tsx:95 @@ -5879,7 +6014,7 @@ msgstr "죄송하지만 이 리스트를 불러올 수 없습니다. 이 문제 msgid "We're sorry, but we weren't able to load your muted words at this time. Please try again." msgstr "죄송하지만 현재 뮤트한 단어를 불러올 수 없습니다. 다시 시도해 주세요." -#: src/view/screens/Search/Search.tsx:270 +#: src/view/screens/Search/Search.tsx:206 msgid "We're sorry, but your search could not be completed. Please try again in a few minutes." msgstr "죄송하지만 검색을 완료할 수 없습니다. 몇 분 후에 다시 시도해 주세요." @@ -5893,8 +6028,8 @@ msgid "We're sorry! We can't find the page you were looking for." msgstr "죄송합니다. 페이지를 찾을 수 없습니다." #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:330 -msgid "We're sorry! You can only subscribe to ten labelers, and you've reached your limit of ten." -msgstr "죄송합니다. 라벨러는 10개까지만 구독할 수 있으며 10개에 도달했습니다." +msgid "We're sorry! You can only subscribe to twenty labelers, and you've reached your limit of twenty." +msgstr "죄송합니다. 라벨러는 20개까지만 구독할 수 있으며 20개에 도달했습니다." #: src/screens/Deactivated.tsx:128 msgid "Welcome back!" @@ -5923,10 +6058,20 @@ msgstr "알고리즘 피드에 어떤 언어를 표시하시겠습니까?" msgid "Who can message you?" msgstr "누구의 메시지를 허용하시겠습니까?" -#: src/view/com/modals/Threadgate.tsx:67 +#: src/view/com/modals/Threadgate.tsx:69 +#: src/view/com/threadgate/WhoCanReply.tsx:73 +#: src/view/com/threadgate/WhoCanReply.tsx:130 msgid "Who can reply" msgstr "답글을 달 수 있는 사람" +#: src/view/com/threadgate/WhoCanReply.tsx:206 +msgid "Who can reply dialog" +msgstr "답글을 달 수 있는 사람 대화 상자" + +#: src/view/com/threadgate/WhoCanReply.tsx:210 +msgid "Who can reply?" +msgstr "누가 답글을 달 수 있나요?" + #: src/screens/Home/NoFeedsPinned.tsx:79 #: src/screens/Messages/List/index.tsx:185 msgid "Whoops!" @@ -5965,7 +6110,7 @@ msgstr "가로" msgid "Write a message" msgstr "메시지를 입력하세요" -#: src/view/com/composer/Composer.tsx:549 +#: src/view/com/composer/Composer.tsx:551 msgid "Write post" msgstr "게시물 작성" @@ -6041,7 +6186,7 @@ msgstr "팔로워가 없습니다." #: src/screens/Profile/KnownFollowers.tsx:99 msgid "You don't follow any users who follow @{name}." -msgstr "" +msgstr "@{name} 님을 팔로우하는 사용자를 팔로우하고 있지 않습니다." #: src/view/com/modals/InviteCodes.tsx:67 msgid "You don't have any invite codes yet! We'll send you some when you've been on Bluesky for a little longer." @@ -6146,11 +6291,11 @@ msgstr "신고하려면 하나 이상의 라벨을 선택해야 합니다." msgid "You previously deactivated @{0}." msgstr "이전에 @{0}을(를) 비활성화했습니다." -#: src/view/com/util/forms/PostDropdownBtn.tsx:168 +#: src/view/com/util/forms/PostDropdownBtn.tsx:174 msgid "You will no longer receive notifications for this thread" msgstr "이 스레드에 대한 알림을 더 이상 받지 않습니다" -#: src/view/com/util/forms/PostDropdownBtn.tsx:171 +#: src/view/com/util/forms/PostDropdownBtn.tsx:170 msgid "You will now receive notifications for this thread" msgstr "이제 이 스레드에 대한 알림을 받습니다" From 55812b03940852f1f91cd0a46b5c093601c854a9 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Fri, 21 Jun 2024 12:41:06 -0400 Subject: [PATCH 72/81] Bsky short link service (#4542) * bskylink: scaffold service w/ initial config and schema * bskylink: implement link creation and redirects * bskylink: tidy * bskylink: tests * bskylink: tidy, add error handler * bskylink: add dockerfile * bskylink: add build * bskylink: fix some express plumbing * bskyweb: proxy fallthrough routes to link service redirects * bskyweb: build w/ link proxy * Add AASA to bskylink (#4588) --------- Co-authored-by: Hailey --- .../workflows/build-and-push-bskyweb-aws.yaml | 1 + .../workflows/build-and-push-link-aws.yaml | 55 + Dockerfile.bskylink | 41 + bskylink/package.json | 26 + bskylink/src/bin.ts | 24 + bskylink/src/config.ts | 82 ++ bskylink/src/context.ts | 33 + bskylink/src/db/index.ts | 174 +++ bskylink/src/db/migrations/001-init.ts | 15 + bskylink/src/db/migrations/index.ts | 5 + bskylink/src/db/migrations/provider.ts | 8 + bskylink/src/db/schema.ts | 17 + bskylink/src/index.ts | 45 + bskylink/src/logger.ts | 4 + bskylink/src/routes/create.ts | 111 ++ bskylink/src/routes/health.ts | 20 + bskylink/src/routes/index.ts | 17 + bskylink/src/routes/redirect.ts | 40 + bskylink/src/routes/siteAssociation.ts | 13 + bskylink/src/routes/util.ts | 23 + bskylink/src/util.ts | 8 + bskylink/tests/index.ts | 84 ++ bskylink/tests/infra/_common.sh | 157 +++ bskylink/tests/infra/docker-compose.yaml | 27 + bskylink/tests/infra/with-test-db.sh | 9 + bskylink/tsconfig.json | 10 + bskylink/yarn.lock | 1027 +++++++++++++++++ bskyweb/cmd/bskyweb/main.go | 9 +- bskyweb/cmd/bskyweb/server.go | 34 + 29 files changed, 2118 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build-and-push-link-aws.yaml create mode 100644 Dockerfile.bskylink create mode 100644 bskylink/package.json create mode 100644 bskylink/src/bin.ts create mode 100644 bskylink/src/config.ts create mode 100644 bskylink/src/context.ts create mode 100644 bskylink/src/db/index.ts create mode 100644 bskylink/src/db/migrations/001-init.ts create mode 100644 bskylink/src/db/migrations/index.ts create mode 100644 bskylink/src/db/migrations/provider.ts create mode 100644 bskylink/src/db/schema.ts create mode 100644 bskylink/src/index.ts create mode 100644 bskylink/src/logger.ts create mode 100644 bskylink/src/routes/create.ts create mode 100644 bskylink/src/routes/health.ts create mode 100644 bskylink/src/routes/index.ts create mode 100644 bskylink/src/routes/redirect.ts create mode 100644 bskylink/src/routes/siteAssociation.ts create mode 100644 bskylink/src/routes/util.ts create mode 100644 bskylink/src/util.ts create mode 100644 bskylink/tests/index.ts create mode 100755 bskylink/tests/infra/_common.sh create mode 100644 bskylink/tests/infra/docker-compose.yaml create mode 100755 bskylink/tests/infra/with-test-db.sh create mode 100644 bskylink/tsconfig.json create mode 100644 bskylink/yarn.lock diff --git a/.github/workflows/build-and-push-bskyweb-aws.yaml b/.github/workflows/build-and-push-bskyweb-aws.yaml index 6eb9485b14..bcd759b0ce 100644 --- a/.github/workflows/build-and-push-bskyweb-aws.yaml +++ b/.github/workflows/build-and-push-bskyweb-aws.yaml @@ -4,6 +4,7 @@ on: push: branches: - main + - divy/bskylink env: REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} diff --git a/.github/workflows/build-and-push-link-aws.yaml b/.github/workflows/build-and-push-link-aws.yaml new file mode 100644 index 0000000000..f91af48770 --- /dev/null +++ b/.github/workflows/build-and-push-link-aws.yaml @@ -0,0 +1,55 @@ +name: build-and-push-link-aws +on: + push: + branches: + - divy/bskylink + +env: + REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} + USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }} + PASSWORD: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_PASSWORD }} + IMAGE_NAME: bskylink + +jobs: + link-container-aws: + if: github.repository == 'bluesky-social/social-app' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup Docker buildx + uses: docker/setup-buildx-action@v1 + + - name: Log into registry ${{ env.REGISTRY }} + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ env.USERNAME}} + password: ${{ env.PASSWORD }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=sha,enable=true,priority=100,prefix=,suffix=,format=long + + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v4 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + file: ./Dockerfile.bskylink + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile.bskylink b/Dockerfile.bskylink new file mode 100644 index 0000000000..acde232256 --- /dev/null +++ b/Dockerfile.bskylink @@ -0,0 +1,41 @@ +FROM node:20.11-alpine3.18 as build + +# Move files into the image and install +WORKDIR /app + +COPY ./bskylink/package.json ./ +COPY ./bskylink/yarn.lock ./ +RUN yarn install --frozen-lockfile + +COPY ./bskylink ./ + +# build then prune dev deps +RUN yarn build +RUN yarn install --production --ignore-scripts --prefer-offline + +# Uses assets from build stage to reduce build size +FROM node:20.11-alpine3.18 + +RUN apk add --update dumb-init + +# Avoid zombie processes, handle signal forwarding +ENTRYPOINT ["dumb-init", "--"] + +WORKDIR /app +COPY --from=build /app /app +RUN mkdir /app/data && chown node /app/data + +VOLUME /app/data +EXPOSE 3000 +ENV LINK_PORT=3000 +ENV NODE_ENV=production +# potential perf issues w/ io_uring on this version of node +ENV UV_USE_IO_URING=0 + +# https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#non-root-user +USER node +CMD ["node", "--heapsnapshot-signal=SIGUSR2", "--enable-source-maps", "dist/bin.js"] + +LABEL org.opencontainers.image.source=https://github.com/bluesky-social/social-app +LABEL org.opencontainers.image.description="Bsky Link Service" +LABEL org.opencontainers.image.licenses=UNLICENSED diff --git a/bskylink/package.json b/bskylink/package.json new file mode 100644 index 0000000000..5fdee206b4 --- /dev/null +++ b/bskylink/package.json @@ -0,0 +1,26 @@ +{ + "name": "bskylink", + "version": "0.0.0", + "type": "module", + "main": "index.ts", + "scripts": { + "test": "./tests/infra/with-test-db.sh node --loader ts-node/esm --test ./tests/index.ts", + "build": "tsc" + }, + "dependencies": { + "@atproto/common": "^0.4.0", + "body-parser": "^1.20.2", + "cors": "^2.8.5", + "express": "^4.19.2", + "http-terminator": "^3.2.0", + "kysely": "^0.27.3", + "pg": "^8.12.0", + "pino": "^9.2.0", + "uint8arrays": "^5.1.0" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/pg": "^8.11.6", + "typescript": "^5.4.5" + } +} diff --git a/bskylink/src/bin.ts b/bskylink/src/bin.ts new file mode 100644 index 0000000000..17f068841b --- /dev/null +++ b/bskylink/src/bin.ts @@ -0,0 +1,24 @@ +import {Database, envToCfg, httpLogger, LinkService, readEnv} from './index.js' + +async function main() { + const env = readEnv() + const cfg = envToCfg(env) + if (cfg.db.migrationUrl) { + const migrateDb = Database.postgres({ + url: cfg.db.migrationUrl, + schema: cfg.db.schema, + }) + await migrateDb.migrateToLatestOrThrow() + await migrateDb.close() + } + const link = await LinkService.create(cfg) + await link.start() + httpLogger.info('link service is running') + process.on('SIGTERM', async () => { + httpLogger.info('link service is stopping') + await link.destroy() + httpLogger.info('link service is stopped') + }) +} + +main() diff --git a/bskylink/src/config.ts b/bskylink/src/config.ts new file mode 100644 index 0000000000..ce409cccca --- /dev/null +++ b/bskylink/src/config.ts @@ -0,0 +1,82 @@ +import {envInt, envList, envStr} from '@atproto/common' + +export type Config = { + service: ServiceConfig + db: DbConfig +} + +export type ServiceConfig = { + port: number + version?: string + hostnames: string[] + appHostname: string +} + +export type DbConfig = { + url: string + migrationUrl?: string + pool: DbPoolConfig + schema?: string +} + +export type DbPoolConfig = { + size: number + maxUses: number + idleTimeoutMs: number +} + +export type Environment = { + port?: number + version?: string + hostnames: string[] + appHostname?: string + dbPostgresUrl?: string + dbPostgresMigrationUrl?: string + dbPostgresSchema?: string + dbPostgresPoolSize?: number + dbPostgresPoolMaxUses?: number + dbPostgresPoolIdleTimeoutMs?: number +} + +export const readEnv = (): Environment => { + return { + port: envInt('LINK_PORT'), + version: envStr('LINK_VERSION'), + hostnames: envList('LINK_HOSTNAMES'), + appHostname: envStr('LINK_APP_HOSTNAME'), + dbPostgresUrl: envStr('LINK_DB_POSTGRES_URL'), + dbPostgresMigrationUrl: envStr('LINK_DB_POSTGRES_MIGRATION_URL'), + dbPostgresSchema: envStr('LINK_DB_POSTGRES_SCHEMA'), + dbPostgresPoolSize: envInt('LINK_DB_POSTGRES_POOL_SIZE'), + dbPostgresPoolMaxUses: envInt('LINK_DB_POSTGRES_POOL_MAX_USES'), + dbPostgresPoolIdleTimeoutMs: envInt( + 'LINK_DB_POSTGRES_POOL_IDLE_TIMEOUT_MS', + ), + } +} + +export const envToCfg = (env: Environment): Config => { + const serviceCfg: ServiceConfig = { + port: env.port ?? 3000, + version: env.version, + hostnames: env.hostnames, + appHostname: env.appHostname || 'bsky.app', + } + if (!env.dbPostgresUrl) { + throw new Error('Must configure postgres url (LINK_DB_POSTGRES_URL)') + } + const dbCfg: DbConfig = { + url: env.dbPostgresUrl, + migrationUrl: env.dbPostgresMigrationUrl, + schema: env.dbPostgresSchema, + pool: { + idleTimeoutMs: env.dbPostgresPoolIdleTimeoutMs ?? 10000, + maxUses: env.dbPostgresPoolMaxUses ?? Infinity, + size: env.dbPostgresPoolSize ?? 10, + }, + } + return { + service: serviceCfg, + db: dbCfg, + } +} diff --git a/bskylink/src/context.ts b/bskylink/src/context.ts new file mode 100644 index 0000000000..7e6f2f34e8 --- /dev/null +++ b/bskylink/src/context.ts @@ -0,0 +1,33 @@ +import {Config} from './config.js' +import Database from './db/index.js' + +export type AppContextOptions = { + cfg: Config + db: Database +} + +export class AppContext { + cfg: Config + db: Database + abortController = new AbortController() + + constructor(private opts: AppContextOptions) { + this.cfg = this.opts.cfg + this.db = this.opts.db + } + + static async fromConfig(cfg: Config, overrides?: Partial) { + const db = Database.postgres({ + url: cfg.db.url, + schema: cfg.db.schema, + poolSize: cfg.db.pool.size, + poolMaxUses: cfg.db.pool.maxUses, + poolIdleTimeoutMs: cfg.db.pool.idleTimeoutMs, + }) + return new AppContext({ + cfg, + db, + ...overrides, + }) + } +} diff --git a/bskylink/src/db/index.ts b/bskylink/src/db/index.ts new file mode 100644 index 0000000000..5f201cc07d --- /dev/null +++ b/bskylink/src/db/index.ts @@ -0,0 +1,174 @@ +import assert from 'assert' +import { + Kysely, + KyselyPlugin, + Migrator, + PluginTransformQueryArgs, + PluginTransformResultArgs, + PostgresDialect, + QueryResult, + RootOperationNode, + UnknownRow, +} from 'kysely' +import {default as Pg} from 'pg' + +import {dbLogger as log} from '../logger.js' +import {default as migrations} from './migrations/index.js' +import {DbMigrationProvider} from './migrations/provider.js' +import {DbSchema} from './schema.js' + +export class Database { + migrator: Migrator + destroyed = false + + constructor(public db: Kysely, public cfg: PgConfig) { + this.migrator = new Migrator({ + db, + migrationTableSchema: cfg.schema, + provider: new DbMigrationProvider(migrations), + }) + } + + static postgres(opts: PgOptions): Database { + const {schema, url, txLockNonce} = opts + const pool = + opts.pool ?? + new Pg.Pool({ + connectionString: url, + max: opts.poolSize, + maxUses: opts.poolMaxUses, + idleTimeoutMillis: opts.poolIdleTimeoutMs, + }) + + // Select count(*) and other pg bigints as js integer + Pg.types.setTypeParser(Pg.types.builtins.INT8, n => parseInt(n, 10)) + + // Setup schema usage, primarily for test parallelism (each test suite runs in its own pg schema) + if (schema && !/^[a-z_]+$/i.test(schema)) { + throw new Error(`Postgres schema must only contain [A-Za-z_]: ${schema}`) + } + + pool.on('error', onPoolError) + + const db = new Kysely({ + dialect: new PostgresDialect({pool}), + }) + + return new Database(db, { + pool, + schema, + url, + txLockNonce, + }) + } + + async transaction(fn: (db: Database) => Promise): Promise { + const leakyTxPlugin = new LeakyTxPlugin() + return this.db + .withPlugin(leakyTxPlugin) + .transaction() + .execute(txn => { + const dbTxn = new Database(txn, this.cfg) + return fn(dbTxn) + .catch(async err => { + leakyTxPlugin.endTx() + // ensure that all in-flight queries are flushed & the connection is open + await dbTxn.db.getExecutor().provideConnection(async () => {}) + throw err + }) + .finally(() => leakyTxPlugin.endTx()) + }) + } + + get schema(): string | undefined { + return this.cfg.schema + } + + get isTransaction() { + return this.db.isTransaction + } + + assertTransaction() { + assert(this.isTransaction, 'Transaction required') + } + + assertNotTransaction() { + assert(!this.isTransaction, 'Cannot be in a transaction') + } + + async close(): Promise { + if (this.destroyed) return + await this.db.destroy() + this.destroyed = true + } + + async migrateToOrThrow(migration: string) { + if (this.schema) { + await this.db.schema.createSchema(this.schema).ifNotExists().execute() + } + const {error, results} = await this.migrator.migrateTo(migration) + if (error) { + throw error + } + if (!results) { + throw new Error('An unknown failure occurred while migrating') + } + return results + } + + async migrateToLatestOrThrow() { + if (this.schema) { + await this.db.schema.createSchema(this.schema).ifNotExists().execute() + } + const {error, results} = await this.migrator.migrateToLatest() + if (error) { + throw error + } + if (!results) { + throw new Error('An unknown failure occurred while migrating') + } + return results + } +} + +export default Database + +export type PgConfig = { + pool: Pg.Pool + url: string + schema?: string + txLockNonce?: string +} + +type PgOptions = { + url: string + pool?: Pg.Pool + schema?: string + poolSize?: number + poolMaxUses?: number + poolIdleTimeoutMs?: number + txLockNonce?: string +} + +class LeakyTxPlugin implements KyselyPlugin { + private txOver = false + + endTx() { + this.txOver = true + } + + transformQuery(args: PluginTransformQueryArgs): RootOperationNode { + if (this.txOver) { + throw new Error('tx already failed') + } + return args.node + } + + async transformResult( + args: PluginTransformResultArgs, + ): Promise> { + return args.result + } +} + +const onPoolError = (err: Error) => log.error({err}, 'db pool error') diff --git a/bskylink/src/db/migrations/001-init.ts b/bskylink/src/db/migrations/001-init.ts new file mode 100644 index 0000000000..fe3bcf1867 --- /dev/null +++ b/bskylink/src/db/migrations/001-init.ts @@ -0,0 +1,15 @@ +import {Kysely} from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .createTable('link') + .addColumn('id', 'varchar', col => col.primaryKey()) + .addColumn('type', 'smallint', col => col.notNull()) // integer enum: 1->starterpack + .addColumn('path', 'varchar', col => col.notNull()) + .addUniqueConstraint('link_path_unique', ['path']) + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable('link').execute() +} diff --git a/bskylink/src/db/migrations/index.ts b/bskylink/src/db/migrations/index.ts new file mode 100644 index 0000000000..05e4de9377 --- /dev/null +++ b/bskylink/src/db/migrations/index.ts @@ -0,0 +1,5 @@ +import * as init from './001-init.js' + +export default { + '001': init, +} diff --git a/bskylink/src/db/migrations/provider.ts b/bskylink/src/db/migrations/provider.ts new file mode 100644 index 0000000000..bef93a48fd --- /dev/null +++ b/bskylink/src/db/migrations/provider.ts @@ -0,0 +1,8 @@ +import {Migration, MigrationProvider} from 'kysely' + +export class DbMigrationProvider implements MigrationProvider { + constructor(private migrations: Record) {} + async getMigrations(): Promise> { + return this.migrations + } +} diff --git a/bskylink/src/db/schema.ts b/bskylink/src/db/schema.ts new file mode 100644 index 0000000000..8d97f58005 --- /dev/null +++ b/bskylink/src/db/schema.ts @@ -0,0 +1,17 @@ +import {Selectable} from 'kysely' + +export type DbSchema = { + link: Link +} + +export interface Link { + id: string + type: LinkType + path: string +} + +export enum LinkType { + StarterPack = 1, +} + +export type LinkEntry = Selectable diff --git a/bskylink/src/index.ts b/bskylink/src/index.ts new file mode 100644 index 0000000000..ca425eee8c --- /dev/null +++ b/bskylink/src/index.ts @@ -0,0 +1,45 @@ +import events from 'node:events' +import http from 'node:http' + +import cors from 'cors' +import express from 'express' +import {createHttpTerminator, HttpTerminator} from 'http-terminator' + +import {Config} from './config.js' +import {AppContext} from './context.js' +import {default as routes, errorHandler} from './routes/index.js' + +export * from './config.js' +export * from './db/index.js' +export * from './logger.js' + +export class LinkService { + public server?: http.Server + private terminator?: HttpTerminator + + constructor(public app: express.Application, public ctx: AppContext) {} + + static async create(cfg: Config): Promise { + let app = express() + app.use(cors()) + + const ctx = await AppContext.fromConfig(cfg) + app = routes(ctx, app) + app.use(errorHandler) + + return new LinkService(app, ctx) + } + + async start() { + this.server = this.app.listen(this.ctx.cfg.service.port) + this.server.keepAliveTimeout = 90000 + this.terminator = createHttpTerminator({server: this.server}) + await events.once(this.server, 'listening') + } + + async destroy() { + this.ctx.abortController.abort() + await this.terminator?.terminate() + await this.ctx.db.close() + } +} diff --git a/bskylink/src/logger.ts b/bskylink/src/logger.ts new file mode 100644 index 0000000000..25bb590a1d --- /dev/null +++ b/bskylink/src/logger.ts @@ -0,0 +1,4 @@ +import {subsystemLogger} from '@atproto/common' + +export const httpLogger = subsystemLogger('bskylink') +export const dbLogger = subsystemLogger('bskylink:db') diff --git a/bskylink/src/routes/create.ts b/bskylink/src/routes/create.ts new file mode 100644 index 0000000000..db7c3f8090 --- /dev/null +++ b/bskylink/src/routes/create.ts @@ -0,0 +1,111 @@ +import assert from 'node:assert' + +import bodyParser from 'body-parser' +import {Express, Request} from 'express' + +import {AppContext} from '../context.js' +import {LinkType} from '../db/schema.js' +import {randomId} from '../util.js' +import {handler} from './util.js' + +export default function (ctx: AppContext, app: Express) { + return app.post( + '/link', + bodyParser.json(), + handler(async (req, res) => { + let path: string + if (typeof req.body?.path === 'string') { + path = req.body.path + } else { + return res.status(400).json({ + error: 'InvalidPath', + message: '"path" parameter is missing or not a string', + }) + } + if (!path.startsWith('/')) { + return res.status(400).json({ + error: 'InvalidPath', + message: + '"path" parameter must be formatted as a path, starting with a "/"', + }) + } + const parts = getPathParts(path) + if (parts.length === 3 && parts[0] === 'start') { + // link pattern: /start/{did}/{rkey} + if (!parts[1].startsWith('did:')) { + // enforce strong links + return res.status(400).json({ + error: 'InvalidPath', + message: + '"path" parameter for starter pack must contain the actor\'s DID', + }) + } + const id = await ensureLink(ctx, LinkType.StarterPack, parts) + return res.json({url: getUrl(ctx, req, id)}) + } + return res.status(400).json({ + error: 'InvalidPath', + message: '"path" parameter does not have a known format', + }) + }), + ) +} + +const ensureLink = async (ctx: AppContext, type: LinkType, parts: string[]) => { + const normalizedPath = normalizedPathFromParts(parts) + const created = await ctx.db.db + .insertInto('link') + .values({ + id: randomId(), + type, + path: normalizedPath, + }) + .onConflict(oc => oc.column('path').doNothing()) + .returningAll() + .executeTakeFirst() + if (created) { + return created.id + } + const found = await ctx.db.db + .selectFrom('link') + .selectAll() + .where('path', '=', normalizedPath) + .executeTakeFirstOrThrow() + return found.id +} + +const getUrl = (ctx: AppContext, req: Request, id: string) => { + if (!ctx.cfg.service.hostnames.length) { + assert(req.headers.host, 'request must be made with host header') + const baseUrl = + req.protocol === 'http' && req.headers.host.startsWith('localhost:') + ? `http://${req.headers.host}` + : `https://${req.headers.host}` + return `${baseUrl}/${id}` + } + const baseUrl = ctx.cfg.service.hostnames.includes(req.headers.host) + ? `https://${req.headers.host}` + : `https://${ctx.cfg.service.hostnames[0]}` + return `${baseUrl}/${id}` +} + +const normalizedPathFromParts = (parts: string[]): string => { + return ( + '/' + + parts + .map(encodeURIComponent) + .map(part => part.replaceAll('%3A', ':')) // preserve colons + .join('/') + ) +} + +const getPathParts = (path: string): string[] => { + if (path === '/') return [] + if (path.endsWith('/')) { + path = path.slice(0, -1) // ignore trailing slash + } + return path + .slice(1) // remove leading slash + .split('/') + .map(decodeURIComponent) +} diff --git a/bskylink/src/routes/health.ts b/bskylink/src/routes/health.ts new file mode 100644 index 0000000000..c8a30c59ea --- /dev/null +++ b/bskylink/src/routes/health.ts @@ -0,0 +1,20 @@ +import {Express} from 'express' +import {sql} from 'kysely' + +import {AppContext} from '../context.js' +import {handler} from './util.js' + +export default function (ctx: AppContext, app: Express) { + return app.get( + '/_health', + handler(async (_req, res) => { + const {version} = ctx.cfg.service + try { + await sql`select 1`.execute(ctx.db.db) + return res.send({version}) + } catch (err) { + return res.status(503).send({version, error: 'Service Unavailable'}) + } + }), + ) +} diff --git a/bskylink/src/routes/index.ts b/bskylink/src/routes/index.ts new file mode 100644 index 0000000000..f60b99bcb7 --- /dev/null +++ b/bskylink/src/routes/index.ts @@ -0,0 +1,17 @@ +import {Express} from 'express' + +import {AppContext} from '../context.js' +import {default as create} from './create.js' +import {default as health} from './health.js' +import {default as redirect} from './redirect.js' +import {default as siteAssociation} from './siteAssociation.js' + +export * from './util.js' + +export default function (ctx: AppContext, app: Express) { + app = health(ctx, app) // GET /_health + app = siteAssociation(ctx, app) // GET /.well-known/apple-app-site-association + app = create(ctx, app) // POST /link + app = redirect(ctx, app) // GET /:linkId (should go last due to permissive matching) + return app +} diff --git a/bskylink/src/routes/redirect.ts b/bskylink/src/routes/redirect.ts new file mode 100644 index 0000000000..7791ea815e --- /dev/null +++ b/bskylink/src/routes/redirect.ts @@ -0,0 +1,40 @@ +import assert from 'node:assert' + +import {DAY, SECOND} from '@atproto/common' +import {Express} from 'express' + +import {AppContext} from '../context.js' +import {handler} from './util.js' + +export default function (ctx: AppContext, app: Express) { + return app.get( + '/:linkId', + handler(async (req, res) => { + const linkId = req.params.linkId + assert( + typeof linkId === 'string', + 'express guarantees id parameter is a string', + ) + const found = await ctx.db.db + .selectFrom('link') + .selectAll() + .where('id', '=', linkId) + .executeTakeFirst() + if (!found) { + // potentially broken or mistyped link— send user to the app + res.setHeader('Location', `https://${ctx.cfg.service.appHostname}`) + res.setHeader('Cache-Control', 'no-store') + return res.status(302).end() + } + // build url from original url in order to preserve query params + const url = new URL( + req.originalUrl, + `https://${ctx.cfg.service.appHostname}`, + ) + url.pathname = found.path + res.setHeader('Location', url.href) + res.setHeader('Cache-Control', `max-age=${(7 * DAY) / SECOND}`) + return res.status(301).end() + }), + ) +} diff --git a/bskylink/src/routes/siteAssociation.ts b/bskylink/src/routes/siteAssociation.ts new file mode 100644 index 0000000000..ae3b42e304 --- /dev/null +++ b/bskylink/src/routes/siteAssociation.ts @@ -0,0 +1,13 @@ +import {Express} from 'express' + +import {AppContext} from '../context.js' + +export default function (ctx: AppContext, app: Express) { + return app.get('/.well-known/apple-app-site-association', (req, res) => { + res.json({ + appclips: { + apps: ['B3LX46C5HS.xyz.blueskyweb.app.AppClip'], + }, + }) + }) +} diff --git a/bskylink/src/routes/util.ts b/bskylink/src/routes/util.ts new file mode 100644 index 0000000000..bcac64b01d --- /dev/null +++ b/bskylink/src/routes/util.ts @@ -0,0 +1,23 @@ +import {ErrorRequestHandler, Request, RequestHandler, Response} from 'express' + +import {httpLogger} from '../logger.js' + +export type Handler = (req: Request, res: Response) => Awaited + +export const handler = (runHandler: Handler): RequestHandler => { + return async (req, res, next) => { + try { + await runHandler(req, res) + } catch (err) { + next(err) + } + } +} + +export const errorHandler: ErrorRequestHandler = (err, _req, res, next) => { + httpLogger.error({err}, 'request error') + if (res.headersSent) { + return next(err) + } + return res.status(500).end('server error') +} diff --git a/bskylink/src/util.ts b/bskylink/src/util.ts new file mode 100644 index 0000000000..0b57dd5c5b --- /dev/null +++ b/bskylink/src/util.ts @@ -0,0 +1,8 @@ +import {randomBytes} from 'node:crypto' + +import {toString} from 'uint8arrays' + +// 40bit random id of 5-7 characters +export const randomId = () => { + return toString(randomBytes(5), 'base58btc') +} diff --git a/bskylink/tests/index.ts b/bskylink/tests/index.ts new file mode 100644 index 0000000000..51449c21be --- /dev/null +++ b/bskylink/tests/index.ts @@ -0,0 +1,84 @@ +import assert from 'node:assert' +import {AddressInfo} from 'node:net' +import {after, before, describe, it} from 'node:test' + +import {Database, envToCfg, LinkService, readEnv} from '../src/index.js' + +describe('link service', async () => { + let linkService: LinkService + let baseUrl: string + before(async () => { + const env = readEnv() + const cfg = envToCfg({ + ...env, + hostnames: ['test.bsky.link'], + appHostname: 'test.bsky.app', + dbPostgresSchema: 'link_test', + dbPostgresUrl: process.env.DB_POSTGRES_URL, + }) + const migrateDb = Database.postgres({ + url: cfg.db.url, + schema: cfg.db.schema, + }) + await migrateDb.migrateToLatestOrThrow() + await migrateDb.close() + linkService = await LinkService.create(cfg) + await linkService.start() + const {port} = linkService.server?.address() as AddressInfo + baseUrl = `http://localhost:${port}` + }) + + after(async () => { + await linkService?.destroy() + }) + + it('creates a starter pack link', async () => { + const link = await getLink('/start/did:example:alice/xxx') + const url = new URL(link) + assert.strictEqual(url.origin, 'https://test.bsky.link') + assert.match(url.pathname, /^\/[a-z0-9]+$/i) + }) + + it('normalizes input paths and provides same link each time.', async () => { + const link1 = await getLink('/start/did%3Aexample%3Abob/yyy') + const link2 = await getLink('/start/did:example:bob/yyy/') + assert.strictEqual(link1, link2) + }) + + it('serves permanent redirect, preserving query params.', async () => { + const link = await getLink('/start/did:example:carol/zzz/') + const [status, location] = await getRedirect(`${link}?a=b`) + assert.strictEqual(status, 301) + const locationUrl = new URL(location) + assert.strictEqual( + locationUrl.pathname + locationUrl.search, + '/start/did:example:carol/zzz?a=b', + ) + }) + + async function getRedirect(link: string): Promise<[number, string]> { + const url = new URL(link) + const base = new URL(baseUrl) + url.protocol = base.protocol + url.host = base.host + const res = await fetch(url, {redirect: 'manual'}) + await res.arrayBuffer() // drain + assert( + res.status === 301 || res.status === 303, + 'response was not a redirect', + ) + return [res.status, res.headers.get('location') ?? ''] + } + + async function getLink(path: string): Promise { + const res = await fetch(new URL('/link', baseUrl), { + method: 'post', + headers: {'content-type': 'application/json'}, + body: JSON.stringify({path}), + }) + assert.strictEqual(res.status, 200) + const payload = await res.json() + assert(typeof payload.url === 'string') + return payload.url + } +}) diff --git a/bskylink/tests/infra/_common.sh b/bskylink/tests/infra/_common.sh new file mode 100755 index 0000000000..1587f5c70a --- /dev/null +++ b/bskylink/tests/infra/_common.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env sh + +# Exit if any command fails +set -e + +get_container_id() { + local compose_file=$1 + local service=$2 + if [ -z "${compose_file}" ] || [ -z "${service}" ]; then + echo "usage: get_container_id " + exit 1 + fi + + # first line of jq normalizes for docker compose breaking change, see docker/compose#10958 + docker compose --file $compose_file ps --format json --status running \ + | jq -sc '.[] | if type=="array" then .[] else . end' | jq -s \ + | jq -r '.[]? | select(.Service == "'${service}'") | .ID' +} + +# Exports all environment variables +export_env() { + export_pg_env +} + +# Exports postgres environment variables +export_pg_env() { + # Based on creds in compose.yaml + export PGPORT=5433 + export PGHOST=localhost + export PGUSER=pg + export PGPASSWORD=password + export PGDATABASE=postgres + export DB_POSTGRES_URL="postgresql://pg:password@127.0.0.1:5433/postgres" +} + + +pg_clear() { + local pg_uri=$1 + + for schema_name in `psql "${pg_uri}" -c "SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT LIKE 'pg_%' AND schema_name NOT LIKE 'information_schema';" -t`; do + psql "${pg_uri}" -c "DROP SCHEMA \"${schema_name}\" CASCADE;" + done +} + +pg_init() { + local pg_uri=$1 + + psql "${pg_uri}" -c "CREATE SCHEMA IF NOT EXISTS \"public\";" +} + +main_native() { + local services=${SERVICES} + local postgres_url_env_var=`[[ $services == *"db_test"* ]] && echo "DB_TEST_POSTGRES_URL" || echo "DB_POSTGRES_URL"` + + postgres_url="${!postgres_url_env_var}" + + if [ -n "${postgres_url}" ]; then + echo "Using ${postgres_url_env_var} (${postgres_url}) to connect to postgres." + pg_init "${postgres_url}" + else + echo "Postgres connection string missing did you set ${postgres_url_env_var}?" + exit 1 + fi + + cleanup() { + local services=$@ + + if [ -n "${postgres_url}" ] && [[ $services == *"db_test"* ]]; then + pg_clear "${postgres_url}" &> /dev/null + fi + } + + # trap SIGINT and performs cleanup + trap "on_sigint ${services}" INT + on_sigint() { + cleanup $@ + exit $? + } + + # Run the arguments as a command + DB_POSTGRES_URL="${postgres_url}" \ + "$@" + code=$? + + cleanup ${services} + + exit ${code} +} + +main_docker() { + # Expect a SERVICES env var to be set with the docker service names + local services=${SERVICES} + + dir=$(dirname $0) + compose_file="${dir}/docker-compose.yaml" + + # whether this particular script started the container(s) + started_container=false + + # performs cleanup as necessary, i.e. taking down containers + # if this script started them + cleanup() { + local services=$@ + echo # newline + if $started_container; then + docker compose --file $compose_file rm --force --stop --volumes ${services} + fi + } + + # trap SIGINT and performs cleanup + trap "on_sigint ${services}" INT + on_sigint() { + cleanup $@ + exit $? + } + + # check if all services are running already + not_running=false + for service in $services; do + container_id=$(get_container_id $compose_file $service) + if [ -z $container_id ]; then + not_running=true + break + fi + done + + # if any are missing, recreate all services + if $not_running; then + started_container=true + docker compose --file $compose_file up --wait --force-recreate ${services} + else + echo "all services ${services} are already running" + fi + + # do not exit when following commands fail, so we can intercept exit code & tear down docker + set +e + + # setup environment variables and run args + export_env + "$@" + # save return code for later + code=$? + + # performs cleanup as necessary + cleanup ${services} + exit ${code} +} + +# Main entry point +main() { + if ! docker ps >/dev/null 2>&1; then + echo "Docker unavailable. Running on host." + main_native $@ + else + main_docker $@ + fi +} diff --git a/bskylink/tests/infra/docker-compose.yaml b/bskylink/tests/infra/docker-compose.yaml new file mode 100644 index 0000000000..4bc939e01c --- /dev/null +++ b/bskylink/tests/infra/docker-compose.yaml @@ -0,0 +1,27 @@ +version: '3.8' +services: + # An ephermerally-stored postgres database for single-use test runs + db_test: &db_test + image: postgres:14.11-alpine + environment: + - POSTGRES_USER=pg + - POSTGRES_PASSWORD=password + ports: + - '5433:5432' + # Healthcheck ensures db is queryable when `docker-compose up --wait` completes + healthcheck: + test: 'pg_isready -U pg' + interval: 500ms + timeout: 10s + retries: 20 + # A persistently-stored postgres database + db: + <<: *db_test + ports: + - '5432:5432' + healthcheck: + disable: true + volumes: + - link_db:/var/lib/postgresql/data +volumes: + link_db: diff --git a/bskylink/tests/infra/with-test-db.sh b/bskylink/tests/infra/with-test-db.sh new file mode 100755 index 0000000000..cc083491a5 --- /dev/null +++ b/bskylink/tests/infra/with-test-db.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env sh + +# Example usage: +# ./with-test-db.sh psql postgresql://pg:password@localhost:5433/postgres -c 'select 1;' + +dir=$(dirname $0) +. ${dir}/_common.sh + +SERVICES="db_test" main "$@" diff --git a/bskylink/tsconfig.json b/bskylink/tsconfig.json new file mode 100644 index 0000000000..3c382acc41 --- /dev/null +++ b/bskylink/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "esModuleInterop": true, + "moduleResolution": "NodeNext", + "outDir": "dist", + "lib": ["ES2021.String"] + }, + "include": ["./src/index.ts", "./src/bin.ts"] + } diff --git a/bskylink/yarn.lock b/bskylink/yarn.lock new file mode 100644 index 0000000000..d2fa31456b --- /dev/null +++ b/bskylink/yarn.lock @@ -0,0 +1,1027 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@atproto/common-web@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.3.0.tgz#36da8c2c31d8cf8a140c3c8f03223319bf4430bb" + integrity sha512-67VnV6JJyX+ZWyjV7xFQMypAgDmjVaR9ZCuU/QW+mqlqI7fex2uL4Fv+7/jHadgzhuJHVd6OHOvNn0wR5WZYtA== + dependencies: + graphemer "^1.4.0" + multiformats "^9.9.0" + uint8arrays "3.0.0" + zod "^3.21.4" + +"@atproto/common@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.4.0.tgz#d77696c7eb545426df727837d9ee333b429fe7ef" + integrity sha512-yOXuPlCjT/OK9j+neIGYn9wkxx/AlxQSucysAF0xgwu0Ji8jAtKBf9Jv6R5ObYAjAD/kVUvEYumle+Yq/R9/7g== + dependencies: + "@atproto/common-web" "^0.3.0" + "@ipld/dag-cbor" "^7.0.3" + cbor-x "^1.5.1" + iso-datestring-validator "^2.2.2" + multiformats "^9.9.0" + pino "^8.15.0" + +"@cbor-extract/cbor-extract-darwin-arm64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz#8d65cb861a99622e1b4a268e2d522d2ec6137338" + integrity sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w== + +"@cbor-extract/cbor-extract-darwin-x64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz#9fbec199c888c5ec485a1839f4fad0485ab6c40a" + integrity sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w== + +"@cbor-extract/cbor-extract-linux-arm64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz#bf77e0db4a1d2200a5aa072e02210d5043e953ae" + integrity sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ== + +"@cbor-extract/cbor-extract-linux-arm@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz#491335037eb8533ed8e21b139c59f6df04e39709" + integrity sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q== + +"@cbor-extract/cbor-extract-linux-x64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz#672574485ccd24759bf8fb8eab9dbca517d35b97" + integrity sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw== + +"@cbor-extract/cbor-extract-win32-x64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz#4b3f07af047f984c082de34b116e765cb9af975f" + integrity sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w== + +"@ipld/dag-cbor@^7.0.3": + version "7.0.3" + resolved "https://registry.yarnpkg.com/@ipld/dag-cbor/-/dag-cbor-7.0.3.tgz#aa31b28afb11a807c3d627828a344e5521ac4a1e" + integrity sha512-1VVh2huHsuohdXC1bGJNE8WR72slZ9XE2T3wbBBq31dm7ZBatmKLLxrB+XAqafxfRFjv08RZmj/W/ZqaM13AuA== + dependencies: + cborg "^1.6.0" + multiformats "^9.5.4" + +"@types/cors@^2.8.17": + version "2.8.17" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b" + integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA== + dependencies: + "@types/node" "*" + +"@types/node@*": + version "20.14.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.2.tgz#a5f4d2bcb4b6a87bffcaa717718c5a0f208f4a18" + integrity sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q== + dependencies: + undici-types "~5.26.4" + +"@types/pg@^8.11.6": + version "8.11.6" + resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.11.6.tgz#a2d0fb0a14b53951a17df5197401569fb9c0c54b" + integrity sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ== + dependencies: + "@types/node" "*" + pg-protocol "*" + pg-types "^4.0.1" + +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +atomic-sleep@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" + integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +body-parser@1.20.2, body-parser@^1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +boolean@^3.1.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b" + integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw== + +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +cbor-extract@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cbor-extract/-/cbor-extract-2.2.0.tgz#cee78e630cbeae3918d1e2e58e0cebaf3a3be840" + integrity sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA== + dependencies: + node-gyp-build-optional-packages "5.1.1" + optionalDependencies: + "@cbor-extract/cbor-extract-darwin-arm64" "2.2.0" + "@cbor-extract/cbor-extract-darwin-x64" "2.2.0" + "@cbor-extract/cbor-extract-linux-arm" "2.2.0" + "@cbor-extract/cbor-extract-linux-arm64" "2.2.0" + "@cbor-extract/cbor-extract-linux-x64" "2.2.0" + "@cbor-extract/cbor-extract-win32-x64" "2.2.0" + +cbor-x@^1.5.1: + version "1.5.9" + resolved "https://registry.yarnpkg.com/cbor-x/-/cbor-x-1.5.9.tgz#ed6b2afcd7884bdd697674bfb7332c1473a13ecf" + integrity sha512-OEI5rEu3MeR0WWNUXuIGkxmbXVhABP+VtgAXzm48c9ulkrsvxshjjk94XSOGphyAKeNGLPfAxxzEtgQ6rEVpYQ== + optionalDependencies: + cbor-extract "^2.2.0" + +cborg@^1.6.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/cborg/-/cborg-1.10.2.tgz#83cd581b55b3574c816f82696307c7512db759a1" + integrity sha512-b3tFPA9pUr2zCUiCfRd2+wok2/LBSNUMKOuRRok+WlvvAgEt/PlbgPTsZUcwCOs53IJvLgTp0eotwtosE6njug== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== + +cors@^2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +delay@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" + integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-libc@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +express@^4.19.2: + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.2" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.6.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +fast-printf@^1.6.9: + version "1.6.9" + resolved "https://registry.yarnpkg.com/fast-printf/-/fast-printf-1.6.9.tgz#212f56570d2dc8ccdd057ee93d50dd414d07d676" + integrity sha512-FChq8hbz65WMj4rstcQsFB0O7Cy++nmbNfLYnD9cYv2cRn8EG6k/MGn9kO/tjO66t09DLDugj3yL+V2o6Qftrg== + dependencies: + boolean "^3.1.4" + +fast-redact@^3.1.1: + version "3.5.0" + resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.5.0.tgz#e9ea02f7e57d0cd8438180083e93077e496285e4" + integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A== + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-terminator@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/http-terminator/-/http-terminator-3.2.0.tgz#bc158d2694b733ca4fbf22a35065a81a609fb3e9" + integrity sha512-JLjck1EzPaWjsmIf8bziM3p9fgR1Y3JoUKAkyYEbZmFrIvJM6I8vVJfBGWlEtV9IWOvzNnaTtjuwZeBY2kwB4g== + dependencies: + delay "^5.0.0" + p-wait-for "^3.2.0" + roarr "^7.0.4" + type-fest "^2.3.3" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +iso-datestring-validator@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz#2daa80d2900b7a954f9f731d42f96ee0c19a6895" + integrity sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA== + +kysely@^0.27.3: + version "0.27.3" + resolved "https://registry.yarnpkg.com/kysely/-/kysely-0.27.3.tgz#6cc6c757040500b43c4ac596cdbb12be400ee276" + integrity sha512-lG03Ru+XyOJFsjH3OMY6R/9U38IjDPfnOfDgO3ynhbDr+Dz8fak+X6L62vqu3iybQnj+lG84OttBuU9KY3L9kA== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multiformats@^13.0.0: + version "13.1.1" + resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-13.1.1.tgz#b22ce4df26330d2cf0d69f5bdcbc9a787095a6e5" + integrity sha512-JiptvwMmlxlzIlLLwhCi/srf/nk409UL0eUBr0kioRJq15hqqKyg68iftrBvhCRjR6Rw4fkNnSc4ZJXJDuta/Q== + +multiformats@^9.4.2, multiformats@^9.5.4, multiformats@^9.9.0: + version "9.9.0" + resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37" + integrity sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +node-gyp-build-optional-packages@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz#52b143b9dd77b7669073cbfe39e3f4118bfc603c" + integrity sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw== + dependencies: + detect-libc "^2.0.1" + +object-assign@^4: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== + +obuf@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-exit-leak-free@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8" + integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== + +p-timeout@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" + integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== + dependencies: + p-finally "^1.0.0" + +p-wait-for@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/p-wait-for/-/p-wait-for-3.2.0.tgz#640429bcabf3b0dd9f492c31539c5718cb6a3f1f" + integrity sha512-wpgERjNkLrBiFmkMEjuZJEWKKDrNfHCKA1OhyN1wg1FrLkULbviEy6py1AyJUgZ72YWFbZ38FIpnqvVqAlDUwA== + dependencies: + p-timeout "^3.0.0" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +pg-cloudflare@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98" + integrity sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q== + +pg-connection-string@^2.6.4: + version "2.6.4" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.4.tgz#f543862adfa49fa4e14bc8a8892d2a84d754246d" + integrity sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA== + +pg-int8@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" + integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== + +pg-numeric@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pg-numeric/-/pg-numeric-1.0.2.tgz#816d9a44026086ae8ae74839acd6a09b0636aa3a" + integrity sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw== + +pg-pool@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.2.tgz#3a592370b8ae3f02a7c8130d245bc02fa2c5f3f2" + integrity sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg== + +pg-protocol@*, pg-protocol@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.6.1.tgz#21333e6d83b01faaebfe7a33a7ad6bfd9ed38cb3" + integrity sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg== + +pg-types@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" + integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== + dependencies: + pg-int8 "1.0.1" + postgres-array "~2.0.0" + postgres-bytea "~1.0.0" + postgres-date "~1.0.4" + postgres-interval "^1.1.0" + +pg-types@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-4.0.2.tgz#399209a57c326f162461faa870145bb0f918b76d" + integrity sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng== + dependencies: + pg-int8 "1.0.1" + pg-numeric "1.0.2" + postgres-array "~3.0.1" + postgres-bytea "~3.0.0" + postgres-date "~2.1.0" + postgres-interval "^3.0.0" + postgres-range "^1.1.1" + +pg@^8.12.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.12.0.tgz#9341724db571022490b657908f65aee8db91df79" + integrity sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ== + dependencies: + pg-connection-string "^2.6.4" + pg-pool "^3.6.2" + pg-protocol "^1.6.1" + pg-types "^2.1.0" + pgpass "1.x" + optionalDependencies: + pg-cloudflare "^1.1.1" + +pgpass@1.x: + version "1.0.5" + resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.5.tgz#9b873e4a564bb10fa7a7dbd55312728d422a223d" + integrity sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug== + dependencies: + split2 "^4.1.0" + +pino-abstract-transport@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz#97f9f2631931e242da531b5c66d3079c12c9d1b5" + integrity sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q== + dependencies: + readable-stream "^4.0.0" + split2 "^4.0.0" + +pino-std-serializers@^6.0.0: + version "6.2.2" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz#d9a9b5f2b9a402486a5fc4db0a737570a860aab3" + integrity sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA== + +pino-std-serializers@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz#7c625038b13718dbbd84ab446bd673dc52259e3b" + integrity sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA== + +pino@^8.15.0: + version "8.21.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-8.21.0.tgz#e1207f3675a2722940d62da79a7a55a98409f00d" + integrity sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q== + dependencies: + atomic-sleep "^1.0.0" + fast-redact "^3.1.1" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^1.2.0" + pino-std-serializers "^6.0.0" + process-warning "^3.0.0" + quick-format-unescaped "^4.0.3" + real-require "^0.2.0" + safe-stable-stringify "^2.3.1" + sonic-boom "^3.7.0" + thread-stream "^2.6.0" + +pino@^9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-9.2.0.tgz#e77a9516f3a3e5550d9b76d9f65ac6118ef02bdd" + integrity sha512-g3/hpwfujK5a4oVbaefoJxezLzsDgLcNJeITvC6yrfwYeT9la+edCK42j5QpEQSQCZgTKapXvnQIdgZwvRaZug== + dependencies: + atomic-sleep "^1.0.0" + fast-redact "^3.1.1" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^1.2.0" + pino-std-serializers "^7.0.0" + process-warning "^3.0.0" + quick-format-unescaped "^4.0.3" + real-require "^0.2.0" + safe-stable-stringify "^2.3.1" + sonic-boom "^4.0.1" + thread-stream "^3.0.0" + +postgres-array@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" + integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== + +postgres-array@~3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-3.0.2.tgz#68d6182cb0f7f152a7e60dc6a6889ed74b0a5f98" + integrity sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog== + +postgres-bytea@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" + integrity sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w== + +postgres-bytea@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-3.0.0.tgz#9048dc461ac7ba70a6a42d109221619ecd1cb089" + integrity sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw== + dependencies: + obuf "~1.1.2" + +postgres-date@~1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" + integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== + +postgres-date@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-2.1.0.tgz#b85d3c1fb6fb3c6c8db1e9942a13a3bf625189d0" + integrity sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA== + +postgres-interval@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" + integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== + dependencies: + xtend "^4.0.0" + +postgres-interval@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-3.0.0.tgz#baf7a8b3ebab19b7f38f07566c7aab0962f0c86a" + integrity sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw== + +postgres-range@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/postgres-range/-/postgres-range-1.1.4.tgz#a59c5f9520909bcec5e63e8cf913a92e4c952863" + integrity sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w== + +process-warning@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-3.0.0.tgz#96e5b88884187a1dce6f5c3166d611132058710b" + integrity sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +quick-format-unescaped@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" + integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +readable-stream@^4.0.0: + version "4.5.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" + integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + +real-require@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78" + integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== + +roarr@^7.0.4: + version "7.21.1" + resolved "https://registry.yarnpkg.com/roarr/-/roarr-7.21.1.tgz#fd6452ca822a65f736c35e5372f04ee9f2ca3851" + integrity sha512-3niqt5bXFY1InKU8HKWqqYTYjtrBaxBMnXELXCXUYgtNYGUtZM5rB46HIC430AyacL95iEniGf7RgqsesykLmQ== + dependencies: + fast-printf "^1.6.9" + safe-stable-stringify "^2.4.3" + semver-compare "^1.0.0" + +safe-buffer@5.2.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-stable-stringify@^2.3.1, safe-stable-stringify@^2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" + integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +side-channel@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +sonic-boom@^3.7.0: + version "3.8.1" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.8.1.tgz#d5ba8c4e26d6176c9a1d14d549d9ff579a163422" + integrity sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg== + dependencies: + atomic-sleep "^1.0.0" + +sonic-boom@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-4.0.1.tgz#515b7cef2c9290cb362c4536388ddeece07aed30" + integrity sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ== + dependencies: + atomic-sleep "^1.0.0" + +split2@^4.0.0, split2@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +string_decoder@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +thread-stream@^2.6.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.7.0.tgz#d8a8e1b3fd538a6cca8ce69dbe5d3d097b601e11" + integrity sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw== + dependencies: + real-require "^0.2.0" + +thread-stream@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-3.1.0.tgz#4b2ef252a7c215064507d4ef70c05a5e2d34c4f1" + integrity sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A== + dependencies: + real-require "^0.2.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +type-fest@^2.3.3: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@^5.4.5: + version "5.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" + integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== + +uint8arrays@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.0.0.tgz#260869efb8422418b6f04e3fac73a3908175c63b" + integrity sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA== + dependencies: + multiformats "^9.4.2" + +uint8arrays@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-5.1.0.tgz#14047c9bdf825d025b7391299436e5e50e7270f1" + integrity sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww== + dependencies: + multiformats "^13.0.0" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +zod@^3.21.4: + version "3.23.8" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" + integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== diff --git a/bskyweb/cmd/bskyweb/main.go b/bskyweb/cmd/bskyweb/main.go index 5185ff573a..49629e3f2b 100644 --- a/bskyweb/cmd/bskyweb/main.go +++ b/bskyweb/cmd/bskyweb/main.go @@ -35,7 +35,7 @@ func run(args []string) { Flags: []cli.Flag{ &cli.StringFlag{ Name: "appview-host", - Usage: "method, hostname, and port of PDS instance", + Usage: "scheme, hostname, and port of PDS instance", Value: "http://localhost:2584", // retain old PDS env var for easy transition EnvVars: []string{"ATP_APPVIEW_HOST", "ATP_PDS_HOST"}, @@ -47,6 +47,13 @@ func run(args []string) { Value: ":8100", EnvVars: []string{"HTTP_ADDRESS"}, }, + &cli.StringFlag{ + Name: "link-host", + Usage: "scheme, hostname, and port of link service", + Required: false, + Value: "", + EnvVars: []string{"LINK_HOST"}, + }, &cli.BoolFlag{ Name: "debug", Usage: "Enable debug mode", diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index bb81e780f5..6d32e0e212 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -6,6 +6,7 @@ import ( "fmt" "io/fs" "net/http" + "net/url" "os" "os/signal" "strings" @@ -36,6 +37,7 @@ func serve(cctx *cli.Context) error { debug := cctx.Bool("debug") httpAddress := cctx.String("http-address") appviewHost := cctx.String("appview-host") + linkHost := cctx.String("link-host") // Echo e := echo.New() @@ -221,6 +223,14 @@ func serve(cctx *cli.Context) error { e.GET("/profile/:handleOrDID/post/:rkey/liked-by", server.WebGeneric) e.GET("/profile/:handleOrDID/post/:rkey/reposted-by", server.WebGeneric) + if linkHost != "" { + linkUrl, err := url.Parse(linkHost) + if err != nil { + return err + } + e.Group("/:linkId", server.LinkProxyMiddleware(linkUrl)) + } + // Start the server. log.Infof("starting server address=%s", httpAddress) go func() { @@ -292,6 +302,30 @@ func (srv *Server) Download(c echo.Context) error { return c.Redirect(http.StatusFound, "/") } +// Handler for proxying top-level paths to link service, which ends up serving a redirect +func (srv *Server) LinkProxyMiddleware(url *url.URL) echo.MiddlewareFunc { + return middleware.ProxyWithConfig( + middleware.ProxyConfig{ + Balancer: middleware.NewRoundRobinBalancer( + []*middleware.ProxyTarget{{URL: url}}, + ), + Skipper: func(c echo.Context) bool { + req := c.Request() + if req.Method == "GET" && + strings.LastIndex(strings.TrimRight(req.URL.Path, "/"), "/") == 0 && // top-level path + !strings.HasPrefix(req.URL.Path, "/_") { // e.g. /_health endpoint + return false + } + return true + }, + RetryCount: 2, + ErrorHandler: func(c echo.Context, err error) error { + return c.Redirect(302, "/") + }, + }, + ) +} + // handler for endpoint that have no specific server-side handling func (srv *Server) WebGeneric(c echo.Context) error { data := pongo2.Context{} From cb376479493dbc3a24876449f6466789ddcef6ea Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 21 Jun 2024 14:50:49 -0500 Subject: [PATCH 73/81] Fetch more than 3 suggested follows after first load (#4595) * Fetch more than 3 sugg follows after first load * Preview handling via overfetching --- src/state/queries/suggested-follows.ts | 9 +++++++-- src/view/screens/Search/Explore.tsx | 25 +++++++++++++++++++------ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/state/queries/suggested-follows.ts b/src/state/queries/suggested-follows.ts index 40251d43d7..a1244721a2 100644 --- a/src/state/queries/suggested-follows.ts +++ b/src/state/queries/suggested-follows.ts @@ -34,13 +34,14 @@ const suggestedFollowsByActorQueryKey = (did: string) => [ did, ] -type SuggestedFollowsOptions = {limit?: number} +type SuggestedFollowsOptions = {limit?: number; subsequentPageLimit?: number} export function useSuggestedFollowsQuery(options?: SuggestedFollowsOptions) { const {currentAccount} = useSession() const agent = useAgent() const moderationOpts = useModerationOpts() const {data: preferences} = usePreferencesQuery() + const limit = options?.limit || 25 return useInfiniteQuery< AppBskyActorGetSuggestions.OutputSchema, @@ -54,9 +55,13 @@ export function useSuggestedFollowsQuery(options?: SuggestedFollowsOptions) { queryKey: suggestedFollowsQueryKey(options), queryFn: async ({pageParam}) => { const contentLangs = getContentLanguages().join(',') + const maybeDifferentLimit = + options?.subsequentPageLimit && pageParam + ? options.subsequentPageLimit + : limit const res = await agent.app.bsky.actor.getSuggestions( { - limit: options?.limit || 25, + limit: maybeDifferentLimit, cursor: pageParam, }, { diff --git a/src/view/screens/Search/Explore.tsx b/src/view/screens/Search/Explore.tsx index dd93bf8130..f6988548b2 100644 --- a/src/view/screens/Search/Explore.tsx +++ b/src/view/screens/Search/Explore.tsx @@ -282,7 +282,7 @@ export function Explore() { isFetchingNextPage: isFetchingNextProfilesPage, error: profilesError, fetchNextPage: fetchNextProfilesPage, - } = useSuggestedFollowsQuery({limit: 3}) + } = useSuggestedFollowsQuery({limit: 6, subsequentPageLimit: 10}) const { data: feeds, hasNextPage: hasNextFeedsPage, @@ -290,7 +290,7 @@ export function Explore() { isFetchingNextPage: isFetchingNextFeedsPage, error: feedsError, fetchNextPage: fetchNextFeedsPage, - } = useGetPopularFeedsQuery({limit: 3}) + } = useGetPopularFeedsQuery({limit: 10}) const isLoadingMoreProfiles = isFetchingNextProfilesPage && !isLoadingProfiles const onLoadMoreProfiles = React.useCallback(async () => { @@ -340,11 +340,12 @@ export function Explore() { // Currently the responses contain duplicate items. // Needs to be fixed on backend, but let's dedupe to be safe. let seen = new Set() + const profileItems: ExploreScreenItems[] = [] for (const page of profiles.pages) { for (const actor of page.actors) { if (!seen.has(actor.did)) { seen.add(actor.did) - i.push({ + profileItems.push({ type: 'profile', key: actor.did, profile: actor, @@ -354,13 +355,19 @@ export function Explore() { } if (hasNextProfilesPage) { + // splice off 3 as previews if we have a next page + const previews = profileItems.splice(-3) + // push remainder + i.push(...profileItems) i.push({ type: 'loadMore', key: 'loadMoreProfiles', isLoadingMore: isLoadingMoreProfiles, onLoadMore: onLoadMoreProfiles, - items: i.filter(item => item.type === 'profile').slice(-3), + items: previews, }) + } else { + i.push(...profileItems) } } else { if (profilesError) { @@ -390,11 +397,12 @@ export function Explore() { // Currently the responses contain duplicate items. // Needs to be fixed on backend, but let's dedupe to be safe. let seen = new Set() + const feedItems: ExploreScreenItems[] = [] for (const page of feeds.pages) { for (const feed of page.feeds) { if (!seen.has(feed.uri)) { seen.add(feed.uri) - i.push({ + feedItems.push({ type: 'feed', key: feed.uri, feed, @@ -403,6 +411,7 @@ export function Explore() { } } + // feeds errors can occur during pagination, so feeds is truthy if (feedsError) { i.push({ type: 'error', @@ -418,13 +427,17 @@ export function Explore() { error: cleanError(preferencesError), }) } else if (hasNextFeedsPage) { + const preview = feedItems.splice(-3) + i.push(...feedItems) i.push({ type: 'loadMore', key: 'loadMoreFeeds', isLoadingMore: isLoadingMoreFeeds, onLoadMore: onLoadMoreFeeds, - items: i.filter(item => item.type === 'feed').slice(-3), + items: preview, }) + } else { + i.push(...feedItems) } } else { if (feedsError) { From 4d6787009ccbae2812aaeddefe6dc77742363f36 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 21 Jun 2024 16:50:23 -0500 Subject: [PATCH 74/81] Pinned feeds cards (#4526) * Add lists support to FeedCard * Add useSavedFeeds query, similar to usePinnedFeedInfos * Integrate into Feeds screen * Fix alignment on mobile * Update usages * Add placeholder loading state * Handle no feeds state * Reuse previous data for placeholder * Staged loading * Improve staged loading * Use setQueryData approach to pre-caching * Add types for a little more safety * Fix precaching --------- Co-authored-by: Dan Abramov --- src/components/FeedCard.tsx | 135 ++++++++-- src/state/queries/feed.ts | 139 ++++++++++ src/state/queries/resolve-uri.ts | 15 +- src/view/screens/Feeds.tsx | 387 +++++++++++++--------------- src/view/screens/Search/Explore.tsx | 2 +- src/view/screens/Search/Search.tsx | 2 +- 6 files changed, 447 insertions(+), 233 deletions(-) diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx index 94d97cb620..bd0649097e 100644 --- a/src/components/FeedCard.tsx +++ b/src/components/FeedCard.tsx @@ -1,6 +1,11 @@ import React from 'react' import {GestureResponderEvent, View} from 'react-native' -import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api' +import { + AppBskyActorDefs, + AppBskyFeedDefs, + AppBskyGraphDefs, + AtUri, +} from '@atproto/api' import {msg, plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -20,23 +25,35 @@ import {Button, ButtonIcon} from '#/components/Button' import {useRichText} from '#/components/hooks/useRichText' import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' -import {Link as InternalLink} from '#/components/Link' +import {Link as InternalLink, LinkProps} from '#/components/Link' import {Loader} from '#/components/Loader' import * as Prompt from '#/components/Prompt' import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' -export function Default({feed}: {feed: AppBskyFeedDefs.GeneratorView}) { +export function Default({ + type, + view, +}: + | { + type: 'feed' + view: AppBskyFeedDefs.GeneratorView + } + | { + type: 'list' + view: AppBskyGraphDefs.ListView + }) { + const displayName = type === 'feed' ? view.displayName : view.name return ( - +
- - - + + +
- - + + {type === 'feed' && }
) @@ -46,13 +63,10 @@ export function Link({ children, feed, }: { - children: React.ReactElement - feed: AppBskyFeedDefs.GeneratorView -}) { + feed: AppBskyFeedDefs.GeneratorView | AppBskyGraphDefs.ListView +} & Omit) { const href = React.useMemo(() => { - const urip = new AtUri(feed.uri) - const handleOrDid = feed.creator.handle || feed.creator.did - return `/profile/${handleOrDid}/feed/${urip.rkey}` + return createProfileFeedHref({feed}) }, [feed]) return {children} } @@ -62,11 +76,33 @@ export function Outer({children}: {children: React.ReactNode}) { } export function Header({children}: {children: React.ReactNode}) { - return {children} + return ( + + {children} + + ) } -export function Avatar({src}: {src: string | undefined}) { - return +export type AvatarProps = {src: string | undefined; size?: number} + +export function Avatar({src, size = 40}: AvatarProps) { + return +} + +export function AvatarPlaceholder({size = 40}: Omit) { + const t = useTheme() + return ( + + ) } export function TitleAndByline({ @@ -74,22 +110,54 @@ export function TitleAndByline({ creator, }: { title: string - creator: AppBskyActorDefs.ProfileViewBasic + creator?: AppBskyActorDefs.ProfileViewBasic }) { const t = useTheme() return ( - + {title} - - Feed by {sanitizeHandle(creator.handle, '@')} - + {creator && ( + + Feed by {sanitizeHandle(creator.handle, '@')} + + )} + + ) +} + +export function TitleAndBylinePlaceholder({creator}: {creator?: boolean}) { + const t = useTheme() + + return ( + + + + {creator && ( + + )} ) } @@ -203,3 +271,16 @@ function ActionInner({uri, pin}: {uri: string; pin?: boolean}) { ) } + +export function createProfileFeedHref({ + feed, +}: { + feed: AppBskyFeedDefs.GeneratorView | AppBskyGraphDefs.ListView +}) { + const urip = new AtUri(feed.uri) + const type = urip.collection === 'app.bsky.feed.generator' ? 'feed' : 'list' + const handleOrDid = feed.creator.handle || feed.creator.did + return `/profile/${handleOrDid}/${type === 'feed' ? 'feed' : 'lists'}/${ + urip.rkey + }` +} diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts index 83d6a7634d..972dbf995a 100644 --- a/src/state/queries/feed.ts +++ b/src/state/queries/feed.ts @@ -9,20 +9,24 @@ import { } from '@atproto/api' import { InfiniteData, + QueryClient, QueryKey, useInfiniteQuery, useMutation, useQuery, + useQueryClient, } from '@tanstack/react-query' import {DISCOVER_FEED_URI, DISCOVER_SAVED_FEED} from '#/lib/constants' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {STALE} from '#/state/queries' +import {RQKEY as listQueryKey} from '#/state/queries/list' import {usePreferencesQuery} from '#/state/queries/preferences' import {useAgent, useSession} from '#/state/session' import {router} from '#/routes' import {FeedDescriptor} from './post-feed' +import {precacheResolvedUri} from './resolve-uri' export type FeedSourceFeedInfo = { type: 'feed' @@ -201,6 +205,7 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) { const agent = useAgent() const limit = options?.limit || 10 const {data: preferences} = usePreferencesQuery() + const queryClient = useQueryClient() // Make sure this doesn't invalidate unless really needed. const selectArgs = useMemo( @@ -225,6 +230,13 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) { limit, cursor: pageParam, }) + + // precache feeds + for (const feed of res.data.feeds) { + const hydratedFeed = hydrateFeedGenerator(feed) + precacheFeed(queryClient, hydratedFeed) + } + return res.data }, initialPageParam: undefined, @@ -449,3 +461,130 @@ export function usePinnedFeedsInfos() { }, }) } + +export type SavedFeedItem = + | { + type: 'feed' + config: AppBskyActorDefs.SavedFeed + view: AppBskyFeedDefs.GeneratorView + } + | { + type: 'list' + config: AppBskyActorDefs.SavedFeed + view: AppBskyGraphDefs.ListView + } + | { + type: 'timeline' + config: AppBskyActorDefs.SavedFeed + view: undefined + } + +export function useSavedFeeds() { + const agent = useAgent() + const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery() + const savedItems = preferences?.savedFeeds ?? [] + const queryClient = useQueryClient() + + return useQuery({ + staleTime: STALE.INFINITY, + enabled: !isLoadingPrefs, + queryKey: [pinnedFeedInfosQueryKeyRoot, ...savedItems], + placeholderData: previousData => { + return ( + previousData || { + count: savedItems.length, + feeds: [], + } + ) + }, + queryFn: async () => { + const resolvedFeeds = new Map() + const resolvedLists = new Map() + + const savedFeeds = savedItems.filter(feed => feed.type === 'feed') + const savedLists = savedItems.filter(feed => feed.type === 'list') + + let feedsPromise = Promise.resolve() + if (savedFeeds.length > 0) { + feedsPromise = agent.app.bsky.feed + .getFeedGenerators({ + feeds: savedFeeds.map(f => f.value), + }) + .then(res => { + res.data.feeds.forEach(f => { + resolvedFeeds.set(f.uri, f) + }) + }) + } + + const listsPromises = savedLists.map(list => + agent.app.bsky.graph + .getList({ + list: list.value, + limit: 1, + }) + .then(res => { + const listView = res.data.list + resolvedLists.set(listView.uri, listView) + }), + ) + + await Promise.allSettled([feedsPromise, ...listsPromises]) + + resolvedFeeds.forEach(feed => { + const hydratedFeed = hydrateFeedGenerator(feed) + precacheFeed(queryClient, hydratedFeed) + }) + resolvedLists.forEach(list => { + precacheList(queryClient, list) + }) + + const res: SavedFeedItem[] = savedItems.map(s => { + if (s.type === 'timeline') { + return { + type: 'timeline', + config: s, + view: undefined, + } + } + + return { + type: s.type, + config: s, + view: + s.type === 'feed' + ? resolvedFeeds.get(s.value) + : resolvedLists.get(s.value), + } + }) as SavedFeedItem[] + + return { + count: savedItems.length, + feeds: res, + } + }, + }) +} + +function precacheFeed(queryClient: QueryClient, hydratedFeed: FeedSourceInfo) { + precacheResolvedUri( + queryClient, + hydratedFeed.creatorHandle, + hydratedFeed.creatorDid, + ) + queryClient.setQueryData( + feedSourceInfoQueryKey({uri: hydratedFeed.uri}), + hydratedFeed, + ) +} + +function precacheList( + queryClient: QueryClient, + list: AppBskyGraphDefs.ListView, +) { + precacheResolvedUri(queryClient, list.creator.handle, list.creator.did) + queryClient.setQueryData( + listQueryKey(list.uri), + list, + ) +} diff --git a/src/state/queries/resolve-uri.ts b/src/state/queries/resolve-uri.ts index 7bd26435cf..c1fd8e240a 100644 --- a/src/state/queries/resolve-uri.ts +++ b/src/state/queries/resolve-uri.ts @@ -1,5 +1,10 @@ import {AppBskyActorDefs, AtUri} from '@atproto/api' -import {useQuery, useQueryClient, UseQueryResult} from '@tanstack/react-query' +import { + QueryClient, + useQuery, + useQueryClient, + UseQueryResult, +} from '@tanstack/react-query' import {STALE} from '#/state/queries' import {useAgent} from '#/state/session' @@ -50,3 +55,11 @@ export function useResolveDidQuery(didOrHandle: string | undefined) { enabled: !!didOrHandle, }) } + +export function precacheResolvedUri( + queryClient: QueryClient, + handle: string, + did: string, +) { + queryClient.setQueryData(RQKEY(handle), did) +} diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx index 1345211775..70437a9e76 100644 --- a/src/view/screens/Feeds.tsx +++ b/src/view/screens/Feeds.tsx @@ -1,8 +1,6 @@ import React from 'react' import {ActivityIndicator, type FlatList, StyleSheet, View} from 'react-native' -import {AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' +import {AppBskyFeedDefs} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useFocusEffect} from '@react-navigation/native' @@ -10,12 +8,11 @@ import debounce from 'lodash.debounce' import {isNative, isWeb} from '#/platform/detection' import { - getAvatarTypeFromUri, - useFeedSourceInfoQuery, + SavedFeedItem, useGetPopularFeedsQuery, + useSavedFeeds, useSearchPopularFeedsMutation, } from '#/state/queries/feed' -import {usePreferencesQuery} from '#/state/queries/preferences' import {useSession} from '#/state/session' import {useSetMinimalShellMode} from '#/state/shell' import {useComposerControls} from '#/state/shell/composer' @@ -28,14 +25,10 @@ import {s} from 'lib/styles' import {ErrorMessage} from 'view/com/util/error/ErrorMessage' import {FAB} from 'view/com/util/fab/FAB' import {SearchInput} from 'view/com/util/forms/SearchInput' -import {Link, TextLink} from 'view/com/util/Link' +import {TextLink} from 'view/com/util/Link' import {List} from 'view/com/util/List' -import { - FeedFeedLoadingPlaceholder, - LoadingPlaceholder, -} from 'view/com/util/LoadingPlaceholder' +import {FeedFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder' import {Text} from 'view/com/util/text/Text' -import {UserAvatar} from 'view/com/util/UserAvatar' import {ViewHeader} from 'view/com/util/ViewHeader' import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed' import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType' @@ -47,6 +40,7 @@ import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkl import hairlineWidth = StyleSheet.hairlineWidth import {Divider} from '#/components/Divider' import * as FeedCard from '#/components/FeedCard' +import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' type Props = NativeStackScreenProps @@ -61,9 +55,8 @@ type FlatlistSlice = key: string } | { - type: 'savedFeedsLoading' + type: 'savedFeedPlaceholder' key: string - // pendingItems: number, } | { type: 'savedFeedNoResults' @@ -72,8 +65,7 @@ type FlatlistSlice = | { type: 'savedFeed' key: string - feedUri: string - savedFeedConfig: AppBskyActorDefs.SavedFeed + savedFeed: SavedFeedItem } | { type: 'savedFeedsLoadMore' @@ -113,11 +105,11 @@ export function FeedsScreen(_props: Props) { const [query, setQuery] = React.useState('') const [isPTR, setIsPTR] = React.useState(false) const { - data: preferences, - isLoading: isPreferencesLoading, - error: preferencesError, - refetch: refetchPreferences, - } = usePreferencesQuery() + data: savedFeeds, + isPlaceholderData: isSavedFeedsPlaceholder, + error: savedFeedsError, + refetch: refetchSavedFeeds, + } = useSavedFeeds() const { data: popularFeeds, isFetching: isPopularFeedsFetching, @@ -173,11 +165,11 @@ export function FeedsScreen(_props: Props) { const onPullToRefresh = React.useCallback(async () => { setIsPTR(true) await Promise.all([ - refetchPreferences().catch(_e => undefined), + refetchSavedFeeds().catch(_e => undefined), refetchPopularFeeds().catch(_e => undefined), ]) setIsPTR(false) - }, [setIsPTR, refetchPreferences, refetchPopularFeeds]) + }, [setIsPTR, refetchSavedFeeds, refetchPopularFeeds]) const onEndReached = React.useCallback(() => { if ( isPopularFeedsFetching || @@ -203,6 +195,11 @@ export function FeedsScreen(_props: Props) { const items = React.useMemo(() => { let slices: FlatlistSlice[] = [] + const hasActualSavedCount = + !isSavedFeedsPlaceholder || + (isSavedFeedsPlaceholder && (savedFeeds?.count || 0) > 0) + const canShowDiscoverSection = + !hasSession || (hasSession && hasActualSavedCount) if (hasSession) { slices.push({ @@ -210,47 +207,63 @@ export function FeedsScreen(_props: Props) { type: 'savedFeedsHeader', }) - if (preferencesError) { + if (savedFeedsError) { slices.push({ key: 'savedFeedsError', type: 'error', - error: cleanError(preferencesError.toString()), + error: cleanError(savedFeedsError.toString()), }) } else { - if (isPreferencesLoading || !preferences?.savedFeeds) { - slices.push({ - key: 'savedFeedsLoading', - type: 'savedFeedsLoading', - // pendingItems: this.rootStore.preferences.savedFeeds.length || 3, - }) + if (isSavedFeedsPlaceholder && !savedFeeds?.feeds.length) { + /* + * Initial render in placeholder state is 0 on a cold page load, + * because preferences haven't loaded yet. + * + * In practice, `savedFeeds` is always defined, but we check for TS + * and for safety. + * + * In both cases, we show 4 as the the loading state. + */ + const min = 8 + const count = savedFeeds + ? savedFeeds.count === 0 + ? min + : savedFeeds.count + : min + Array(count) + .fill(0) + .forEach((_, i) => { + slices.push({ + key: 'savedFeedPlaceholder' + i, + type: 'savedFeedPlaceholder', + }) + }) } else { - if (preferences.savedFeeds?.length) { - const noFollowingFeed = preferences.savedFeeds.every( + if (savedFeeds?.feeds?.length) { + const noFollowingFeed = savedFeeds.feeds.every( f => f.type !== 'timeline', ) slices = slices.concat( - preferences.savedFeeds - .filter(f => { - return f.pinned + savedFeeds.feeds + .filter(s => { + return s.config.pinned }) - .map(feed => ({ - key: `savedFeed:${feed.value}:${feed.id}`, + .map(s => ({ + key: `savedFeed:${s.view?.uri}:${s.config.id}`, type: 'savedFeed', - feedUri: feed.value, - savedFeedConfig: feed, + savedFeed: s, })), ) slices = slices.concat( - preferences.savedFeeds - .filter(f => { - return !f.pinned + savedFeeds.feeds + .filter(s => { + return !s.config.pinned }) - .map(feed => ({ - key: `savedFeed:${feed.value}:${feed.id}`, + .map(s => ({ + key: `savedFeed:${s.view?.uri}:${s.config.id}`, type: 'savedFeed', - feedUri: feed.value, - savedFeedConfig: feed, + savedFeed: s, })), ) @@ -270,59 +283,36 @@ export function FeedsScreen(_props: Props) { } } - slices.push({ - key: 'popularFeedsHeader', - type: 'popularFeedsHeader', - }) - - if (popularFeedsError || searchError) { + if (!hasSession || (hasSession && canShowDiscoverSection)) { slices.push({ - key: 'popularFeedsError', - type: 'error', - error: cleanError( - popularFeedsError?.toString() ?? searchError?.toString() ?? '', - ), + key: 'popularFeedsHeader', + type: 'popularFeedsHeader', }) - } else { - if (isUserSearching) { - if (isSearchPending || !searchResults) { - slices.push({ - key: 'popularFeedsLoading', - type: 'popularFeedsLoading', - }) - } else { - if (!searchResults || searchResults?.length === 0) { - slices.push({ - key: 'popularFeedsNoResults', - type: 'popularFeedsNoResults', - }) - } else { - slices = slices.concat( - searchResults.map(feed => ({ - key: `popularFeed:${feed.uri}`, - type: 'popularFeed', - feedUri: feed.uri, - feed, - })), - ) - } - } + + if (popularFeedsError || searchError) { + slices.push({ + key: 'popularFeedsError', + type: 'error', + error: cleanError( + popularFeedsError?.toString() ?? searchError?.toString() ?? '', + ), + }) } else { - if (isPopularFeedsFetching && !popularFeeds?.pages) { - slices.push({ - key: 'popularFeedsLoading', - type: 'popularFeedsLoading', - }) - } else { - if (!popularFeeds?.pages) { + if (isUserSearching) { + if (isSearchPending || !searchResults) { slices.push({ - key: 'popularFeedsNoResults', - type: 'popularFeedsNoResults', + key: 'popularFeedsLoading', + type: 'popularFeedsLoading', }) } else { - for (const page of popularFeeds.pages || []) { + if (!searchResults || searchResults?.length === 0) { + slices.push({ + key: 'popularFeedsNoResults', + type: 'popularFeedsNoResults', + }) + } else { slices = slices.concat( - page.feeds.map(feed => ({ + searchResults.map(feed => ({ key: `popularFeed:${feed.uri}`, type: 'popularFeed', feedUri: feed.uri, @@ -330,12 +320,37 @@ export function FeedsScreen(_props: Props) { })), ) } - - if (isPopularFeedsFetchingNextPage) { + } + } else { + if (isPopularFeedsFetching && !popularFeeds?.pages) { + slices.push({ + key: 'popularFeedsLoading', + type: 'popularFeedsLoading', + }) + } else { + if (!popularFeeds?.pages) { slices.push({ - key: 'popularFeedsLoadingMore', - type: 'popularFeedsLoadingMore', + key: 'popularFeedsNoResults', + type: 'popularFeedsNoResults', }) + } else { + for (const page of popularFeeds.pages || []) { + slices = slices.concat( + page.feeds.map(feed => ({ + key: `popularFeed:${feed.uri}`, + type: 'popularFeed', + feedUri: feed.uri, + feed, + })), + ) + } + + if (isPopularFeedsFetchingNextPage) { + slices.push({ + key: 'popularFeedsLoadingMore', + type: 'popularFeedsLoadingMore', + }) + } } } } @@ -345,9 +360,9 @@ export function FeedsScreen(_props: Props) { return slices }, [ hasSession, - preferences, - isPreferencesLoading, - preferencesError, + savedFeeds, + isSavedFeedsPlaceholder, + savedFeedsError, popularFeeds, isPopularFeedsFetching, popularFeedsError, @@ -407,10 +422,7 @@ export function FeedsScreen(_props: Props) { ({item}: {item: FlatlistSlice}) => { if (item.type === 'error') { return - } else if ( - item.type === 'popularFeedsLoadingMore' || - item.type === 'savedFeedsLoading' - ) { + } else if (item.type === 'popularFeedsLoadingMore') { return ( @@ -459,8 +471,10 @@ export function FeedsScreen(_props: Props) { ) + } else if (item.type === 'savedFeedPlaceholder') { + return } else if (item.type === 'savedFeed') { - return + return } else if (item.type === 'popularFeedsHeader') { return ( <> @@ -481,7 +495,7 @@ export function FeedsScreen(_props: Props) { } else if (item.type === 'popularFeed') { return ( - + ) @@ -571,136 +585,103 @@ export function FeedsScreen(_props: Props) { ) } -function FeedOrFollowing({ - savedFeedConfig: feed, -}: { - savedFeedConfig: AppBskyActorDefs.SavedFeed -}) { - return feed.type === 'timeline' ? ( +function FeedOrFollowing({savedFeed}: {savedFeed: SavedFeedItem}) { + return savedFeed.type === 'timeline' ? ( ) : ( - + ) } function FollowingFeed() { - const pal = usePalette('default') const t = useTheme() - const {isMobile} = useWebMediaQueries() + const {_} = useLingui() return ( - - + - - - - Following - - + ]}> + + + + ) } function SavedFeed({ - savedFeedConfig: feed, + savedFeed, }: { - savedFeedConfig: AppBskyActorDefs.SavedFeed + savedFeed: SavedFeedItem & {type: 'feed' | 'list'} }) { - const pal = usePalette('default') - const {isMobile} = useWebMediaQueries() - const {data: info, error} = useFeedSourceInfoQuery({uri: feed.value}) - const typeAvatar = getAvatarTypeFromUri(feed.value) - - if (!info) - return ( - - ) + const t = useTheme() + const {view: feed} = savedFeed + const displayName = + savedFeed.type === 'feed' ? savedFeed.view.displayName : savedFeed.view.name return ( - - {error ? ( + + {({hovered, pressed}) => ( - - - ) : ( - - )} - - - {info.displayName} - - {error ? ( - - - Feed offline - - - ) : null} - + style={[ + a.flex_1, + a.px_lg, + a.py_md, + a.border_b, + t.atoms.border_contrast_low, + (hovered || pressed) && t.atoms.bg_contrast_25, + ]}> + + + - {isMobile && ( - + + + )} - + ) } -function SavedFeedLoadingPlaceholder() { - const pal = usePalette('default') - const {isMobile} = useWebMediaQueries() +function SavedFeedPlaceholder() { + const t = useTheme() return ( - - + + + + ) } diff --git a/src/view/screens/Search/Explore.tsx b/src/view/screens/Search/Explore.tsx index f6988548b2..8f6f6d4ba7 100644 --- a/src/view/screens/Search/Explore.tsx +++ b/src/view/screens/Search/Explore.tsx @@ -505,7 +505,7 @@ export function Explore() { a.px_lg, a.py_lg, ]}> - + ) } diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx index 0b1fe37aaf..76ffba935f 100644 --- a/src/view/screens/Search/Search.tsx +++ b/src/view/screens/Search/Search.tsx @@ -306,7 +306,7 @@ let SearchScreenFeedsResults = ({ a.px_lg, a.py_lg, ]}> - + )} keyExtractor={item => item.uri} From 4d9e686e3b77756f8a404c4b38c7016f84a75ea0 Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 21 Jun 2024 15:15:12 -0700 Subject: [PATCH 75/81] add flex shrink (#4597) --- src/components/ProfileHoverCard/index.web.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ProfileHoverCard/index.web.tsx b/src/components/ProfileHoverCard/index.web.tsx index 319eccfa4a..4db9c4f8e5 100644 --- a/src/components/ProfileHoverCard/index.web.tsx +++ b/src/components/ProfileHoverCard/index.web.tsx @@ -64,7 +64,7 @@ export function ProfileHoverCard(props: ProfileHoverCardProps) { return props.children } else { return ( - + ) From 707ea5bf062755d4fd3a9476a4457cdc9d4991b7 Mon Sep 17 00:00:00 2001 From: Minseo Lee Date: Sat, 22 Jun 2024 07:41:58 +0900 Subject: [PATCH 76/81] Add options for Feeds in `Navigation.tsx` (#4503) * Update Navigation.tsx * Update Navigation.tsx --- src/Navigation.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 5d4ba0e3f7..f2b7cd911f 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -312,7 +312,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { getComponent={() => MessagesSettingsScreen} options={{title: title(msg`Chat settings`), requireAuth: true}} /> - FeedsScreen} /> + FeedsScreen} + options={{title: title(msg`Feeds`)}} + /> ) } From 7db8dd8980c71e189315d89289196820db8b7875 Mon Sep 17 00:00:00 2001 From: dan Date: Sat, 22 Jun 2024 02:11:39 +0300 Subject: [PATCH 77/81] Add debug feedContext label (#4598) --- src/lib/statsig/gates.ts | 1 + src/view/com/util/post-ctrls/PostCtrls.tsx | 29 ++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index 6e460dc60f..46ef934ef6 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -1,5 +1,6 @@ export type Gate = // Keep this alphabetic please. + | 'debug_show_feedcontext' | 'native_pwi_disabled' | 'request_notifications_permission_after_onboarding_v2' | 'show_avi_follow_button' diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index 472ce4043a..231808bf28 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -6,6 +6,7 @@ import { View, type ViewStyle, } from 'react-native' +import * as Clipboard from 'expo-clipboard' import { AppBskyFeedDefs, AppBskyFeedPost, @@ -19,6 +20,7 @@ import {POST_CTRL_HITSLOP} from '#/lib/constants' import {useHaptics} from '#/lib/haptics' import {makeProfileLink} from '#/lib/routes/links' import {shareUrl} from '#/lib/sharing' +import {useGate} from '#/lib/statsig/statsig' import {toShareUrl} from '#/lib/strings/url-helpers' import {s} from '#/lib/styles' import {Shadow} from '#/state/cache/types' @@ -41,6 +43,7 @@ import * as Prompt from '#/components/Prompt' import {PostDropdownBtn} from '../forms/PostDropdownBtn' import {formatCount} from '../numeric/format' import {Text} from '../text/Text' +import * as Toast from '../Toast' import {RepostButton} from './RepostButton' let PostCtrls = ({ @@ -75,6 +78,7 @@ let PostCtrls = ({ const loggedOutWarningPromptControl = useDialogControl() const {sendInteraction} = useFeedFeedbackContext() const playHaptic = useHaptics() + const gate = useGate() const shouldShowLoggedOutWarning = React.useMemo(() => { return ( @@ -329,6 +333,31 @@ let PostCtrls = ({ timestamp={post.indexedAt} /> + {gate('debug_show_feedcontext') && feedContext && ( + { + e.stopPropagation() + Clipboard.setStringAsync(feedContext) + Toast.show(_(msg`Copied to clipboard`)) + }}> + + {feedContext} + + + )} ) } From 1715afd80ed7d9de1f2d82befa04815015d34a3a Mon Sep 17 00:00:00 2001 From: dan Date: Sat, 22 Jun 2024 03:54:47 +0300 Subject: [PATCH 78/81] [Statsig] Send Discover aggregate interactions (#4599) --- src/lib/statsig/events.ts | 16 ++++++ src/lib/statsig/statsig.tsx | 3 + src/state/feed-feedback.tsx | 107 +++++++++++++++++++++++++++++++++++- 3 files changed, 125 insertions(+), 1 deletion(-) diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index 0d77ec8a36..2e8cedb54b 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -73,6 +73,22 @@ export type LogEvents = { feedType: string reason: 'pull-to-refresh' | 'soft-reset' | 'load-latest' } + 'discover:showMore': { + feedContext: string + } + 'discover:showLess': { + feedContext: string + } + 'discover:clickthrough:sampled': { + count: number + } + 'discover:engaged:sampled': { + count: number + } + 'discover:seen:sampled': { + count: number + } + 'composer:gif:open': {} 'composer:gif:select': {} diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx index b5a239c3a5..94a1e63d0e 100644 --- a/src/lib/statsig/statsig.tsx +++ b/src/lib/statsig/statsig.tsx @@ -115,6 +115,9 @@ const DOWNSAMPLED_EVENTS: Set = new Set([ 'home:feedDisplayed:sampled', 'feed:endReached:sampled', 'feed:refresh:sampled', + 'discover:clickthrough:sampled', + 'discover:engaged:sampled', + 'discover:seen:sampled', ]) const isDownsampledSession = Math.random() < 0.9 // 90% likely diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index 64bdd4b893..88f50daca4 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -4,6 +4,7 @@ import {AppBskyFeedDefs, BskyAgent} from '@atproto/api' import throttle from 'lodash.throttle' import {PROD_DEFAULT_FEED} from '#/lib/constants' +import {logEvent} from '#/lib/statsig/statsig' import {logger} from '#/logger' import { FeedDescriptor, @@ -34,6 +35,16 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { WeakSet >(new WeakSet()) + const aggregatedStats = React.useRef(null) + const throttledFlushAggregatedStats = React.useMemo( + () => + throttle(() => flushToStatsig(aggregatedStats.current), 45e3, { + leading: true, // The outer call is already throttled somewhat. + trailing: true, + }), + [], + ) + const sendToFeedNoDelay = React.useCallback(() => { const proxyAgent = agent.withProxy( // @ts-ignore TODO need to update withProxy() to support this key -prf @@ -45,12 +56,20 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { const interactions = Array.from(queue.current).map(toInteraction) queue.current.clear() + // Send to the feed proxyAgent.app.bsky.feed .sendInteractions({interactions}) .catch((e: any) => { logger.warn('Failed to send feed interactions', {error: e}) }) - }, [agent]) + + // Send to Statsig + if (aggregatedStats.current === null) { + aggregatedStats.current = createAggregatedStats() + } + sendOrAggregateInteractionsForStats(aggregatedStats.current, interactions) + throttledFlushAggregatedStats() + }, [agent, throttledFlushAggregatedStats]) const sendToFeed = React.useMemo( () => @@ -149,3 +168,89 @@ function toInteraction(str: string): AppBskyFeedDefs.Interaction { const [item, event, feedContext] = str.split('|') return {item, event, feedContext} } + +type AggregatedStats = { + clickthroughCount: number + engagedCount: number + seenCount: number +} + +function createAggregatedStats(): AggregatedStats { + return { + clickthroughCount: 0, + engagedCount: 0, + seenCount: 0, + } +} + +function sendOrAggregateInteractionsForStats( + stats: AggregatedStats, + interactions: AppBskyFeedDefs.Interaction[], +) { + for (let interaction of interactions) { + switch (interaction.event) { + // Pressing "Show more" / "Show less" is relatively uncommon so we won't aggregate them. + // This lets us send the feed context together with them. + case 'app.bsky.feed.defs#requestLess': { + logEvent('discover:showLess', { + feedContext: interaction.feedContext ?? '', + }) + break + } + case 'app.bsky.feed.defs#requestMore': { + logEvent('discover:showMore', { + feedContext: interaction.feedContext ?? '', + }) + break + } + + // The rest of the events are aggregated and sent later in batches. + case 'app.bsky.feed.defs#clickthroughAuthor': + case 'app.bsky.feed.defs#clickthroughEmbed': + case 'app.bsky.feed.defs#clickthroughItem': + case 'app.bsky.feed.defs#clickthroughReposter': { + stats.clickthroughCount++ + break + } + case 'app.bsky.feed.defs#interactionLike': + case 'app.bsky.feed.defs#interactionQuote': + case 'app.bsky.feed.defs#interactionReply': + case 'app.bsky.feed.defs#interactionRepost': + case 'app.bsky.feed.defs#interactionShare': { + stats.engagedCount++ + break + } + case 'app.bsky.feed.defs#interactionSeen': { + stats.seenCount++ + break + } + } + } +} + +function flushToStatsig(stats: AggregatedStats | null) { + if (stats === null) { + return + } + + if (stats.clickthroughCount > 0) { + logEvent('discover:clickthrough:sampled', { + count: stats.clickthroughCount, + }) + stats.clickthroughCount = 0 + } + + if (stats.engagedCount > 0) { + logEvent('discover:engaged:sampled', { + count: stats.engagedCount, + }) + stats.engagedCount = 0 + } + + if (stats.seenCount > 0) { + logEvent('discover:seen:sampled', { + count: stats.seenCount, + }) + stats.seenCount = 0 + } +} From 35f64535cb8dfa0fe46e740a6398f3b991ecfbc7 Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 21 Jun 2024 19:59:08 -0700 Subject: [PATCH 79/81] Tweak feed card to prevent spinnerz when pushing to screen (#4600) --- src/components/FeedCard.tsx | 102 ++++++++++++++++++------ src/state/queries/feed.ts | 10 ++- src/view/com/feeds/ProfileFeedgens.tsx | 105 +++++++++++-------------- src/view/com/lists/ProfileLists.tsx | 32 ++++---- src/view/screens/Feeds.tsx | 9 ++- 5 files changed, 153 insertions(+), 105 deletions(-) diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx index bd0649097e..7f3cb88ff3 100644 --- a/src/components/FeedCard.tsx +++ b/src/components/FeedCard.tsx @@ -8,6 +8,7 @@ import { } from '@atproto/api' import {msg, plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useQueryClient} from '@tanstack/react-query' import {logger} from '#/logger' import { @@ -16,6 +17,7 @@ import { useRemoveFeedMutation, } from '#/state/queries/preferences' import {sanitizeHandle} from 'lib/strings/handles' +import {precacheFeedFromGeneratorView, precacheList} from 'state/queries/feed' import {useSession} from 'state/session' import {UserAvatar} from '#/view/com/util/UserAvatar' import * as Toast from 'view/com/util/Toast' @@ -31,10 +33,7 @@ import * as Prompt from '#/components/Prompt' import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' -export function Default({ - type, - view, -}: +type Props = | { type: 'feed' view: AppBskyFeedDefs.GeneratorView @@ -42,15 +41,24 @@ export function Default({ | { type: 'list' view: AppBskyGraphDefs.ListView - }) { + } + +export function Default(props: Props) { + const {type, view} = props const displayName = type === 'feed' ? view.displayName : view.name + const purpose = type === 'list' ? view.purpose : undefined return ( - +
- - + +
{type === 'feed' && } @@ -60,15 +68,31 @@ export function Default({ } export function Link({ + type, + view, + label, children, - feed, -}: { - feed: AppBskyFeedDefs.GeneratorView | AppBskyGraphDefs.ListView -} & Omit) { +}: Props & Omit) { + const queryClient = useQueryClient() + const href = React.useMemo(() => { - return createProfileFeedHref({feed}) - }, [feed]) - return {children} + return createProfileFeedHref({feed: view}) + }, [view]) + + return ( + { + if (type === 'feed') { + precacheFeedFromGeneratorView(queryClient, view) + } else { + precacheList(queryClient, view) + } + }}> + {children} + + ) } export function Outer({children}: {children: React.ReactNode}) { @@ -108,9 +132,13 @@ export function AvatarPlaceholder({size = 40}: Omit) { export function TitleAndByline({ title, creator, + type, + purpose, }: { title: string creator?: AppBskyActorDefs.ProfileViewBasic + type: 'feed' | 'list' + purpose?: AppBskyGraphDefs.ListView['purpose'] }) { const t = useTheme() @@ -123,7 +151,15 @@ export function TitleAndByline({ - Feed by {sanitizeHandle(creator.handle, '@')} + {type === 'list' && purpose === 'app.bsky.graph.defs#curatelist' ? ( + List by {sanitizeHandle(creator.handle, '@')} + ) : type === 'list' && purpose === 'app.bsky.graph.defs#modlist' ? ( + + Moderation list by {sanitizeHandle(creator.handle, '@')} + + ) : ( + Feed by {sanitizeHandle(creator.handle, '@')} + )} )} @@ -184,13 +220,31 @@ export function Likes({count}: {count: number}) { ) } -export function Action({uri, pin}: {uri: string; pin?: boolean}) { +export function Action({ + uri, + pin, + type, + purpose, +}: { + uri: string + pin?: boolean + type: 'feed' | 'list' + purpose?: AppBskyGraphDefs.ListView['purpose'] +}) { const {hasSession} = useSession() - if (!hasSession) return null - return + if (!hasSession || purpose !== 'app.bsky.graph.defs#curatelist') return null + return } -function ActionInner({uri, pin}: {uri: string; pin?: boolean}) { +function ActionInner({ + uri, + pin, + type, +}: { + uri: string + pin?: boolean + type: 'feed' | 'list' +}) { const {_} = useLingui() const {data: preferences} = usePreferencesQuery() const {isPending: isAddSavedFeedPending, mutateAsync: saveFeeds} = @@ -198,9 +252,7 @@ function ActionInner({uri, pin}: {uri: string; pin?: boolean}) { const {isPending: isRemovePending, mutateAsync: removeFeed} = useRemoveFeedMutation() const savedFeedConfig = React.useMemo(() => { - return preferences?.savedFeeds?.find( - feed => feed.type === 'feed' && feed.value === uri, - ) + return preferences?.savedFeeds?.find(feed => feed.value === uri) }, [preferences?.savedFeeds, uri]) const removePromptControl = Prompt.usePromptControl() const isPending = isAddSavedFeedPending || isRemovePending @@ -216,7 +268,7 @@ function ActionInner({uri, pin}: {uri: string; pin?: boolean}) { } else { await saveFeeds([ { - type: 'feed', + type, value: uri, pinned: pin || false, }, @@ -228,7 +280,7 @@ function ActionInner({uri, pin}: {uri: string; pin?: boolean}) { Toast.show(_(msg`Failed to update feeds`)) } }, - [_, pin, saveFeeds, removeFeed, uri, savedFeedConfig], + [_, pin, saveFeeds, removeFeed, uri, savedFeedConfig, type], ) const onPrompRemoveFeed = React.useCallback( diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts index 972dbf995a..e5d6151775 100644 --- a/src/state/queries/feed.ts +++ b/src/state/queries/feed.ts @@ -578,7 +578,7 @@ function precacheFeed(queryClient: QueryClient, hydratedFeed: FeedSourceInfo) { ) } -function precacheList( +export function precacheList( queryClient: QueryClient, list: AppBskyGraphDefs.ListView, ) { @@ -588,3 +588,11 @@ function precacheList( list, ) } + +export function precacheFeedFromGeneratorView( + queryClient: QueryClient, + view: AppBskyFeedDefs.GeneratorView, +) { + const hydratedFeed = hydrateFeedGenerator(view) + precacheFeed(queryClient, hydratedFeed) +} diff --git a/src/view/com/feeds/ProfileFeedgens.tsx b/src/view/com/feeds/ProfileFeedgens.tsx index 197f35e4d0..ec1a55e22e 100644 --- a/src/view/com/feeds/ProfileFeedgens.tsx +++ b/src/view/com/feeds/ProfileFeedgens.tsx @@ -3,7 +3,6 @@ import { findNodeHandle, ListRenderItemInfo, StyleProp, - StyleSheet, View, ViewStyle, } from 'react-native' @@ -12,18 +11,17 @@ import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' import {cleanError} from '#/lib/strings/errors' -import {useTheme} from '#/lib/ThemeContext' import {logger} from '#/logger' import {isNative, isWeb} from '#/platform/detection' -import {hydrateFeedGenerator} from '#/state/queries/feed' import {usePreferencesQuery} from '#/state/queries/preferences' import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens' import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {EmptyState} from 'view/com/util/EmptyState' +import {atoms as a, useTheme} from '#/alf' +import * as FeedCard from '#/components/FeedCard' import {ErrorMessage} from '../util/error/ErrorMessage' import {List, ListRef} from '../util/List' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' -import {FeedSourceCardLoaded} from './FeedSourceCard' const LOADING = {_reactKey: '__loading__'} const EMPTY = {_reactKey: '__empty__'} @@ -52,7 +50,7 @@ export const ProfileFeedgens = React.forwardRef< ref, ) { const {_} = useLingui() - const theme = useTheme() + const t = useTheme() const [isPTRing, setIsPTRing] = React.useState(false) const opts = React.useMemo(() => ({enabled}), [enabled]) const { @@ -79,10 +77,9 @@ export const ProfileFeedgens = React.forwardRef< items = items.concat([EMPTY]) } else if (data?.pages) { for (const page of data?.pages) { - items = items.concat(page.feeds.map(feed => hydrateFeedGenerator(feed))) + items = items.concat(page.feeds) } - } - if (isError && !isEmpty) { + } else if (isError && !isEmpty) { items = items.concat([LOAD_MORE_ERROR_ITEM]) } return items @@ -132,48 +129,46 @@ export const ProfileFeedgens = React.forwardRef< // rendering // = - const renderItemInner = React.useCallback( - ({item, index}: ListRenderItemInfo) => { - if (item === EMPTY) { - return ( - - ) - } else if (item === ERROR_ITEM) { - return ( - - ) - } else if (item === LOAD_MORE_ERROR_ITEM) { - return ( - - ) - } else if (item === LOADING) { - return - } - if (preferences) { - return ( - - ) - } - return null - }, - [error, refetch, onPressRetryLoadMore, preferences, _], - ) + const renderItem = ({item, index}: ListRenderItemInfo) => { + if (item === EMPTY) { + return ( + + ) + } else if (item === ERROR_ITEM) { + return ( + + ) + } else if (item === LOAD_MORE_ERROR_ITEM) { + return ( + + ) + } else if (item === LOADING) { + return + } + if (preferences) { + return ( + + + + ) + } + return null + } React.useEffect(() => { if (enabled && scrollElRef.current) { @@ -189,12 +184,12 @@ export const ProfileFeedgens = React.forwardRef< ref={scrollElRef} data={items} keyExtractor={(item: any) => item._reactKey || item.uri} - renderItem={renderItemInner} + renderItem={renderItem} refreshing={isPTRing} onRefresh={onRefresh} headerOffset={headerOffset} contentContainerStyle={isNative && {paddingBottom: headerOffset + 100}} - indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} + indicatorStyle={t.name === 'light' ? 'black' : 'white'} removeClippedSubviews={true} // @ts-ignore our .web version only -prf desktopFixedHeight @@ -203,9 +198,3 @@ export const ProfileFeedgens = React.forwardRef< ) }) - -const styles = StyleSheet.create({ - item: { - paddingHorizontal: 18, - }, -}) diff --git a/src/view/com/lists/ProfileLists.tsx b/src/view/com/lists/ProfileLists.tsx index e7fdfe4bd5..62c944efcb 100644 --- a/src/view/com/lists/ProfileLists.tsx +++ b/src/view/com/lists/ProfileLists.tsx @@ -3,7 +3,6 @@ import { findNodeHandle, ListRenderItemInfo, StyleProp, - StyleSheet, View, ViewStyle, } from 'react-native' @@ -12,17 +11,17 @@ import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' import {cleanError} from '#/lib/strings/errors' -import {useTheme} from '#/lib/ThemeContext' import {logger} from '#/logger' import {isNative, isWeb} from '#/platform/detection' import {RQKEY, useProfileListsQuery} from '#/state/queries/profile-lists' import {useAnalytics} from 'lib/analytics/analytics' import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {EmptyState} from 'view/com/util/EmptyState' +import {atoms as a, useTheme} from '#/alf' +import * as FeedCard from '#/components/FeedCard' import {ErrorMessage} from '../util/error/ErrorMessage' import {List, ListRef} from '../util/List' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' -import {ListCard} from './ListCard' const LOADING = {_reactKey: '__loading__'} const EMPTY = {_reactKey: '__empty__'} @@ -48,7 +47,7 @@ export const ProfileLists = React.forwardRef( {did, scrollElRef, headerOffset, enabled, style, testID, setScrollViewTag}, ref, ) { - const theme = useTheme() + const t = useTheme() const {track} = useAnalytics() const {_} = useLingui() const [isPTRing, setIsPTRing] = React.useState(false) @@ -166,15 +165,18 @@ export const ProfileLists = React.forwardRef( return } return ( - + + + ) }, - [error, refetch, onPressRetryLoadMore, _], + [error, refetch, onPressRetryLoadMore, _, t.atoms.border_contrast_low], ) React.useEffect(() => { @@ -198,7 +200,7 @@ export const ProfileLists = React.forwardRef( contentContainerStyle={ isNative && {paddingBottom: headerOffset + 100} } - indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} + indicatorStyle={t.name === 'light' ? 'black' : 'white'} removeClippedSubviews={true} // @ts-ignore our .web version only -prf desktopFixedHeight @@ -208,9 +210,3 @@ export const ProfileLists = React.forwardRef( ) }, ) - -const styles = StyleSheet.create({ - item: { - paddingHorizontal: 18, - }, -}) diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx index 70437a9e76..2e5b485136 100644 --- a/src/view/screens/Feeds.tsx +++ b/src/view/screens/Feeds.tsx @@ -627,7 +627,7 @@ function FollowingFeed() { fill={t.palette.white} /> - + ) @@ -644,7 +644,7 @@ function SavedFeed({ savedFeed.type === 'feed' ? savedFeed.view.displayName : savedFeed.view.name return ( - + {({hovered, pressed}) => ( - + From f089f4578131e83cd177b7809ce0f7b75779dfdc Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 21 Jun 2024 21:38:04 -0700 Subject: [PATCH 80/81] Starter Packs (#4332) Co-authored-by: Dan Abramov Co-authored-by: Paul Frazee Co-authored-by: Eric Bailey Co-authored-by: Samuel Newman --- __tests__/lib/string.test.ts | 181 +++++ app.config.js | 18 +- .../icons/qrCode_stroke2_corner0_rounded.svg | 1 + assets/icons/starterPack.svg | 1 + assets/icons/starter_pack_icon.svg | 1 + assets/logo.png | Bin 0 -> 10126 bytes bskyweb/cmd/bskyweb/server.go | 4 + .../.well-known/apple-app-site-association | 6 +- modules/BlueskyClip/AppDelegate.swift | 32 + .../App-Icon-1024x1024@1x.png | Bin 0 -> 473960 bytes .../AppIcon.appiconset/Contents.json | 14 + .../BlueskyClip/Images.xcassets/Contents.json | 6 + modules/BlueskyClip/ViewController.swift | 133 ++++ .../android/build.gradle | 47 ++ .../android/src/main/AndroidManifest.xml | 2 + .../ExpoBlueskyDevicePrefsModule.kt | 10 + .../referrer/ExpoBlueskyReferrerModule.kt | 54 ++ .../expo-module.config.json | 12 + modules/expo-bluesky-swiss-army/index.ts | 4 + .../ExpoBlueskyDevicePrefsModule.swift | 23 + .../ios/ExpoBlueskySwissArmy.podspec | 21 + .../Referrer/ExpoBlueskyReferrerModule.swift | 7 + .../src/DevicePrefs/index.ios.ts | 18 + .../src/DevicePrefs/index.ts | 16 + .../src/NotImplemented.ts | 16 + .../src/Referrer/index.android.ts | 9 + .../src/Referrer/index.ts | 7 + .../src/Referrer/types.ts | 7 + package.json | 5 +- .../withAppEntitlements.js | 16 + .../withClipEntitlements.js | 32 + .../withClipInfoPlist.js | 38 ++ .../starterPackAppClipExtension/withFiles.js | 40 ++ .../withStarterPackAppClip.js | 40 ++ .../withXcodeTarget.js | 91 +++ scripts/updateExtensions.sh | 9 + src/App.native.tsx | 9 +- src/App.web.tsx | 9 +- src/Navigation.tsx | 23 + src/components/LinearGradientBackground.tsx | 23 + src/components/NewskieDialog.tsx | 70 +- src/components/ProfileCard.tsx | 91 +++ .../ReportDialog/SelectReportOptionView.tsx | 3 + src/components/ReportDialog/types.ts | 2 +- src/components/StarterPack/Main/FeedsList.tsx | 68 ++ .../StarterPack/Main/ProfilesList.tsx | 119 ++++ .../StarterPack/ProfileStarterPacks.tsx | 320 +++++++++ src/components/StarterPack/QrCode.tsx | 119 ++++ src/components/StarterPack/QrCodeDialog.tsx | 201 ++++++ src/components/StarterPack/ShareDialog.tsx | 180 +++++ .../StarterPack/StarterPackCard.tsx | 117 ++++ .../StarterPack/Wizard/ScreenTransition.tsx | 31 + .../Wizard/WizardEditListDialog.tsx | 152 +++++ .../StarterPack/Wizard/WizardListCard.tsx | 182 +++++ src/components/forms/TextField.tsx | 2 + .../hooks/useStarterPackEntry.native.ts | 68 ++ src/components/hooks/useStarterPackEntry.ts | 29 + src/components/icons/QrCode.tsx | 5 + src/components/icons/StarterPack.tsx | 8 + src/components/icons/TEMPLATE.tsx | 31 +- src/components/icons/common.ts | 32 - src/components/icons/common.tsx | 59 ++ src/lib/browser.native.ts | 1 + src/lib/browser.ts | 2 + src/lib/generate-starterpack.ts | 164 +++++ src/lib/hooks/useBottomBarOffset.ts | 14 + src/lib/hooks/useNotificationHandler.ts | 2 + .../create-sanitized-display-name.ts | 21 + src/lib/moderation/useReportOptions.ts | 9 + src/lib/routes/links.ts | 17 + src/lib/routes/types.ts | 12 + src/lib/statsig/events.ts | 35 +- src/lib/statsig/gates.ts | 1 + src/lib/strings/starter-pack.ts | 101 +++ src/routes.ts | 4 + src/screens/Login/LoginForm.tsx | 3 + src/screens/Login/ScreenTransition.tsx | 11 +- src/screens/Onboarding/StepFinished.tsx | 117 +++- src/screens/Profile/Header/DisplayName.tsx | 6 +- src/screens/Signup/index.tsx | 40 +- .../StarterPack/StarterPackLandingScreen.tsx | 378 +++++++++++ src/screens/StarterPack/StarterPackScreen.tsx | 627 ++++++++++++++++++ src/screens/StarterPack/Wizard/State.tsx | 163 +++++ .../StarterPack/Wizard/StepDetails.tsx | 84 +++ src/screens/StarterPack/Wizard/StepFeeds.tsx | 113 ++++ .../StarterPack/Wizard/StepFinished.tsx | 0 .../StarterPack/Wizard/StepProfiles.tsx | 101 +++ src/screens/StarterPack/Wizard/index.tsx | 575 ++++++++++++++++ src/state/persisted/schema.ts | 2 + src/state/preferences/index.tsx | 5 +- src/state/preferences/used-starter-packs.tsx | 37 ++ src/state/queries/actor-search.ts | 46 +- src/state/queries/actor-starter-packs.ts | 47 ++ src/state/queries/feed.ts | 17 + src/state/queries/list-members.ts | 19 +- src/state/queries/notifications/feed.ts | 11 +- src/state/queries/notifications/types.ts | 49 +- src/state/queries/notifications/util.ts | 83 ++- src/state/queries/profile-lists.ts | 10 +- src/state/queries/shorten-link.ts | 23 + src/state/queries/starter-packs.ts | 317 +++++++++ src/state/session/agent.ts | 12 - src/state/shell/logged-out.tsx | 17 +- src/state/shell/starter-pack.tsx | 25 + src/view/com/auth/LoggedOut.tsx | 42 +- src/view/com/feeds/FeedSourceCard.tsx | 3 + src/view/com/notifications/FeedItem.tsx | 87 ++- src/view/com/profile/FollowButton.tsx | 11 +- src/view/com/profile/ProfileCard.tsx | 6 +- src/view/com/profile/ProfileSubpageHeader.tsx | 10 +- src/view/screens/Home.tsx | 2 +- src/view/screens/Profile.tsx | 115 ++-- src/view/screens/Storybook/Icons.tsx | 8 + src/view/shell/desktop/LeftNav.tsx | 8 +- yarn.lock | 86 ++- 115 files changed, 6336 insertions(+), 237 deletions(-) create mode 100644 assets/icons/qrCode_stroke2_corner0_rounded.svg create mode 100644 assets/icons/starterPack.svg create mode 100644 assets/icons/starter_pack_icon.svg create mode 100644 assets/logo.png create mode 100644 modules/BlueskyClip/AppDelegate.swift create mode 100644 modules/BlueskyClip/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png create mode 100644 modules/BlueskyClip/Images.xcassets/AppIcon.appiconset/Contents.json create mode 100644 modules/BlueskyClip/Images.xcassets/Contents.json create mode 100644 modules/BlueskyClip/ViewController.swift create mode 100644 modules/expo-bluesky-swiss-army/android/build.gradle create mode 100644 modules/expo-bluesky-swiss-army/android/src/main/AndroidManifest.xml create mode 100644 modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/deviceprefs/ExpoBlueskyDevicePrefsModule.kt create mode 100644 modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt create mode 100644 modules/expo-bluesky-swiss-army/expo-module.config.json create mode 100644 modules/expo-bluesky-swiss-army/index.ts create mode 100644 modules/expo-bluesky-swiss-army/ios/DevicePrefs/ExpoBlueskyDevicePrefsModule.swift create mode 100644 modules/expo-bluesky-swiss-army/ios/ExpoBlueskySwissArmy.podspec create mode 100644 modules/expo-bluesky-swiss-army/ios/Referrer/ExpoBlueskyReferrerModule.swift create mode 100644 modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ios.ts create mode 100644 modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ts create mode 100644 modules/expo-bluesky-swiss-army/src/NotImplemented.ts create mode 100644 modules/expo-bluesky-swiss-army/src/Referrer/index.android.ts create mode 100644 modules/expo-bluesky-swiss-army/src/Referrer/index.ts create mode 100644 modules/expo-bluesky-swiss-army/src/Referrer/types.ts create mode 100644 plugins/starterPackAppClipExtension/withAppEntitlements.js create mode 100644 plugins/starterPackAppClipExtension/withClipEntitlements.js create mode 100644 plugins/starterPackAppClipExtension/withClipInfoPlist.js create mode 100644 plugins/starterPackAppClipExtension/withFiles.js create mode 100644 plugins/starterPackAppClipExtension/withStarterPackAppClip.js create mode 100644 plugins/starterPackAppClipExtension/withXcodeTarget.js create mode 100644 src/components/LinearGradientBackground.tsx create mode 100644 src/components/ProfileCard.tsx create mode 100644 src/components/StarterPack/Main/FeedsList.tsx create mode 100644 src/components/StarterPack/Main/ProfilesList.tsx create mode 100644 src/components/StarterPack/ProfileStarterPacks.tsx create mode 100644 src/components/StarterPack/QrCode.tsx create mode 100644 src/components/StarterPack/QrCodeDialog.tsx create mode 100644 src/components/StarterPack/ShareDialog.tsx create mode 100644 src/components/StarterPack/StarterPackCard.tsx create mode 100644 src/components/StarterPack/Wizard/ScreenTransition.tsx create mode 100644 src/components/StarterPack/Wizard/WizardEditListDialog.tsx create mode 100644 src/components/StarterPack/Wizard/WizardListCard.tsx create mode 100644 src/components/hooks/useStarterPackEntry.native.ts create mode 100644 src/components/hooks/useStarterPackEntry.ts create mode 100644 src/components/icons/QrCode.tsx create mode 100644 src/components/icons/StarterPack.tsx delete mode 100644 src/components/icons/common.ts create mode 100644 src/components/icons/common.tsx create mode 100644 src/lib/generate-starterpack.ts create mode 100644 src/lib/hooks/useBottomBarOffset.ts create mode 100644 src/lib/moderation/create-sanitized-display-name.ts create mode 100644 src/lib/strings/starter-pack.ts create mode 100644 src/screens/StarterPack/StarterPackLandingScreen.tsx create mode 100644 src/screens/StarterPack/StarterPackScreen.tsx create mode 100644 src/screens/StarterPack/Wizard/State.tsx create mode 100644 src/screens/StarterPack/Wizard/StepDetails.tsx create mode 100644 src/screens/StarterPack/Wizard/StepFeeds.tsx create mode 100644 src/screens/StarterPack/Wizard/StepFinished.tsx create mode 100644 src/screens/StarterPack/Wizard/StepProfiles.tsx create mode 100644 src/screens/StarterPack/Wizard/index.tsx create mode 100644 src/state/preferences/used-starter-packs.tsx create mode 100644 src/state/queries/actor-starter-packs.ts create mode 100644 src/state/queries/shorten-link.ts create mode 100644 src/state/queries/starter-packs.ts create mode 100644 src/state/shell/starter-pack.tsx diff --git a/__tests__/lib/string.test.ts b/__tests__/lib/string.test.ts index 30072ccb1d..0da9551e30 100644 --- a/__tests__/lib/string.test.ts +++ b/__tests__/lib/string.test.ts @@ -1,6 +1,11 @@ import {RichText} from '@atproto/api' import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player' +import { + createStarterPackGooglePlayUri, + createStarterPackLinkFromAndroidReferrer, + parseStarterPackUri, +} from 'lib/strings/starter-pack' import {cleanError} from '../../src/lib/strings/errors' import {createFullHandle, makeValidHandle} from '../../src/lib/strings/handles' import {enforceLen} from '../../src/lib/strings/helpers' @@ -796,3 +801,179 @@ describe('parseEmbedPlayerFromUrl', () => { } }) }) + +describe('createStarterPackLinkFromAndroidReferrer', () => { + const validOutput = 'at://haileyok.com/app.bsky.graph.starterpack/rkey' + + it('returns a link when input contains utm_source and utm_content', () => { + expect( + createStarterPackLinkFromAndroidReferrer( + 'utm_source=bluesky&utm_content=starterpack_haileyok.com_rkey', + ), + ).toEqual(validOutput) + + expect( + createStarterPackLinkFromAndroidReferrer( + 'utm_source=bluesky&utm_content=starterpack_test-lover-9000.com_rkey', + ), + ).toEqual('at://test-lover-9000.com/app.bsky.graph.starterpack/rkey') + }) + + it('returns a link when input contains utm_source and utm_content in different order', () => { + expect( + createStarterPackLinkFromAndroidReferrer( + 'utm_content=starterpack_haileyok.com_rkey&utm_source=bluesky', + ), + ).toEqual(validOutput) + }) + + it('returns a link when input contains other parameters as well', () => { + expect( + createStarterPackLinkFromAndroidReferrer( + 'utm_source=bluesky&utm_medium=starterpack&utm_content=starterpack_haileyok.com_rkey', + ), + ).toEqual(validOutput) + }) + + it('returns null when utm_source is not present', () => { + expect( + createStarterPackLinkFromAndroidReferrer( + 'utm_content=starterpack_haileyok.com_rkey', + ), + ).toEqual(null) + }) + + it('returns null when utm_content is not present', () => { + expect( + createStarterPackLinkFromAndroidReferrer('utm_source=bluesky'), + ).toEqual(null) + }) + + it('returns null when utm_content is malformed', () => { + expect( + createStarterPackLinkFromAndroidReferrer( + 'utm_content=starterpack_haileyok.com', + ), + ).toEqual(null) + + expect( + createStarterPackLinkFromAndroidReferrer('utm_content=starterpack'), + ).toEqual(null) + + expect( + createStarterPackLinkFromAndroidReferrer( + 'utm_content=starterpack_haileyok.com_rkey_more', + ), + ).toEqual(null) + + expect( + createStarterPackLinkFromAndroidReferrer( + 'utm_content=notastarterpack_haileyok.com_rkey', + ), + ).toEqual(null) + }) +}) + +describe('parseStarterPackHttpUri', () => { + const baseUri = 'https://bsky.app/start' + + it('returns a valid at uri when http uri is valid', () => { + const validHttpUri = `${baseUri}/haileyok.com/rkey` + expect(parseStarterPackUri(validHttpUri)).toEqual({ + name: 'haileyok.com', + rkey: 'rkey', + }) + + const validHttpUri2 = `${baseUri}/haileyok.com/ilovetesting` + expect(parseStarterPackUri(validHttpUri2)).toEqual({ + name: 'haileyok.com', + rkey: 'ilovetesting', + }) + + const validHttpUri3 = `${baseUri}/testlover9000.com/rkey` + expect(parseStarterPackUri(validHttpUri3)).toEqual({ + name: 'testlover9000.com', + rkey: 'rkey', + }) + }) + + it('returns null when there is no rkey', () => { + const validHttpUri = `${baseUri}/haileyok.com` + expect(parseStarterPackUri(validHttpUri)).toEqual(null) + }) + + it('returns null when there is an extra path', () => { + const validHttpUri = `${baseUri}/haileyok.com/rkey/other` + expect(parseStarterPackUri(validHttpUri)).toEqual(null) + }) + + it('returns null when there is no handle or rkey', () => { + const validHttpUri = `${baseUri}` + expect(parseStarterPackUri(validHttpUri)).toEqual(null) + }) + + it('returns null when the route is not /start or /starter-pack', () => { + const validHttpUri = 'https://bsky.app/start/haileyok.com/rkey' + expect(parseStarterPackUri(validHttpUri)).toEqual({ + name: 'haileyok.com', + rkey: 'rkey', + }) + + const validHttpUri2 = 'https://bsky.app/starter-pack/haileyok.com/rkey' + expect(parseStarterPackUri(validHttpUri2)).toEqual({ + name: 'haileyok.com', + rkey: 'rkey', + }) + + const invalidHttpUri = 'https://bsky.app/profile/haileyok.com/rkey' + expect(parseStarterPackUri(invalidHttpUri)).toEqual(null) + }) + + it('returns the at uri when the input is a valid starterpack at uri', () => { + const validAtUri = 'at://did:123/app.bsky.graph.starterpack/rkey' + expect(parseStarterPackUri(validAtUri)).toEqual({ + name: 'did:123', + rkey: 'rkey', + }) + }) + + it('returns null when the at uri has no rkey', () => { + const validAtUri = 'at://did:123/app.bsky.graph.starterpack' + expect(parseStarterPackUri(validAtUri)).toEqual(null) + }) + + it('returns null when the collection is not app.bsky.graph.starterpack', () => { + const validAtUri = 'at://did:123/app.bsky.graph.list/rkey' + expect(parseStarterPackUri(validAtUri)).toEqual(null) + }) + + it('returns null when the input is undefined', () => { + expect(parseStarterPackUri(undefined)).toEqual(null) + }) +}) + +describe('createStarterPackGooglePlayUri', () => { + const base = + 'https://play.google.com/store/apps/details?id=xyz.blueskyweb.app&referrer=utm_source%3Dbluesky%26utm_medium%3Dstarterpack%26utm_content%3Dstarterpack_' + + it('returns valid google play uri when input is valid', () => { + expect(createStarterPackGooglePlayUri('name', 'rkey')).toEqual( + `${base}name_rkey`, + ) + }) + + it('returns null when no rkey is supplied', () => { + // @ts-expect-error test + expect(createStarterPackGooglePlayUri('name', undefined)).toEqual(null) + }) + + it('returns null when no name or rkey are supplied', () => { + // @ts-expect-error test + expect(createStarterPackGooglePlayUri(undefined, undefined)).toEqual(null) + }) + + it('returns null when rkey is supplied but no name', () => { + // @ts-expect-error test + expect(createStarterPackGooglePlayUri(undefined, 'rkey')).toEqual(null) + }) +}) diff --git a/app.config.js b/app.config.js index eafacc6cc1..57d4305865 100644 --- a/app.config.js +++ b/app.config.js @@ -39,6 +39,17 @@ module.exports = function (config) { const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight' const IS_PRODUCTION = process.env.EXPO_PUBLIC_ENV === 'production' + const ASSOCIATED_DOMAINS = [ + 'applinks:bsky.app', + 'applinks:staging.bsky.app', + 'appclips:bsky.app', + 'appclips:go.bsky.app', // Allows App Clip to work when scanning QR codes + // When testing local services, enter an ngrok (et al) domain here. It must use a standard HTTP/HTTPS port. + ...(IS_DEV || IS_TESTFLIGHT + ? ['appclips:sptesting.haileyok.com', 'applinks:sptesting.haileyok.com'] + : []), + ] + const UPDATES_CHANNEL = IS_TESTFLIGHT ? 'testflight' : IS_PRODUCTION @@ -83,7 +94,7 @@ module.exports = function (config) { NSPhotoLibraryUsageDescription: 'Used for profile pictures, posts, and other kinds of content', }, - associatedDomains: ['applinks:bsky.app', 'applinks:staging.bsky.app'], + associatedDomains: ASSOCIATED_DOMAINS, splash: { ...SPLASH_CONFIG, dark: DARK_SPLASH_CONFIG, @@ -202,6 +213,7 @@ module.exports = function (config) { sounds: PLATFORM === 'ios' ? ['assets/dm.aiff'] : ['assets/dm.mp3'], }, ], + './plugins/starterPackAppClipExtension/withStarterPackAppClip.js', './plugins/withAndroidManifestPlugin.js', './plugins/withAndroidManifestFCMIconPlugin.js', './plugins/withAndroidStylesWindowBackgroundPlugin.js', @@ -234,6 +246,10 @@ module.exports = function (config) { ], }, }, + { + targetName: 'BlueskyClip', + bundleIdentifier: 'xyz.blueskyweb.app.AppClip', + }, ], }, }, diff --git a/assets/icons/qrCode_stroke2_corner0_rounded.svg b/assets/icons/qrCode_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..b17db39533 --- /dev/null +++ b/assets/icons/qrCode_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/starterPack.svg b/assets/icons/starterPack.svg new file mode 100644 index 0000000000..7f0df55952 --- /dev/null +++ b/assets/icons/starterPack.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/starter_pack_icon.svg b/assets/icons/starter_pack_icon.svg new file mode 100644 index 0000000000..47a2f49b64 --- /dev/null +++ b/assets/icons/starter_pack_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..cc2d660341e380d1150b1f86735ac78d8394e87f GIT binary patch literal 10126 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 U`^y*j|JebmN}7rl^42f^4?+A_Y5)KL literal 0 HcmV?d00001 diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 6d32e0e212..96fb07ddfe 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -223,6 +223,10 @@ func serve(cctx *cli.Context) error { e.GET("/profile/:handleOrDID/post/:rkey/liked-by", server.WebGeneric) e.GET("/profile/:handleOrDID/post/:rkey/reposted-by", server.WebGeneric) + // starter packs + e.GET("/starter-pack/:handleOrDID/:rkey", server.WebGeneric) + e.GET("/start/:handleOrDID/:rkey", server.WebGeneric) + if linkHost != "" { linkUrl, err := url.Parse(linkHost) if err != nil { diff --git a/bskyweb/static/.well-known/apple-app-site-association b/bskyweb/static/.well-known/apple-app-site-association index 232acdf255..0a05fa35f4 100644 --- a/bskyweb/static/.well-known/apple-app-site-association +++ b/bskyweb/static/.well-known/apple-app-site-association @@ -1,6 +1,8 @@ { "applinks": { - "apps": [], + "appclips": { + "apps": ["B3LX46C5HS.xyz.blueskyweb.app.AppClip"] + }, "details": [ { "appID": "B3LX46C5HS.xyz.blueskyweb.app", @@ -10,4 +12,4 @@ } ] } -} \ No newline at end of file +} diff --git a/modules/BlueskyClip/AppDelegate.swift b/modules/BlueskyClip/AppDelegate.swift new file mode 100644 index 0000000000..684194953c --- /dev/null +++ b/modules/BlueskyClip/AppDelegate.swift @@ -0,0 +1,32 @@ +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + var controller: ViewController? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + let window = UIWindow() + self.window = UIWindow() + + let controller = ViewController(window: window) + self.controller = controller + + window.rootViewController = self.controller + window.makeKeyAndVisible() + + return true + } + + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + self.controller?.handleURL(url: url) + return true + } + + func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { + if let incomingURL = userActivity.webpageURL { + self.controller?.handleURL(url: incomingURL) + } + return true + } +} diff --git a/modules/BlueskyClip/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png b/modules/BlueskyClip/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..75ce4b813acb8a0fa79a8c766c0c5c8d3afd9457 GIT binary patch literal 473960 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

H~IxaQ>!ovzE5c(jLI`u&*6mCyM@@HSA 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 z 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%0A=7Pd}_05 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 F_8+!`Bmn>b literal 0 HcmV?d00001 diff --git a/modules/BlueskyClip/Images.xcassets/AppIcon.appiconset/Contents.json b/modules/BlueskyClip/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..c3bb428dbd --- /dev/null +++ b/modules/BlueskyClip/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "App-Icon-1024x1024@1x.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/modules/BlueskyClip/Images.xcassets/Contents.json b/modules/BlueskyClip/Images.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/modules/BlueskyClip/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/modules/BlueskyClip/ViewController.swift b/modules/BlueskyClip/ViewController.swift new file mode 100644 index 0000000000..b178644b8f --- /dev/null +++ b/modules/BlueskyClip/ViewController.swift @@ -0,0 +1,133 @@ +import UIKit +import WebKit +import StoreKit + +class ViewController: UIViewController, WKScriptMessageHandler, WKNavigationDelegate { + let defaults = UserDefaults(suiteName: "group.app.bsky") + + var window: UIWindow + var webView: WKWebView? + + var prevUrl: URL? + var starterPackUrl: URL? + + init(window: UIWindow) { + self.window = window + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + let contentController = WKUserContentController() + contentController.add(self, name: "onMessage") + let configuration = WKWebViewConfiguration() + configuration.userContentController = contentController + + let webView = WKWebView(frame: self.view.bounds, configuration: configuration) + webView.translatesAutoresizingMaskIntoConstraints = false + webView.contentMode = .scaleToFill + webView.navigationDelegate = self + self.view.addSubview(webView) + self.webView = webView + } + + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + guard let response = message.body as? String, + let data = response.data(using: .utf8), + let payload = try? JSONDecoder().decode(WebViewActionPayload.self, from: data) else { + return + } + + switch payload.action { + case .present: + guard let url = self.starterPackUrl else { + return + } + + self.presentAppStoreOverlay() + defaults?.setValue(url.absoluteString, forKey: "starterPackUri") + + case .store: + guard let keyToStoreAs = payload.keyToStoreAs, let jsonToStore = payload.jsonToStore else { + return + } + + self.defaults?.setValue(jsonToStore, forKey: keyToStoreAs) + } + } + + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy { + // Detect when we land on the right URL. This is incase of a short link opening the app clip + guard let url = navigationAction.request.url else { + return .allow + } + + // Store the previous one to compare later, but only set starterPackUrl when we find the right one + prevUrl = url + // pathComponents starts with "/" as the first component, then each path name. so... + // ["/", "start", "name", "rkey"] + if url.pathComponents.count == 4, + url.pathComponents[1] == "start" { + self.starterPackUrl = url + } + + return .allow + } + + func handleURL(url: URL) { + let urlString = "\(url.absoluteString)?clip=true" + if let url = URL(string: urlString) { + self.webView?.load(URLRequest(url: url)) + } + } + + func presentAppStoreOverlay() { + guard let windowScene = self.window.windowScene else { + return + } + + let configuration = SKOverlay.AppClipConfiguration(position: .bottomRaised) + let overlay = SKOverlay(configuration: configuration) + + overlay.present(in: windowScene) + } + + func getHost(_ url: URL?) -> String? { + if #available(iOS 16.0, *) { + return url?.host() + } else { + return url?.host + } + } + + func getQuery(_ url: URL?) -> String? { + if #available(iOS 16.0, *) { + return url?.query() + } else { + return url?.query + } + } + + func urlMatchesPrevious(_ url: URL?) -> Bool { + if #available(iOS 16.0, *) { + return url?.query() == prevUrl?.query() && url?.host() == prevUrl?.host() && url?.query() == prevUrl?.query() + } else { + return url?.query == prevUrl?.query && url?.host == prevUrl?.host && url?.query == prevUrl?.query + } + } +} + +struct WebViewActionPayload: Decodable { + enum Action: String, Decodable { + case present, store + } + + let action: Action + let keyToStoreAs: String? + let jsonToStore: String? +} diff --git a/modules/expo-bluesky-swiss-army/android/build.gradle b/modules/expo-bluesky-swiss-army/android/build.gradle new file mode 100644 index 0000000000..b031cde57e --- /dev/null +++ b/modules/expo-bluesky-swiss-army/android/build.gradle @@ -0,0 +1,47 @@ +apply plugin: 'com.android.library' + +group = 'expo.modules.blueskyswissarmy' +version = '0.6.0' + +def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle") +apply from: expoModulesCorePlugin +applyKotlinExpoModulesCorePlugin() +useCoreDependencies() +useExpoPublishing() + +// If you want to use the managed Android SDK versions from expo-modules-core, set this to true. +// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code. +// Most of the time, you may like to manage the Android SDK versions yourself. +def useManagedAndroidSdkVersions = false +if (useManagedAndroidSdkVersions) { + useDefaultAndroidSdkVersions() +} else { + buildscript { + // Simple helper that allows the root project to override versions declared by this library. + ext.safeExtGet = { prop, fallback -> + rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback + } + } + project.android { + compileSdkVersion safeExtGet("compileSdkVersion", 34) + defaultConfig { + minSdkVersion safeExtGet("minSdkVersion", 21) + targetSdkVersion safeExtGet("targetSdkVersion", 34) + } + } +} + +android { + namespace "expo.modules.blueskyswissarmy" + defaultConfig { + versionCode 1 + versionName "0.6.0" + } + lintOptions { + abortOnError false + } +} + +dependencies { + implementation("com.android.installreferrer:installreferrer:2.2") +} diff --git a/modules/expo-bluesky-swiss-army/android/src/main/AndroidManifest.xml b/modules/expo-bluesky-swiss-army/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..bdae66c8f5 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/android/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/deviceprefs/ExpoBlueskyDevicePrefsModule.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/deviceprefs/ExpoBlueskyDevicePrefsModule.kt new file mode 100644 index 0000000000..29017f17aa --- /dev/null +++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/deviceprefs/ExpoBlueskyDevicePrefsModule.kt @@ -0,0 +1,10 @@ +package expo.modules.blueskyswissarmy.deviceprefs + +import expo.modules.kotlin.modules.Module +import expo.modules.kotlin.modules.ModuleDefinition + +class ExpoBlueskyDevicePrefsModule : Module() { + override fun definition() = ModuleDefinition { + Name("ExpoBlueskyDevicePrefs") + } +} diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt new file mode 100644 index 0000000000..3589b364e0 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt @@ -0,0 +1,54 @@ +package expo.modules.blueskyswissarmy.referrer + +import android.util.Log +import com.android.installreferrer.api.InstallReferrerClient +import com.android.installreferrer.api.InstallReferrerStateListener +import expo.modules.kotlin.modules.Module +import expo.modules.kotlin.modules.ModuleDefinition +import expo.modules.kotlin.Promise + +class ExpoBlueskyReferrerModule : Module() { + override fun definition() = ModuleDefinition { + Name("ExpoBlueskyReferrer") + + AsyncFunction("getGooglePlayReferrerInfoAsync") { promise: Promise -> + val referrerClient = InstallReferrerClient.newBuilder(appContext.reactContext).build() + referrerClient.startConnection(object : InstallReferrerStateListener { + override fun onInstallReferrerSetupFinished(responseCode: Int) { + if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) { + Log.d("ExpoGooglePlayReferrer", "Successfully retrieved referrer info.") + + val response = referrerClient.installReferrer + Log.d("ExpoGooglePlayReferrer", "Install referrer: ${response.installReferrer}") + + promise.resolve( + mapOf( + "installReferrer" to response.installReferrer, + "clickTimestamp" to response.referrerClickTimestampSeconds, + "installTimestamp" to response.installBeginTimestampSeconds + ) + ) + } else { + Log.d("ExpoGooglePlayReferrer", "Failed to get referrer info. Unknown error.") + promise.reject( + "ERR_GOOGLE_PLAY_REFERRER_UNKNOWN", + "Failed to get referrer info", + Exception("Failed to get referrer info") + ) + } + referrerClient.endConnection() + } + + override fun onInstallReferrerServiceDisconnected() { + Log.d("ExpoGooglePlayReferrer", "Failed to get referrer info. Service disconnected.") + referrerClient.endConnection() + promise.reject( + "ERR_GOOGLE_PLAY_REFERRER_DISCONNECTED", + "Failed to get referrer info", + Exception("Failed to get referrer info") + ) + } + }) + } + } +} \ No newline at end of file diff --git a/modules/expo-bluesky-swiss-army/expo-module.config.json b/modules/expo-bluesky-swiss-army/expo-module.config.json new file mode 100644 index 0000000000..730bc6114f --- /dev/null +++ b/modules/expo-bluesky-swiss-army/expo-module.config.json @@ -0,0 +1,12 @@ +{ + "platforms": ["ios", "tvos", "android", "web"], + "ios": { + "modules": ["ExpoBlueskyDevicePrefsModule", "ExpoBlueskyReferrerModule"] + }, + "android": { + "modules": [ + "expo.modules.blueskyswissarmy.deviceprefs.ExpoBlueskyDevicePrefsModule", + "expo.modules.blueskyswissarmy.referrer.ExpoBlueskyReferrerModule" + ] + } +} diff --git a/modules/expo-bluesky-swiss-army/index.ts b/modules/expo-bluesky-swiss-army/index.ts new file mode 100644 index 0000000000..1b2f892494 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/index.ts @@ -0,0 +1,4 @@ +import * as DevicePrefs from './src/DevicePrefs' +import * as Referrer from './src/Referrer' + +export {DevicePrefs, Referrer} diff --git a/modules/expo-bluesky-swiss-army/ios/DevicePrefs/ExpoBlueskyDevicePrefsModule.swift b/modules/expo-bluesky-swiss-army/ios/DevicePrefs/ExpoBlueskyDevicePrefsModule.swift new file mode 100644 index 0000000000..b13a9fe3fa --- /dev/null +++ b/modules/expo-bluesky-swiss-army/ios/DevicePrefs/ExpoBlueskyDevicePrefsModule.swift @@ -0,0 +1,23 @@ +import ExpoModulesCore + +public class ExpoBlueskyDevicePrefsModule: Module { + func getDefaults(_ useAppGroup: Bool) -> UserDefaults? { + if useAppGroup { + return UserDefaults(suiteName: "group.app.bsky") + } else { + return UserDefaults.standard + } + } + + public func definition() -> ModuleDefinition { + Name("ExpoBlueskyDevicePrefs") + + AsyncFunction("getStringValueAsync") { (key: String, useAppGroup: Bool) in + return self.getDefaults(useAppGroup)?.string(forKey: key) + } + + AsyncFunction("setStringValueAsync") { (key: String, value: String?, useAppGroup: Bool) in + self.getDefaults(useAppGroup)?.setValue(value, forKey: key) + } + } +} diff --git a/modules/expo-bluesky-swiss-army/ios/ExpoBlueskySwissArmy.podspec b/modules/expo-bluesky-swiss-army/ios/ExpoBlueskySwissArmy.podspec new file mode 100644 index 0000000000..be4b0eae45 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/ios/ExpoBlueskySwissArmy.podspec @@ -0,0 +1,21 @@ +Pod::Spec.new do |s| + s.name = 'ExpoBlueskySwissArmy' + s.version = '1.0.0' + s.summary = 'A collection of native tools for Bluesky' + s.description = 'A collection of native tools for Bluesky' + s.author = '' + s.homepage = 'https://github.com/bluesky-social/social-app' + s.platforms = { :ios => '13.4', :tvos => '13.4' } + s.source = { git: '' } + s.static_framework = true + + s.dependency 'ExpoModulesCore' + + # Swift/Objective-C compatibility + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + 'SWIFT_COMPILATION_MODE' => 'wholemodule' + } + + s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}" +end diff --git a/modules/expo-bluesky-swiss-army/ios/Referrer/ExpoBlueskyReferrerModule.swift b/modules/expo-bluesky-swiss-army/ios/Referrer/ExpoBlueskyReferrerModule.swift new file mode 100644 index 0000000000..fd28c51e6f --- /dev/null +++ b/modules/expo-bluesky-swiss-army/ios/Referrer/ExpoBlueskyReferrerModule.swift @@ -0,0 +1,7 @@ +import ExpoModulesCore + +public class ExpoBlueskyReferrerModule: Module { + public func definition() -> ModuleDefinition { + Name("ExpoBlueskyReferrer") + } +} diff --git a/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ios.ts b/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ios.ts new file mode 100644 index 0000000000..4271850866 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ios.ts @@ -0,0 +1,18 @@ +import {requireNativeModule} from 'expo-modules-core' + +const NativeModule = requireNativeModule('ExpoBlueskyDevicePrefs') + +export function getStringValueAsync( + key: string, + useAppGroup?: boolean, +): Promise { + return NativeModule.getStringValueAsync(key, useAppGroup) +} + +export function setStringValueAsync( + key: string, + value: string | null, + useAppGroup?: boolean, +): Promise { + return NativeModule.setStringValueAsync(key, value, useAppGroup) +} diff --git a/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ts b/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ts new file mode 100644 index 0000000000..f1eee6c282 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ts @@ -0,0 +1,16 @@ +import {NotImplementedError} from '../NotImplemented' + +export function getStringValueAsync( + key: string, + useAppGroup?: boolean, +): Promise { + throw new NotImplementedError({key, useAppGroup}) +} + +export function setStringValueAsync( + key: string, + value: string | null, + useAppGroup?: boolean, +): Promise { + throw new NotImplementedError({key, value, useAppGroup}) +} diff --git a/modules/expo-bluesky-swiss-army/src/NotImplemented.ts b/modules/expo-bluesky-swiss-army/src/NotImplemented.ts new file mode 100644 index 0000000000..876cd7b328 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/NotImplemented.ts @@ -0,0 +1,16 @@ +import {Platform} from 'react-native' + +export class NotImplementedError extends Error { + constructor(params = {}) { + if (__DEV__) { + const caller = new Error().stack?.split('\n')[2] + super( + `Not implemented on ${Platform.OS}. Given params: ${JSON.stringify( + params, + )} ${caller}`, + ) + } else { + super('Not implemented') + } + } +} diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/index.android.ts b/modules/expo-bluesky-swiss-army/src/Referrer/index.android.ts new file mode 100644 index 0000000000..06dfd2d09c --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/Referrer/index.android.ts @@ -0,0 +1,9 @@ +import {requireNativeModule} from 'expo' + +import {GooglePlayReferrerInfo} from './types' + +export const NativeModule = requireNativeModule('ExpoBlueskyReferrer') + +export function getGooglePlayReferrerInfoAsync(): Promise { + return NativeModule.getGooglePlayReferrerInfoAsync() +} diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/index.ts b/modules/expo-bluesky-swiss-army/src/Referrer/index.ts new file mode 100644 index 0000000000..2553985527 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/Referrer/index.ts @@ -0,0 +1,7 @@ +import {NotImplementedError} from '../NotImplemented' +import {GooglePlayReferrerInfo} from './types' + +// @ts-ignore throws +export function getGooglePlayReferrerInfoAsync(): Promise { + throw new NotImplementedError() +} diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/types.ts b/modules/expo-bluesky-swiss-army/src/Referrer/types.ts new file mode 100644 index 0000000000..55faaff4d3 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/Referrer/types.ts @@ -0,0 +1,7 @@ +export type GooglePlayReferrerInfo = + | { + installReferrer?: string + clickTimestamp?: number + installTimestamp?: number + } + | undefined diff --git a/package.json b/package.json index bcd5a1d37e..6577703099 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web" }, "dependencies": { - "@atproto/api": "^0.12.20", + "@atproto/api": "0.12.22-next.0", "@bam.tech/react-native-image-resizer": "^3.0.4", "@braintree/sanitize-url": "^6.0.2", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", @@ -177,6 +177,7 @@ "react-native-pager-view": "6.2.3", "react-native-picker-select": "^9.1.3", "react-native-progress": "bluesky-social/react-native-progress", + "react-native-qrcode-styled": "^0.3.1", "react-native-reanimated": "^3.11.0", "react-native-root-siblings": "^4.1.1", "react-native-safe-area-context": "4.10.1", @@ -205,7 +206,7 @@ "@babel/preset-env": "^7.20.0", "@babel/runtime": "^7.20.0", "@did-plc/server": "^0.0.1", - "@expo/config-plugins": "7.8.0", + "@expo/config-plugins": "8.0.4", "@expo/prebuild-config": "6.7.0", "@lingui/cli": "^4.5.0", "@lingui/macro": "^4.5.0", diff --git a/plugins/starterPackAppClipExtension/withAppEntitlements.js b/plugins/starterPackAppClipExtension/withAppEntitlements.js new file mode 100644 index 0000000000..1bffd82080 --- /dev/null +++ b/plugins/starterPackAppClipExtension/withAppEntitlements.js @@ -0,0 +1,16 @@ +const {withEntitlementsPlist} = require('@expo/config-plugins') + +const withAppEntitlements = config => { + // eslint-disable-next-line no-shadow + return withEntitlementsPlist(config, async config => { + config.modResults['com.apple.security.application-groups'] = [ + `group.app.bsky`, + ] + config.modResults[ + 'com.apple.developer.associated-appclip-app-identifiers' + ] = [`$(AppIdentifierPrefix)${config.ios.bundleIdentifier}.AppClip`] + return config + }) +} + +module.exports = {withAppEntitlements} diff --git a/plugins/starterPackAppClipExtension/withClipEntitlements.js b/plugins/starterPackAppClipExtension/withClipEntitlements.js new file mode 100644 index 0000000000..77636b5c94 --- /dev/null +++ b/plugins/starterPackAppClipExtension/withClipEntitlements.js @@ -0,0 +1,32 @@ +const {withInfoPlist} = require('@expo/config-plugins') +const plist = require('@expo/plist') +const path = require('path') +const fs = require('fs') + +const withClipEntitlements = (config, {targetName}) => { + // eslint-disable-next-line no-shadow + return withInfoPlist(config, config => { + const entitlementsPath = path.join( + config.modRequest.platformProjectRoot, + targetName, + `${targetName}.entitlements`, + ) + + const appClipEntitlements = { + 'com.apple.security.application-groups': [`group.app.bsky`], + 'com.apple.developer.parent-application-identifiers': [ + `$(AppIdentifierPrefix)${config.ios.bundleIdentifier}`, + ], + 'com.apple.developer.associated-domains': config.ios.associatedDomains, + } + + fs.mkdirSync(path.dirname(entitlementsPath), { + recursive: true, + }) + fs.writeFileSync(entitlementsPath, plist.default.build(appClipEntitlements)) + + return config + }) +} + +module.exports = {withClipEntitlements} diff --git a/plugins/starterPackAppClipExtension/withClipInfoPlist.js b/plugins/starterPackAppClipExtension/withClipInfoPlist.js new file mode 100644 index 0000000000..59fbed1a9e --- /dev/null +++ b/plugins/starterPackAppClipExtension/withClipInfoPlist.js @@ -0,0 +1,38 @@ +const {withInfoPlist} = require('@expo/config-plugins') +const plist = require('@expo/plist') +const path = require('path') +const fs = require('fs') + +const withClipInfoPlist = (config, {targetName}) => { + // eslint-disable-next-line no-shadow + return withInfoPlist(config, config => { + const targetPath = path.join( + config.modRequest.platformProjectRoot, + targetName, + 'Info.plist', + ) + + const newPlist = plist.default.build({ + NSAppClip: { + NSAppClipRequestEphemeralUserNotification: false, + NSAppClipRequestLocationConfirmation: false, + }, + UILaunchScreen: {}, + CFBundleName: '$(PRODUCT_NAME)', + CFBundleIdentifier: '$(PRODUCT_BUNDLE_IDENTIFIER)', + CFBundleVersion: '$(CURRENT_PROJECT_VERSION)', + CFBundleExecutable: '$(EXECUTABLE_NAME)', + CFBundlePackageType: '$(PRODUCT_BUNDLE_PACKAGE_TYPE)', + CFBundleShortVersionString: config.version, + CFBundleIconName: 'AppIcon', + UIViewControllerBasedStatusBarAppearance: 'NO', + }) + + fs.mkdirSync(path.dirname(targetPath), {recursive: true}) + fs.writeFileSync(targetPath, newPlist) + + return config + }) +} + +module.exports = {withClipInfoPlist} diff --git a/plugins/starterPackAppClipExtension/withFiles.js b/plugins/starterPackAppClipExtension/withFiles.js new file mode 100644 index 0000000000..ad99f5ae41 --- /dev/null +++ b/plugins/starterPackAppClipExtension/withFiles.js @@ -0,0 +1,40 @@ +const {withXcodeProject} = require('@expo/config-plugins') +const path = require('path') +const fs = require('fs') + +const FILES = ['AppDelegate.swift', 'ViewController.swift'] + +const withFiles = (config, {targetName}) => { + // eslint-disable-next-line no-shadow + return withXcodeProject(config, config => { + const basePath = path.join( + config.modRequest.projectRoot, + 'modules', + targetName, + ) + + for (const file of FILES) { + const sourcePath = path.join(basePath, file) + const targetPath = path.join( + config.modRequest.platformProjectRoot, + targetName, + file, + ) + + fs.mkdirSync(path.dirname(targetPath), {recursive: true}) + fs.copyFileSync(sourcePath, targetPath) + } + + const imagesBasePath = path.join(basePath, 'Images.xcassets') + const imagesTargetPath = path.join( + config.modRequest.platformProjectRoot, + targetName, + 'Images.xcassets', + ) + fs.cpSync(imagesBasePath, imagesTargetPath, {recursive: true}) + + return config + }) +} + +module.exports = {withFiles} diff --git a/plugins/starterPackAppClipExtension/withStarterPackAppClip.js b/plugins/starterPackAppClipExtension/withStarterPackAppClip.js new file mode 100644 index 0000000000..1e3f0b7029 --- /dev/null +++ b/plugins/starterPackAppClipExtension/withStarterPackAppClip.js @@ -0,0 +1,40 @@ +const {withPlugins} = require('@expo/config-plugins') +const {withAppEntitlements} = require('./withAppEntitlements') +const {withClipEntitlements} = require('./withClipEntitlements') +const {withClipInfoPlist} = require('./withClipInfoPlist') +const {withFiles} = require('./withFiles') +const {withXcodeTarget} = require('./withXcodeTarget') + +const APP_CLIP_TARGET_NAME = 'BlueskyClip' + +const withStarterPackAppClip = config => { + return withPlugins(config, [ + withAppEntitlements, + [ + withClipEntitlements, + { + targetName: APP_CLIP_TARGET_NAME, + }, + ], + [ + withClipInfoPlist, + { + targetName: APP_CLIP_TARGET_NAME, + }, + ], + [ + withFiles, + { + targetName: APP_CLIP_TARGET_NAME, + }, + ], + [ + withXcodeTarget, + { + targetName: APP_CLIP_TARGET_NAME, + }, + ], + ]) +} + +module.exports = withStarterPackAppClip diff --git a/plugins/starterPackAppClipExtension/withXcodeTarget.js b/plugins/starterPackAppClipExtension/withXcodeTarget.js new file mode 100644 index 0000000000..61d5f81b07 --- /dev/null +++ b/plugins/starterPackAppClipExtension/withXcodeTarget.js @@ -0,0 +1,91 @@ +const {withXcodeProject} = require('@expo/config-plugins') + +const BUILD_PHASE_FILES = ['AppDelegate.swift', 'ViewController.swift'] + +const withXcodeTarget = (config, {targetName}) => { + // eslint-disable-next-line no-shadow + return withXcodeProject(config, config => { + const pbxProject = config.modResults + + const target = pbxProject.addTarget(targetName, 'application', targetName) + target.pbxNativeTarget.productType = `"com.apple.product-type.application.on-demand-install-capable"` + pbxProject.addBuildPhase( + BUILD_PHASE_FILES.map(f => `${targetName}/${f}`), + 'PBXSourcesBuildPhase', + 'Sources', + target.uuid, + 'application', + '"AppClips"', + ) + pbxProject.addBuildPhase( + [`${targetName}/Images.xcassets`], + 'PBXResourcesBuildPhase', + 'Resources', + target.uuid, + 'application', + '"AppClips"', + ) + + const pbxGroup = pbxProject.addPbxGroup([ + 'AppDelegate.swift', + 'ViewController.swift', + 'Images.xcassets', + `${targetName}.entitlements`, + 'Info.plist', + ]) + + pbxProject.addFile(`${targetName}/Info.plist`, pbxGroup.uuid) + const configurations = pbxProject.pbxXCBuildConfigurationSection() + for (const key in configurations) { + if (typeof configurations[key].buildSettings !== 'undefined') { + const buildSettingsObj = configurations[key].buildSettings + if ( + typeof buildSettingsObj.PRODUCT_NAME !== 'undefined' && + buildSettingsObj.PRODUCT_NAME === `"${targetName}"` + ) { + buildSettingsObj.CLANG_ENABLE_MODULES = 'YES' + buildSettingsObj.INFOPLIST_FILE = `"${targetName}/Info.plist"` + buildSettingsObj.CODE_SIGN_ENTITLEMENTS = `"${targetName}/${targetName}.entitlements"` + buildSettingsObj.CODE_SIGN_STYLE = 'Automatic' + buildSettingsObj.CURRENT_PROJECT_VERSION = `"${ + process.env.BSKY_IOS_BUILD_NUMBER ?? '1' + }"` + buildSettingsObj.GENERATE_INFOPLIST_FILE = 'YES' + buildSettingsObj.MARKETING_VERSION = `"${config.version}"` + buildSettingsObj.PRODUCT_BUNDLE_IDENTIFIER = `"${config.ios?.bundleIdentifier}.AppClip"` + buildSettingsObj.SWIFT_EMIT_LOC_STRINGS = 'YES' + buildSettingsObj.SWIFT_VERSION = '5.0' + buildSettingsObj.TARGETED_DEVICE_FAMILY = `"1"` + buildSettingsObj.DEVELOPMENT_TEAM = 'B3LX46C5HS' + buildSettingsObj.IPHONEOS_DEPLOYMENT_TARGET = '14.0' + buildSettingsObj.ASSETCATALOG_COMPILER_APPICON_NAME = 'AppIcon' + } + } + } + + pbxProject.addTargetAttribute('DevelopmentTeam', 'B3LX46C5HS', targetName) + + if (!pbxProject.hash.project.objects.PBXTargetDependency) { + pbxProject.hash.project.objects.PBXTargetDependency = {} + } + if (!pbxProject.hash.project.objects.PBXContainerItemProxy) { + pbxProject.hash.project.objects.PBXContainerItemProxy = {} + } + pbxProject.addTargetDependency(pbxProject.getFirstTarget().uuid, [ + target.uuid, + ]) + + pbxProject.addBuildPhase( + [`${targetName}.app`], + 'PBXCopyFilesBuildPhase', + 'Embed App Clips', + pbxProject.getFirstTarget().uuid, + 'application', + '"AppClips"', + ) + + return config + }) +} + +module.exports = {withXcodeTarget} diff --git a/scripts/updateExtensions.sh b/scripts/updateExtensions.sh index f3e972aa7b..b01134eebe 100755 --- a/scripts/updateExtensions.sh +++ b/scripts/updateExtensions.sh @@ -1,6 +1,7 @@ #!/bin/bash IOS_SHARE_EXTENSION_DIRECTORY="./ios/Share-with-Bluesky" IOS_NOTIFICATION_EXTENSION_DIRECTORY="./ios/BlueskyNSE" +IOS_APP_CLIP_DIRECTORY="./ios/BlueskyClip" MODULES_DIRECTORY="./modules" if [ ! -d $IOS_SHARE_EXTENSION_DIRECTORY ]; then @@ -16,3 +17,11 @@ if [ ! -d $IOS_NOTIFICATION_EXTENSION_DIRECTORY ]; then else cp -R $IOS_NOTIFICATION_EXTENSION_DIRECTORY $MODULES_DIRECTORY fi + + +if [ ! -d $IOS_APP_CLIP_DIRECTORY ]; then + echo "$IOS_APP_CLIP_DIRECTORY not found inside of your iOS project." + exit 1 +else + cp -R $IOS_APP_CLIP_DIRECTORY $MODULES_DIRECTORY +fi diff --git a/src/App.native.tsx b/src/App.native.tsx index 4c73d87525..639276a12d 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -46,11 +46,13 @@ import {readLastActiveAccount} from '#/state/session/util' import {Provider as ShellStateProvider} from '#/state/shell' import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' +import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {TestCtrls} from '#/view/com/testing/TestCtrls' 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 {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' import {Provider as PortalProvider} from '#/components/Portal' import {Splash} from '#/Splash' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' @@ -67,6 +69,7 @@ function InnerApp() { const {_} = useLingui() useIntentHandler() + const hasCheckedReferrer = useStarterPackEntry() // init useEffect(() => { @@ -98,7 +101,7 @@ function InnerApp() { - + - + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index 00939c9eb4..31a59d97dd 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -35,11 +35,13 @@ import {readLastActiveAccount} from '#/state/session/util' import {Provider as ShellStateProvider} from '#/state/shell' import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' +import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import * as Toast from '#/view/com/util/Toast' 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 {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' import {Provider as PortalProvider} from '#/components/Portal' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' import I18nProvider from './locale/i18nProvider' @@ -52,6 +54,7 @@ function InnerApp() { const theme = useColorModeTheme() const {_} = useLingui() useIntentHandler() + const hasCheckedReferrer = useStarterPackEntry() // init useEffect(() => { @@ -77,7 +80,7 @@ function InnerApp() { }, [_]) // wait for session to resume - if (!isReady) return null + if (!isReady || !hasCheckedReferrer) return null return ( @@ -146,7 +149,9 @@ function App() { - + + + diff --git a/src/Navigation.tsx b/src/Navigation.tsx index f2b7cd911f..5cb4f4105f 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -43,6 +43,8 @@ import HashtagScreen from '#/screens/Hashtag' import {ModerationScreen} from '#/screens/Moderation' import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers' import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy' +import {StarterPackScreen} from '#/screens/StarterPack/StarterPackScreen' +import {Wizard} from '#/screens/StarterPack/Wizard' import {init as initAnalytics} from './lib/analytics/analytics' import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration' import {attachRouteToLogEvents, logEvent} from './lib/statsig/statsig' @@ -317,6 +319,21 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { getComponent={() => FeedsScreen} options={{title: title(msg`Feeds`)}} /> + StarterPackScreen} + options={{title: title(msg`Starter Pack`), requireAuth: true}} + /> + Wizard} + options={{title: title(msg`Create a starter pack`), requireAuth: true}} + /> + Wizard} + options={{title: title(msg`Edit your starter pack`), requireAuth: true}} + /> ) } @@ -371,6 +388,7 @@ function HomeTabNavigator() { contentStyle: pal.view, }}> HomeScreen} /> + HomeScreen} /> {commonScreens(HomeTab)} ) @@ -507,6 +525,11 @@ const FlatNavigator = () => { getComponent={() => MessagesScreen} options={{title: title(msg`Messages`), requireAuth: true}} /> + HomeScreen} + options={{title: title(msg`Home`)}} + /> {commonScreens(Flat as typeof HomeTab, numUnread)} ) diff --git a/src/components/LinearGradientBackground.tsx b/src/components/LinearGradientBackground.tsx new file mode 100644 index 0000000000..f516b19f5f --- /dev/null +++ b/src/components/LinearGradientBackground.tsx @@ -0,0 +1,23 @@ +import React from 'react' +import {StyleProp, ViewStyle} from 'react-native' +import {LinearGradient} from 'expo-linear-gradient' + +import {gradients} from '#/alf/tokens' + +export function LinearGradientBackground({ + style, + children, +}: { + style: StyleProp + children: React.ReactNode +}) { + const gradient = gradients.sky.values.map(([_, color]) => { + return color + }) + + return ( + + {children} + + ) +} diff --git a/src/components/NewskieDialog.tsx b/src/components/NewskieDialog.tsx index 0354bfc432..6743a592ba 100644 --- a/src/components/NewskieDialog.tsx +++ b/src/components/NewskieDialog.tsx @@ -9,11 +9,13 @@ import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {HITSLOP_10} from 'lib/constants' import {sanitizeDisplayName} from 'lib/strings/display-names' -import {atoms as a} from '#/alf' -import {Button} from '#/components/Button' +import {isWeb} from 'platform/detection' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {useDialogControl} from '#/components/Dialog' import {Newskie} from '#/components/icons/Newskie' +import * as StarterPackCard from '#/components/StarterPack/StarterPackCard' import {Text} from '#/components/Typography' export function NewskieDialog({ @@ -24,6 +26,7 @@ export function NewskieDialog({ disabled?: boolean }) { const {_} = useLingui() + const t = useTheme() const moderationOpts = useModerationOpts() const control = useDialogControl() const profileName = React.useMemo(() => { @@ -68,15 +71,62 @@ export function NewskieDialog({ label={_(msg`New user info dialog`)} style={[{width: 'auto', maxWidth: 400, minWidth: 200}]}> - - Say hello! - - - - {profileName} joined Bluesky{' '} - {timeAgo(createdAt, now, {format: 'long'})} ago - + + + + Say hello! + + + + {profile.joinedViaStarterPack ? ( + + {profileName} joined Bluesky using a starter pack{' '} + {timeAgo(createdAt, now, {format: 'long'})} ago + + ) : ( + + {profileName} joined Bluesky{' '} + {timeAgo(createdAt, now, {format: 'long'})} ago + + )} + {profile.joinedViaStarterPack ? ( + { + control.close() + }}> + + + + + ) : null} + diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx new file mode 100644 index 0000000000..a0d222854b --- /dev/null +++ b/src/components/ProfileCard.tsx @@ -0,0 +1,91 @@ +import React from 'react' +import {View} from 'react-native' +import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' + +import {createSanitizedDisplayName} from 'lib/moderation/create-sanitized-display-name' +import {sanitizeHandle} from 'lib/strings/handles' +import {useProfileShadow} from 'state/cache/profile-shadow' +import {useSession} from 'state/session' +import {FollowButton} from 'view/com/profile/FollowButton' +import {ProfileCardPills} from 'view/com/profile/ProfileCard' +import {UserAvatar} from 'view/com/util/UserAvatar' +import {atoms as a, useTheme} from '#/alf' +import {Link} from '#/components/Link' +import {Text} from '#/components/Typography' + +export function Default({ + profile: profileUnshadowed, + moderationOpts, + logContext = 'ProfileCard', +}: { + profile: AppBskyActorDefs.ProfileViewDetailed + moderationOpts: ModerationOpts + logContext?: 'ProfileCard' | 'StarterPackProfilesList' +}) { + const t = useTheme() + const {currentAccount, hasSession} = useSession() + + const profile = useProfileShadow(profileUnshadowed) + const name = createSanitizedDisplayName(profile) + const handle = `@${sanitizeHandle(profile.handle)}` + const moderation = moderateProfile(profile, moderationOpts) + + return ( + + + + + + {name} + + + {handle} + + + {hasSession && profile.did !== currentAccount?.did && ( + + + + )} + + + + + {profile.description && ( + + {profile.description} + + )} + + ) +} + +function Wrapper({did, children}: {did: string; children: React.ReactNode}) { + return ( + + {children} + + ) +} diff --git a/src/components/ReportDialog/SelectReportOptionView.tsx b/src/components/ReportDialog/SelectReportOptionView.tsx index 4413cbe890..169c07d732 100644 --- a/src/components/ReportDialog/SelectReportOptionView.tsx +++ b/src/components/ReportDialog/SelectReportOptionView.tsx @@ -55,6 +55,9 @@ export function SelectReportOptionView({ } else if (props.params.type === 'feedgen') { title = _(msg`Report this feed`) description = _(msg`Why should this feed be reviewed?`) + } else if (props.params.type === 'starterpack') { + title = _(msg`Report this starter pack`) + description = _(msg`Why should this starter pack be reviewed?`) } else if (props.params.type === 'convoMessage') { title = _(msg`Report this message`) description = _(msg`Why should this message be reviewed?`) diff --git a/src/components/ReportDialog/types.ts b/src/components/ReportDialog/types.ts index ceabe0b901..3f43db4a19 100644 --- a/src/components/ReportDialog/types.ts +++ b/src/components/ReportDialog/types.ts @@ -4,7 +4,7 @@ export type ReportDialogProps = { control: Dialog.DialogOuterProps['control'] params: | { - type: 'post' | 'list' | 'feedgen' | 'other' + type: 'post' | 'list' | 'feedgen' | 'starterpack' | 'other' uri: string cid: string } diff --git a/src/components/StarterPack/Main/FeedsList.tsx b/src/components/StarterPack/Main/FeedsList.tsx new file mode 100644 index 0000000000..e350a422cf --- /dev/null +++ b/src/components/StarterPack/Main/FeedsList.tsx @@ -0,0 +1,68 @@ +import React, {useCallback} from 'react' +import {ListRenderItemInfo, View} from 'react-native' +import {AppBskyFeedDefs} from '@atproto/api' +import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' + +import {useBottomBarOffset} from 'lib/hooks/useBottomBarOffset' +import {isNative, isWeb} from 'platform/detection' +import {List, ListRef} from 'view/com/util/List' +import {SectionRef} from '#/screens/Profile/Sections/types' +import {atoms as a, useTheme} from '#/alf' +import * as FeedCard from '#/components/FeedCard' + +function keyExtractor(item: AppBskyFeedDefs.GeneratorView) { + return item.uri +} + +interface ProfilesListProps { + feeds: AppBskyFeedDefs.GeneratorView[] + headerHeight: number + scrollElRef: ListRef +} + +export const FeedsList = React.forwardRef( + function FeedsListImpl({feeds, headerHeight, scrollElRef}, ref) { + const [initialHeaderHeight] = React.useState(headerHeight) + const bottomBarOffset = useBottomBarOffset(20) + const t = useTheme() + + const onScrollToTop = useCallback(() => { + scrollElRef.current?.scrollToOffset({ + animated: isNative, + offset: -headerHeight, + }) + }, [scrollElRef, headerHeight]) + + React.useImperativeHandle(ref, () => ({ + scrollToTop: onScrollToTop, + })) + + const renderItem = ({item, index}: ListRenderItemInfo) => { + return ( + + + + ) + } + + return ( + + } + showsVerticalScrollIndicator={false} + desktopFixedHeight={true} + /> + ) + }, +) diff --git a/src/components/StarterPack/Main/ProfilesList.tsx b/src/components/StarterPack/Main/ProfilesList.tsx new file mode 100644 index 0000000000..72d35fe2b2 --- /dev/null +++ b/src/components/StarterPack/Main/ProfilesList.tsx @@ -0,0 +1,119 @@ +import React, {useCallback} from 'react' +import {ListRenderItemInfo, View} from 'react-native' +import { + AppBskyActorDefs, + AppBskyGraphGetList, + AtUri, + ModerationOpts, +} from '@atproto/api' +import {InfiniteData, UseInfiniteQueryResult} from '@tanstack/react-query' + +import {useBottomBarOffset} from 'lib/hooks/useBottomBarOffset' +import {isNative, isWeb} from 'platform/detection' +import {useSession} from 'state/session' +import {List, ListRef} from 'view/com/util/List' +import {SectionRef} from '#/screens/Profile/Sections/types' +import {atoms as a, useTheme} from '#/alf' +import {Default as ProfileCard} from '#/components/ProfileCard' + +function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic, index: number) { + return `${item.did}-${index}` +} + +interface ProfilesListProps { + listUri: string + listMembersQuery: UseInfiniteQueryResult< + InfiniteData + > + moderationOpts: ModerationOpts + headerHeight: number + scrollElRef: ListRef +} + +export const ProfilesList = React.forwardRef( + function ProfilesListImpl( + {listUri, listMembersQuery, moderationOpts, headerHeight, scrollElRef}, + ref, + ) { + const t = useTheme() + const [initialHeaderHeight] = React.useState(headerHeight) + const bottomBarOffset = useBottomBarOffset(20) + const {currentAccount} = useSession() + + const [isPTRing, setIsPTRing] = React.useState(false) + + const {data, refetch} = listMembersQuery + + // The server returns these sorted by descending creation date, so we want to invert + const profiles = data?.pages + .flatMap(p => p.items.map(i => i.subject)) + .reverse() + const isOwn = new AtUri(listUri).host === currentAccount?.did + + const getSortedProfiles = () => { + if (!profiles) return + if (!isOwn) return profiles + + const myIndex = profiles.findIndex(p => p.did === currentAccount?.did) + return myIndex !== -1 + ? [ + profiles[myIndex], + ...profiles.slice(0, myIndex), + ...profiles.slice(myIndex + 1), + ] + : profiles + } + const onScrollToTop = useCallback(() => { + scrollElRef.current?.scrollToOffset({ + animated: isNative, + offset: -headerHeight, + }) + }, [scrollElRef, headerHeight]) + + React.useImperativeHandle(ref, () => ({ + scrollToTop: onScrollToTop, + })) + + const renderItem = ({ + item, + index, + }: ListRenderItemInfo) => { + return ( + + + + ) + } + + if (listMembersQuery) + return ( + + } + showsVerticalScrollIndicator={false} + desktopFixedHeight + refreshing={isPTRing} + onRefresh={async () => { + setIsPTRing(true) + await refetch() + setIsPTRing(false) + }} + /> + ) + }, +) diff --git a/src/components/StarterPack/ProfileStarterPacks.tsx b/src/components/StarterPack/ProfileStarterPacks.tsx new file mode 100644 index 0000000000..096f04f2dd --- /dev/null +++ b/src/components/StarterPack/ProfileStarterPacks.tsx @@ -0,0 +1,320 @@ +import React from 'react' +import { + findNodeHandle, + ListRenderItemInfo, + StyleProp, + View, + ViewStyle, +} from 'react-native' +import {AppBskyGraphDefs, AppBskyGraphGetActorStarterPacks} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useNavigation} from '@react-navigation/native' +import {InfiniteData, UseInfiniteQueryResult} from '@tanstack/react-query' + +import {logger} from '#/logger' +import {useGenerateStarterPackMutation} from 'lib/generate-starterpack' +import {useBottomBarOffset} from 'lib/hooks/useBottomBarOffset' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {NavigationProp} from 'lib/routes/types' +import {parseStarterPackUri} from 'lib/strings/starter-pack' +import {List, ListRef} from 'view/com/util/List' +import {Text} from 'view/com/util/text/Text' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {useDialogControl} from '#/components/Dialog' +import {LinearGradientBackground} from '#/components/LinearGradientBackground' +import {Loader} from '#/components/Loader' +import * as Prompt from '#/components/Prompt' +import {Default as StarterPackCard} from '#/components/StarterPack/StarterPackCard' +import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '../icons/Plus' + +interface SectionRef { + scrollToTop: () => void +} + +interface ProfileFeedgensProps { + starterPacksQuery: UseInfiniteQueryResult< + InfiniteData, + Error + > + scrollElRef: ListRef + headerOffset: number + enabled?: boolean + style?: StyleProp + testID?: string + setScrollViewTag: (tag: number | null) => void + isMe: boolean +} + +function keyExtractor(item: AppBskyGraphDefs.StarterPackView) { + return item.uri +} + +export const ProfileStarterPacks = React.forwardRef< + SectionRef, + ProfileFeedgensProps +>(function ProfileFeedgensImpl( + { + starterPacksQuery: query, + scrollElRef, + headerOffset, + enabled, + style, + testID, + setScrollViewTag, + isMe, + }, + ref, +) { + const t = useTheme() + const bottomBarOffset = useBottomBarOffset(100) + const [isPTRing, setIsPTRing] = React.useState(false) + const {data, refetch, isFetching, hasNextPage, fetchNextPage} = query + const {isTabletOrDesktop} = useWebMediaQueries() + + const items = data?.pages.flatMap(page => page.starterPacks) + + React.useImperativeHandle(ref, () => ({ + scrollToTop: () => {}, + })) + + const onRefresh = React.useCallback(async () => { + setIsPTRing(true) + try { + await refetch() + } catch (err) { + logger.error('Failed to refresh starter packs', {message: err}) + } + setIsPTRing(false) + }, [refetch, setIsPTRing]) + + const onEndReached = React.useCallback(async () => { + if (isFetching || !hasNextPage) return + + try { + await fetchNextPage() + } catch (err) { + logger.error('Failed to load more starter packs', {message: err}) + } + }, [isFetching, hasNextPage, fetchNextPage]) + + React.useEffect(() => { + if (enabled && scrollElRef.current) { + const nativeTag = findNodeHandle(scrollElRef.current) + setScrollViewTag(nativeTag) + } + }, [enabled, scrollElRef, setScrollViewTag]) + + const renderItem = ({ + item, + index, + }: ListRenderItemInfo) => { + return ( + + + + ) + } + + return ( + + + + ) +}) + +function CreateAnother() { + const {_} = useLingui() + const t = useTheme() + const navigation = useNavigation() + + return ( + + + + ) +} + +function Empty() { + const {_} = useLingui() + const t = useTheme() + const navigation = useNavigation() + const confirmDialogControl = useDialogControl() + const followersDialogControl = useDialogControl() + const errorDialogControl = useDialogControl() + + const [isGenerating, setIsGenerating] = React.useState(false) + + const {mutate: generateStarterPack} = useGenerateStarterPackMutation({ + onSuccess: ({uri}) => { + const parsed = parseStarterPackUri(uri) + if (parsed) { + navigation.push('StarterPack', { + name: parsed.name, + rkey: parsed.rkey, + }) + } + setIsGenerating(false) + }, + onError: e => { + logger.error('Failed to generate starter pack', {safeMessage: e}) + setIsGenerating(false) + if (e.name === 'NOT_ENOUGH_FOLLOWERS') { + followersDialogControl.open() + } else { + errorDialogControl.open() + } + }, + }) + + const generate = () => { + setIsGenerating(true) + generateStarterPack() + } + + return ( + + + + You haven't created a starter pack yet! + + + Starter packs let you easily share your favorite feeds and people with + your friends. + + + + + + + + + + Generate a starter pack + + + + Bluesky will choose a set of recommended accounts from people in + your network. + + + + + { + navigation.navigate('StarterPackWizard') + }} + /> + + + {}} + showCancel={false} + /> + + + ) +} diff --git a/src/components/StarterPack/QrCode.tsx b/src/components/StarterPack/QrCode.tsx new file mode 100644 index 0000000000..08ee03d623 --- /dev/null +++ b/src/components/StarterPack/QrCode.tsx @@ -0,0 +1,119 @@ +import React from 'react' +import {View} from 'react-native' +import QRCode from 'react-native-qrcode-styled' +import ViewShot from 'react-native-view-shot' +import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' +import {Trans} from '@lingui/macro' + +import {isWeb} from 'platform/detection' +import {Logo} from 'view/icons/Logo' +import {Logotype} from 'view/icons/Logotype' +import {useTheme} from '#/alf' +import {atoms as a} from '#/alf' +import {LinearGradientBackground} from '#/components/LinearGradientBackground' +import {Text} from '#/components/Typography' + +interface Props { + starterPack: AppBskyGraphDefs.StarterPackView + link: string +} + +export const QrCode = React.forwardRef(function QrCode( + {starterPack, link}, + ref, +) { + const {record} = starterPack + + if (!AppBskyGraphStarterpack.isRecord(record)) { + return null + } + + return ( + + + + + {record.name} + + + + + Join the conversation + + + + + + + + on + + + + + + + + + + ) +}) + +export function QrCodeInner({link}: {link: string}) { + const t = useTheme() + + return ( + + ) +} diff --git a/src/components/StarterPack/QrCodeDialog.tsx b/src/components/StarterPack/QrCodeDialog.tsx new file mode 100644 index 0000000000..580c6cc7c8 --- /dev/null +++ b/src/components/StarterPack/QrCodeDialog.tsx @@ -0,0 +1,201 @@ +import React from 'react' +import {View} from 'react-native' +import ViewShot from 'react-native-view-shot' +import * as FS from 'expo-file-system' +import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker' +import * as Sharing from 'expo-sharing' +import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {nanoid} from 'nanoid/non-secure' + +import {logger} from '#/logger' +import {saveImageToMediaLibrary} from 'lib/media/manip' +import {logEvent} from 'lib/statsig/statsig' +import {isNative, isWeb} from 'platform/detection' +import * as Toast from '#/view/com/util/Toast' +import {atoms as a} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {DialogControlProps} from '#/components/Dialog' +import {Loader} from '#/components/Loader' +import {QrCode} from '#/components/StarterPack/QrCode' + +export function QrCodeDialog({ + starterPack, + link, + control, +}: { + starterPack: AppBskyGraphDefs.StarterPackView + link?: string + control: DialogControlProps +}) { + const {_} = useLingui() + const [isProcessing, setIsProcessing] = React.useState(false) + + const ref = React.useRef(null) + + const getCanvas = (base64: string): Promise => { + return new Promise(resolve => { + const image = new Image() + image.onload = () => { + const canvas = document.createElement('canvas') + canvas.width = image.width + canvas.height = image.height + + const ctx = canvas.getContext('2d') + ctx?.drawImage(image, 0, 0) + resolve(canvas) + } + image.src = base64 + }) + } + + const onSavePress = async () => { + ref.current?.capture?.().then(async (uri: string) => { + if (isNative) { + const res = await requestMediaLibraryPermissionsAsync() + + if (!res) { + Toast.show( + _( + msg`You must grant access to your photo library to save a QR code`, + ), + ) + return + } + + const filename = `${FS.documentDirectory}/${nanoid(12)}.png` + + // Incase of a FS failure, don't crash the app + try { + await FS.copyAsync({from: uri, to: filename}) + await saveImageToMediaLibrary({uri: filename}) + await FS.deleteAsync(filename) + } catch (e: unknown) { + Toast.show(_(msg`An error occurred while saving the QR code!`)) + logger.error('Failed to save QR code', {error: e}) + return + } + } else { + setIsProcessing(true) + + if (!AppBskyGraphStarterpack.isRecord(starterPack.record)) { + return + } + + 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', + `${starterPack.record.name.replaceAll(' ', '_')}_Share_Card.png`, + ) + link.setAttribute('href', imgHref) + link.click() + } + + logEvent('starterPack:share', { + starterPack: starterPack.uri, + shareType: 'qrcode', + qrShareType: 'save', + }) + setIsProcessing(false) + Toast.show( + isWeb + ? _(msg`QR code has been downloaded!`) + : _(msg`QR code saved to your camera roll!`), + ) + control.close() + }) + } + + const onCopyPress = async () => { + setIsProcessing(true) + ref.current?.capture?.().then(async (uri: string) => { + const canvas = await getCanvas(uri) + // @ts-expect-error web only + canvas.toBlob((blob: Blob) => { + const item = new ClipboardItem({'image/png': blob}) + navigator.clipboard.write([item]) + }) + + logEvent('starterPack:share', { + starterPack: starterPack.uri, + shareType: 'qrcode', + qrShareType: 'copy', + }) + Toast.show(_(msg`QR code copied to your clipboard!`)) + setIsProcessing(false) + control.close() + }) + } + + const onSharePress = async () => { + ref.current?.capture?.().then(async (uri: string) => { + control.close(() => { + Sharing.shareAsync(uri, {mimeType: 'image/png', UTI: 'image/png'}).then( + () => { + logEvent('starterPack:share', { + starterPack: starterPack.uri, + shareType: 'qrcode', + qrShareType: 'share', + }) + }, + ) + }) + }) + } + + return ( + + + + + {!link ? ( + + + + ) : ( + <> + + {isProcessing ? ( + + + + ) : ( + + + + + )} + + )} + + + + ) +} diff --git a/src/components/StarterPack/ShareDialog.tsx b/src/components/StarterPack/ShareDialog.tsx new file mode 100644 index 0000000000..23fa10fb39 --- /dev/null +++ b/src/components/StarterPack/ShareDialog.tsx @@ -0,0 +1,180 @@ +import React from 'react' +import {View} from 'react-native' +import * as FS from 'expo-file-system' +import {Image} from 'expo-image' +import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker' +import {AppBskyGraphDefs} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {nanoid} from 'nanoid/non-secure' + +import {logger} from '#/logger' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {saveImageToMediaLibrary} from 'lib/media/manip' +import {shareUrl} from 'lib/sharing' +import {logEvent} from 'lib/statsig/statsig' +import {getStarterPackOgCard} from 'lib/strings/starter-pack' +import {isNative, isWeb} from 'platform/detection' +import * as Toast from 'view/com/util/Toast' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import {DialogControlProps} from '#/components/Dialog' +import * as Dialog from '#/components/Dialog' +import {Loader} from '#/components/Loader' +import {Text} from '#/components/Typography' + +interface Props { + starterPack: AppBskyGraphDefs.StarterPackView + link?: string + imageLoaded?: boolean + qrDialogControl: DialogControlProps + control: DialogControlProps +} + +export function ShareDialog(props: Props) { + return ( + + + + ) +} + +function ShareDialogInner({ + starterPack, + link, + imageLoaded, + qrDialogControl, + control, +}: Props) { + const {_} = useLingui() + const t = useTheme() + const {isTabletOrDesktop} = useWebMediaQueries() + + const imageUrl = getStarterPackOgCard(starterPack) + + const onShareLink = async () => { + if (!link) return + shareUrl(link) + logEvent('starterPack:share', { + starterPack: starterPack.uri, + shareType: 'link', + }) + control.close() + } + + const onSave = async () => { + const res = await requestMediaLibraryPermissionsAsync() + + if (!res) { + Toast.show( + _(msg`You must grant access to your photo library to save the image.`), + ) + return + } + + const cachePath = await Image.getCachePathAsync(imageUrl) + const filename = `${FS.documentDirectory}/${nanoid(12)}.png` + + if (!cachePath) { + Toast.show(_(msg`An error occurred while saving the image.`)) + return + } + + try { + await FS.copyAsync({from: cachePath, to: filename}) + await saveImageToMediaLibrary({uri: filename}) + await FS.deleteAsync(filename) + + Toast.show(_(msg`Image saved to your camera roll!`)) + control.close() + } catch (e: unknown) { + Toast.show(_(msg`An error occurred while saving the QR code!`)) + logger.error('Failed to save QR code', {error: e}) + return + } + } + + return ( + <> + + + {!imageLoaded || !link ? ( + + + + ) : ( + + + + Invite people to this starter pack! + + + + Share this starter pack and help people join your community on + Bluesky. + + + + + + + + {isNative && ( + + )} + + + )} + + + ) +} diff --git a/src/components/StarterPack/StarterPackCard.tsx b/src/components/StarterPack/StarterPackCard.tsx new file mode 100644 index 0000000000..ab904d7ffa --- /dev/null +++ b/src/components/StarterPack/StarterPackCard.tsx @@ -0,0 +1,117 @@ +import React from 'react' +import {View} from 'react-native' +import {AppBskyGraphStarterpack, AtUri} from '@atproto/api' +import {StarterPackViewBasic} from '@atproto/api/dist/client/types/app/bsky/graph/defs' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {sanitizeHandle} from 'lib/strings/handles' +import {useSession} from 'state/session' +import {atoms as a, useTheme} from '#/alf' +import {StarterPack} from '#/components/icons/StarterPack' +import {Link as InternalLink, LinkProps} from '#/components/Link' +import {Text} from '#/components/Typography' + +export function Default({starterPack}: {starterPack?: StarterPackViewBasic}) { + if (!starterPack) return null + return ( + + + + ) +} + +export function Notification({ + starterPack, +}: { + starterPack?: StarterPackViewBasic +}) { + if (!starterPack) return null + return ( + + + + ) +} + +export function Card({ + starterPack, + noIcon, + noDescription, +}: { + starterPack: StarterPackViewBasic + noIcon?: boolean + noDescription?: boolean +}) { + const {record, creator, joinedAllTimeCount} = starterPack + + const {_} = useLingui() + const t = useTheme() + const {currentAccount} = useSession() + + if (!AppBskyGraphStarterpack.isRecord(record)) { + return null + } + + return ( + + + {!noIcon ? : null} + + + {record.name} + + + + Starter pack by{' '} + {creator?.did === currentAccount?.did + ? _(msg`you`) + : `@${sanitizeHandle(creator.handle)}`} + + + + + {!noDescription && record.description ? ( + + {record.description} + + ) : null} + {!!joinedAllTimeCount && joinedAllTimeCount >= 50 && ( + + {joinedAllTimeCount} users have joined! + + )} + + ) +} + +export function Link({ + starterPack, + children, + ...rest +}: { + starterPack: StarterPackViewBasic +} & Omit) { + const {record} = starterPack + const {rkey, handleOrDid} = React.useMemo(() => { + const rkey = new AtUri(starterPack.uri).rkey + const {creator} = starterPack + return {rkey, handleOrDid: creator.handle || creator.did} + }, [starterPack]) + + if (!AppBskyGraphStarterpack.isRecord(record)) { + return null + } + + return ( + + {children} + + ) +} diff --git a/src/components/StarterPack/Wizard/ScreenTransition.tsx b/src/components/StarterPack/Wizard/ScreenTransition.tsx new file mode 100644 index 0000000000..b7cd4e4c1f --- /dev/null +++ b/src/components/StarterPack/Wizard/ScreenTransition.tsx @@ -0,0 +1,31 @@ +import React from 'react' +import {StyleProp, ViewStyle} from 'react-native' +import Animated, { + FadeIn, + FadeOut, + SlideInLeft, + SlideInRight, +} from 'react-native-reanimated' + +import {isWeb} from 'platform/detection' + +export function ScreenTransition({ + direction, + style, + children, +}: { + direction: 'Backward' | 'Forward' + style?: StyleProp + children: React.ReactNode +}) { + const entering = direction === 'Forward' ? SlideInRight : SlideInLeft + + return ( + + {children} + + ) +} diff --git a/src/components/StarterPack/Wizard/WizardEditListDialog.tsx b/src/components/StarterPack/Wizard/WizardEditListDialog.tsx new file mode 100644 index 0000000000..bf250ac354 --- /dev/null +++ b/src/components/StarterPack/Wizard/WizardEditListDialog.tsx @@ -0,0 +1,152 @@ +import React, {useRef} from 'react' +import type {ListRenderItemInfo} from 'react-native' +import {View} from 'react-native' +import {AppBskyActorDefs, ModerationOpts} from '@atproto/api' +import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' +import {BottomSheetFlatListMethods} from '@discord/bottom-sheet' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {isWeb} from 'platform/detection' +import {useSession} from 'state/session' +import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State' +import {atoms as a, native, useTheme, web} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import { + WizardFeedCard, + WizardProfileCard, +} from '#/components/StarterPack/Wizard/WizardListCard' +import {Text} from '#/components/Typography' + +function keyExtractor( + item: AppBskyActorDefs.ProfileViewBasic | GeneratorView, + index: number, +) { + return `${item.did}-${index}` +} + +export function WizardEditListDialog({ + control, + state, + dispatch, + moderationOpts, + profile, +}: { + control: Dialog.DialogControlProps + state: WizardState + dispatch: (action: WizardAction) => void + moderationOpts: ModerationOpts + profile: AppBskyActorDefs.ProfileViewBasic +}) { + const {_} = useLingui() + const t = useTheme() + const {currentAccount} = useSession() + + const listRef = useRef(null) + + const getData = () => { + if (state.currentStep === 'Feeds') return state.feeds + + return [ + profile, + ...state.profiles.filter(p => p.did !== currentAccount?.did), + ] + } + + const renderItem = ({item}: ListRenderItemInfo) => + state.currentStep === 'Profiles' ? ( + + ) : ( + + ) + + return ( + + + + + + {state.currentStep === 'Profiles' ? ( + Edit People + ) : ( + Edit Feeds + )} + + + {isWeb && ( + + )} + + + } + stickyHeaderIndices={[0]} + style={[ + web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]), + native({ + height: '100%', + paddingHorizontal: 0, + marginTop: 0, + paddingTop: 0, + borderTopLeftRadius: 40, + borderTopRightRadius: 40, + }), + ]} + webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]} + keyboardDismissMode="on-drag" + removeClippedSubviews={true} + /> + + ) +} diff --git a/src/components/StarterPack/Wizard/WizardListCard.tsx b/src/components/StarterPack/Wizard/WizardListCard.tsx new file mode 100644 index 0000000000..f1332011d9 --- /dev/null +++ b/src/components/StarterPack/Wizard/WizardListCard.tsx @@ -0,0 +1,182 @@ +import React from 'react' +import {Keyboard, View} from 'react-native' +import { + AppBskyActorDefs, + AppBskyFeedDefs, + moderateFeedGenerator, + moderateProfile, + ModerationOpts, + ModerationUI, +} from '@atproto/api' +import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {DISCOVER_FEED_URI} from 'lib/constants' +import {sanitizeDisplayName} from 'lib/strings/display-names' +import {sanitizeHandle} from 'lib/strings/handles' +import {useSession} from 'state/session' +import {UserAvatar} from 'view/com/util/UserAvatar' +import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State' +import {atoms as a, useTheme} from '#/alf' +import * as Toggle from '#/components/forms/Toggle' +import {Checkbox} from '#/components/forms/Toggle' +import {Text} from '#/components/Typography' + +function WizardListCard({ + type, + displayName, + subtitle, + onPress, + avatar, + included, + disabled, + moderationUi, +}: { + type: 'user' | 'algo' + profile?: AppBskyActorDefs.ProfileViewBasic + feed?: AppBskyFeedDefs.GeneratorView + displayName: string + subtitle: string + onPress: () => void + avatar?: string + included?: boolean + disabled?: boolean + moderationUi: ModerationUI +}) { + const t = useTheme() + const {_} = useLingui() + + return ( + + + + + {displayName} + + + {subtitle} + + + + + ) +} + +export function WizardProfileCard({ + state, + dispatch, + profile, + moderationOpts, +}: { + state: WizardState + dispatch: (action: WizardAction) => void + profile: AppBskyActorDefs.ProfileViewBasic + moderationOpts: ModerationOpts +}) { + const {currentAccount} = useSession() + + const isMe = profile.did === currentAccount?.did + const included = isMe || state.profiles.some(p => p.did === profile.did) + const disabled = isMe || (!included && state.profiles.length >= 49) + const moderationUi = moderateProfile(profile, moderationOpts).ui('avatar') + const displayName = profile.displayName + ? sanitizeDisplayName(profile.displayName) + : `@${sanitizeHandle(profile.handle)}` + + const onPress = () => { + if (disabled) return + + Keyboard.dismiss() + if (profile.did === currentAccount?.did) return + + if (!included) { + dispatch({type: 'AddProfile', profile}) + } else { + dispatch({type: 'RemoveProfile', profileDid: profile.did}) + } + } + + return ( + + ) +} + +export function WizardFeedCard({ + generator, + state, + dispatch, + moderationOpts, +}: { + generator: GeneratorView + state: WizardState + dispatch: (action: WizardAction) => void + moderationOpts: ModerationOpts +}) { + const isDiscover = generator.uri === DISCOVER_FEED_URI + const included = isDiscover || state.feeds.some(f => f.uri === generator.uri) + const disabled = isDiscover || (!included && state.feeds.length >= 3) + const moderationUi = moderateFeedGenerator(generator, moderationOpts).ui( + 'avatar', + ) + + const onPress = () => { + if (disabled) return + + Keyboard.dismiss() + if (included) { + dispatch({type: 'RemoveFeed', feedUri: generator.uri}) + } else { + dispatch({type: 'AddFeed', feed: generator}) + } + } + + return ( + + ) +} diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx index f7a827b493..d513a6db99 100644 --- a/src/components/forms/TextField.tsx +++ b/src/components/forms/TextField.tsx @@ -140,6 +140,7 @@ export function createInput(Component: typeof TextInput) { onChangeText, isInvalid, inputRef, + style, ...rest }: InputProps) { const t = useTheme() @@ -206,6 +207,7 @@ export function createInput(Component: typeof TextInput) { android({ paddingBottom: 16, }), + style, ]} /> diff --git a/src/components/hooks/useStarterPackEntry.native.ts b/src/components/hooks/useStarterPackEntry.native.ts new file mode 100644 index 0000000000..b6e4ab05b1 --- /dev/null +++ b/src/components/hooks/useStarterPackEntry.native.ts @@ -0,0 +1,68 @@ +import React from 'react' + +import { + createStarterPackLinkFromAndroidReferrer, + httpStarterPackUriToAtUri, +} from 'lib/strings/starter-pack' +import {isAndroid} from 'platform/detection' +import {useHasCheckedForStarterPack} from 'state/preferences/used-starter-packs' +import {useSetActiveStarterPack} from 'state/shell/starter-pack' +import {DevicePrefs, Referrer} from '../../../modules/expo-bluesky-swiss-army' + +export function useStarterPackEntry() { + const [ready, setReady] = React.useState(false) + const setActiveStarterPack = useSetActiveStarterPack() + const hasCheckedForStarterPack = useHasCheckedForStarterPack() + + React.useEffect(() => { + if (ready) return + + // On Android, we cannot clear the referral link. It gets stored for 90 days and all we can do is query for it. So, + // let's just ensure we never check again after the first time. + if (hasCheckedForStarterPack) { + setReady(true) + return + } + + // Safety for Android. Very unlike this could happen, but just in case. The response should be nearly immediate + const timeout = setTimeout(() => { + setReady(true) + }, 500) + + ;(async () => { + let uri: string | null | undefined + + if (isAndroid) { + const res = await Referrer.getGooglePlayReferrerInfoAsync() + + if (res && res.installReferrer) { + uri = createStarterPackLinkFromAndroidReferrer(res.installReferrer) + } + } else { + const res = await DevicePrefs.getStringValueAsync( + 'starterPackUri', + true, + ) + + if (res) { + uri = httpStarterPackUriToAtUri(res) + DevicePrefs.setStringValueAsync('starterPackUri', null, true) + } + } + + if (uri) { + setActiveStarterPack({ + uri, + }) + } + + setReady(true) + })() + + return () => { + clearTimeout(timeout) + } + }, [ready, setActiveStarterPack, hasCheckedForStarterPack]) + + return ready +} diff --git a/src/components/hooks/useStarterPackEntry.ts b/src/components/hooks/useStarterPackEntry.ts new file mode 100644 index 0000000000..dba801e093 --- /dev/null +++ b/src/components/hooks/useStarterPackEntry.ts @@ -0,0 +1,29 @@ +import React from 'react' + +import {httpStarterPackUriToAtUri} from 'lib/strings/starter-pack' +import {useSetActiveStarterPack} from 'state/shell/starter-pack' + +export function useStarterPackEntry() { + const [ready, setReady] = React.useState(false) + + const setActiveStarterPack = useSetActiveStarterPack() + + React.useEffect(() => { + const href = window.location.href + const atUri = httpStarterPackUriToAtUri(href) + + if (atUri) { + const url = new URL(href) + // Determines if an App Clip is loading this landing page + const isClip = url.searchParams.get('clip') === 'true' + setActiveStarterPack({ + uri: atUri, + isClip, + }) + } + + setReady(true) + }, [setActiveStarterPack]) + + return ready +} diff --git a/src/components/icons/QrCode.tsx b/src/components/icons/QrCode.tsx new file mode 100644 index 0000000000..e841071f70 --- /dev/null +++ b/src/components/icons/QrCode.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const QrCode_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3 5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5Zm6 0H5v4h4V5ZM3 15a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4Zm6 0H5v4h4v-4ZM13 5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2V5Zm6 0h-4v4h4V5ZM14 13a1 1 0 0 1 1 1v1h1a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1Zm3 1a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1Zm0 4a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-1v1a1 1 0 1 1-2 0v-2Z', +}) diff --git a/src/components/icons/StarterPack.tsx b/src/components/icons/StarterPack.tsx new file mode 100644 index 0000000000..8c678bca47 --- /dev/null +++ b/src/components/icons/StarterPack.tsx @@ -0,0 +1,8 @@ +import {createMultiPathSVG} from './TEMPLATE' + +export const StarterPack = createMultiPathSVG({ + paths: [ + 'M11.26 5.227 5.02 6.899c-.734.197-1.17.95-.973 1.685l1.672 6.24c.197.734.951 1.17 1.685.973l6.24-1.672c.734-.197 1.17-.951.973-1.685L12.945 6.2a1.375 1.375 0 0 0-1.685-.973Zm-6.566.459a2.632 2.632 0 0 0-1.86 3.223l1.672 6.24a2.632 2.632 0 0 0 3.223 1.861l6.24-1.672a2.631 2.631 0 0 0 1.861-3.223l-1.672-6.24a2.632 2.632 0 0 0-3.223-1.861l-6.24 1.672Z', + 'M15.138 18.411a4.606 4.606 0 1 0 0-9.211 4.606 4.606 0 0 0 0 9.211Zm0 1.257a5.862 5.862 0 1 0 0-11.724 5.862 5.862 0 0 0 0 11.724Z', + ], +}) diff --git a/src/components/icons/TEMPLATE.tsx b/src/components/icons/TEMPLATE.tsx index f49c4280bb..47a5c36b2a 100644 --- a/src/components/icons/TEMPLATE.tsx +++ b/src/components/icons/TEMPLATE.tsx @@ -30,7 +30,7 @@ export const IconTemplate_Stroke2_Corner0_Rounded = React.forwardRef( export function createSinglePathSVG({path}: {path: string}) { return React.forwardRef(function LogoImpl(props, ref) { - const {fill, size, style, ...rest} = useCommonSVGProps(props) + const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props) return ( + {gradient} ) }) } + +export function createMultiPathSVG({paths}: {paths: string[]}) { + return React.forwardRef(function LogoImpl(props, ref) { + const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props) + + return ( + + {gradient} + {paths.map((path, i) => ( + + ))} + + ) + }) +} diff --git a/src/components/icons/common.ts b/src/components/icons/common.ts deleted file mode 100644 index 669c157f51..0000000000 --- a/src/components/icons/common.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {StyleSheet, TextProps} from 'react-native' -import type {PathProps, SvgProps} from 'react-native-svg' - -import {tokens} from '#/alf' - -export type Props = { - fill?: PathProps['fill'] - style?: TextProps['style'] - size?: keyof typeof sizes -} & Omit - -export const sizes = { - xs: 12, - sm: 16, - md: 20, - lg: 24, - xl: 28, -} - -export function useCommonSVGProps(props: Props) { - const {fill, size, ...rest} = props - const style = StyleSheet.flatten(rest.style) - const _fill = fill || style?.color || tokens.color.blue_500 - const _size = Number(size ? sizes[size] : rest.width || sizes.md) - - return { - fill: _fill, - size: _size, - style, - ...rest, - } -} diff --git a/src/components/icons/common.tsx b/src/components/icons/common.tsx new file mode 100644 index 0000000000..662718338c --- /dev/null +++ b/src/components/icons/common.tsx @@ -0,0 +1,59 @@ +import React from 'react' +import {StyleSheet, TextProps} from 'react-native' +import type {PathProps, SvgProps} from 'react-native-svg' +import {Defs, LinearGradient, Stop} from 'react-native-svg' +import {nanoid} from 'nanoid/non-secure' + +import {tokens} from '#/alf' + +export type Props = { + fill?: PathProps['fill'] + style?: TextProps['style'] + size?: keyof typeof sizes + gradient?: keyof typeof tokens.gradients +} & Omit + +export const sizes = { + xs: 12, + sm: 16, + md: 20, + lg: 24, + xl: 28, +} + +export function useCommonSVGProps(props: Props) { + const {fill, size, gradient, ...rest} = props + const style = StyleSheet.flatten(rest.style) + const _size = Number(size ? sizes[size] : rest.width || sizes.md) + let _fill = fill || style?.color || tokens.color.blue_500 + let gradientDef = null + + if (gradient && tokens.gradients[gradient]) { + const id = gradient + '_' + nanoid() + const config = tokens.gradients[gradient] + _fill = `url(#${id})` + gradientDef = ( + + + {config.values.map(([stop, fill]) => ( + + ))} + + + ) + } + + return { + fill: _fill, + size: _size, + style, + gradient: gradientDef, + ...rest, + } +} diff --git a/src/lib/browser.native.ts b/src/lib/browser.native.ts index fb9be56f10..8e045138c8 100644 --- a/src/lib/browser.native.ts +++ b/src/lib/browser.native.ts @@ -1,3 +1,4 @@ export const isSafari = false export const isFirefox = false export const isTouchDevice = true +export const isAndroidWeb = false diff --git a/src/lib/browser.ts b/src/lib/browser.ts index d178a9a64e..08c43fbfdf 100644 --- a/src/lib/browser.ts +++ b/src/lib/browser.ts @@ -5,3 +5,5 @@ export const isSafari = /^((?!chrome|android).)*safari/i.test( export const isFirefox = /firefox|fxios/i.test(navigator.userAgent) export const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 1 +export const isAndroidWeb = + /android/i.test(navigator.userAgent) && isTouchDevice diff --git a/src/lib/generate-starterpack.ts b/src/lib/generate-starterpack.ts new file mode 100644 index 0000000000..64d30a954f --- /dev/null +++ b/src/lib/generate-starterpack.ts @@ -0,0 +1,164 @@ +import { + AppBskyActorDefs, + AppBskyGraphGetStarterPack, + BskyAgent, + Facet, +} from '@atproto/api' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useMutation} from '@tanstack/react-query' + +import {until} from 'lib/async/until' +import {sanitizeDisplayName} from 'lib/strings/display-names' +import {sanitizeHandle} from 'lib/strings/handles' +import {enforceLen} from 'lib/strings/helpers' +import {useAgent} from 'state/session' + +export const createStarterPackList = async ({ + name, + description, + descriptionFacets, + profiles, + agent, +}: { + name: string + description?: string + descriptionFacets?: Facet[] + profiles: AppBskyActorDefs.ProfileViewBasic[] + agent: BskyAgent +}): Promise<{uri: string; cid: string}> => { + if (profiles.length === 0) throw new Error('No profiles given') + + const list = await agent.app.bsky.graph.list.create( + {repo: agent.session!.did}, + { + name, + description, + descriptionFacets, + avatar: undefined, + createdAt: new Date().toISOString(), + purpose: 'app.bsky.graph.defs#referencelist', + }, + ) + if (!list) throw new Error('List creation failed') + await agent.com.atproto.repo.applyWrites({ + repo: agent.session!.did, + writes: [ + createListItem({did: agent.session!.did, listUri: list.uri}), + ].concat( + profiles + // Ensure we don't have ourselves in this list twice + .filter(p => p.did !== agent.session!.did) + .map(p => createListItem({did: p.did, listUri: list.uri})), + ), + }) + + return list +} + +export function useGenerateStarterPackMutation({ + onSuccess, + onError, +}: { + onSuccess: ({uri, cid}: {uri: string; cid: string}) => void + onError: (e: Error) => void +}) { + const {_} = useLingui() + const agent = useAgent() + const starterPackString = _(msg`Starter Pack`) + + return useMutation<{uri: string; cid: string}, Error, void>({ + mutationFn: async () => { + let profile: AppBskyActorDefs.ProfileViewBasic | undefined + let profiles: AppBskyActorDefs.ProfileViewBasic[] | undefined + + await Promise.all([ + (async () => { + profile = ( + await agent.app.bsky.actor.getProfile({ + actor: agent.session!.did, + }) + ).data + })(), + (async () => { + profiles = ( + await agent.app.bsky.actor.searchActors({ + q: encodeURIComponent('*'), + limit: 49, + }) + ).data.actors.filter(p => p.viewer?.following) + })(), + ]) + + if (!profile || !profiles) { + throw new Error('ERROR_DATA') + } + + // We include ourselves when we make the list + if (profiles.length < 7) { + throw new Error('NOT_ENOUGH_FOLLOWERS') + } + + const displayName = enforceLen( + profile.displayName + ? sanitizeDisplayName(profile.displayName) + : `@${sanitizeHandle(profile.handle)}`, + 25, + true, + ) + const starterPackName = `${displayName}'s ${starterPackString}` + + const list = await createStarterPackList({ + name: starterPackName, + profiles, + agent, + }) + + return await agent.app.bsky.graph.starterpack.create( + { + repo: agent.session!.did, + }, + { + name: starterPackName, + list: list.uri, + createdAt: new Date().toISOString(), + }, + ) + }, + onSuccess: async data => { + await whenAppViewReady(agent, data.uri, v => { + return typeof v?.data.starterPack.uri === 'string' + }) + onSuccess(data) + }, + onError: error => { + onError(error) + }, + }) +} + +function createListItem({did, listUri}: {did: string; listUri: string}) { + return { + $type: 'com.atproto.repo.applyWrites#create', + collection: 'app.bsky.graph.listitem', + value: { + $type: 'app.bsky.graph.listitem', + subject: did, + list: listUri, + createdAt: new Date().toISOString(), + }, + } +} + +async function whenAppViewReady( + agent: BskyAgent, + uri: string, + fn: (res?: AppBskyGraphGetStarterPack.Response) => boolean, +) { + await until( + 5, // 5 tries + 1e3, // 1s delay between tries + fn, + () => agent.app.bsky.graph.getStarterPack({starterPack: uri}), + ) +} diff --git a/src/lib/hooks/useBottomBarOffset.ts b/src/lib/hooks/useBottomBarOffset.ts new file mode 100644 index 0000000000..945c980620 --- /dev/null +++ b/src/lib/hooks/useBottomBarOffset.ts @@ -0,0 +1,14 @@ +import {useSafeAreaInsets} from 'react-native-safe-area-context' + +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {clamp} from 'lib/numbers' +import {isWeb} from 'platform/detection' + +export function useBottomBarOffset(modifier: number = 0) { + const {isTabletOrDesktop} = useWebMediaQueries() + const {bottom: bottomInset} = useSafeAreaInsets() + return ( + (isWeb && isTabletOrDesktop ? 0 : clamp(60 + bottomInset, 60, 75)) + + modifier + ) +} diff --git a/src/lib/hooks/useNotificationHandler.ts b/src/lib/hooks/useNotificationHandler.ts index 347062bebe..e4e7e14744 100644 --- a/src/lib/hooks/useNotificationHandler.ts +++ b/src/lib/hooks/useNotificationHandler.ts @@ -26,6 +26,7 @@ type NotificationReason = | 'reply' | 'quote' | 'chat-message' + | 'starterpack-joined' type NotificationPayload = | { @@ -142,6 +143,7 @@ export function useNotificationsHandler() { case 'mention': case 'quote': case 'reply': + case 'starterpack-joined': resetToTab('NotificationsTab') break // TODO implement these after we have an idea of how to handle each individual case diff --git a/src/lib/moderation/create-sanitized-display-name.ts b/src/lib/moderation/create-sanitized-display-name.ts new file mode 100644 index 0000000000..16135b2745 --- /dev/null +++ b/src/lib/moderation/create-sanitized-display-name.ts @@ -0,0 +1,21 @@ +import {AppBskyActorDefs} from '@atproto/api' + +import {sanitizeDisplayName} from 'lib/strings/display-names' +import {sanitizeHandle} from 'lib/strings/handles' + +export function createSanitizedDisplayName( + profile: + | AppBskyActorDefs.ProfileViewBasic + | AppBskyActorDefs.ProfileViewDetailed, + noAt = false, +) { + if (profile.displayName != null && profile.displayName !== '') { + return sanitizeDisplayName(profile.displayName) + } else { + let sanitizedHandle = sanitizeHandle(profile.handle) + if (!noAt) { + sanitizedHandle = `@${sanitizedHandle}` + } + return sanitizedHandle + } +} diff --git a/src/lib/moderation/useReportOptions.ts b/src/lib/moderation/useReportOptions.ts index 54b727b76b..91656857e4 100644 --- a/src/lib/moderation/useReportOptions.ts +++ b/src/lib/moderation/useReportOptions.ts @@ -13,6 +13,7 @@ interface ReportOptions { account: ReportOption[] post: ReportOption[] list: ReportOption[] + starterpack: ReportOption[] feedgen: ReportOption[] other: ReportOption[] convoMessage: ReportOption[] @@ -94,6 +95,14 @@ export function useReportOptions(): ReportOptions { }, ...common, ], + starterpack: [ + { + reason: ComAtprotoModerationDefs.REASONVIOLATION, + title: _(msg`Name or Description Violates Community Standards`), + description: _(msg`Terms used violate community standards`), + }, + ...common, + ], feedgen: [ { reason: ComAtprotoModerationDefs.REASONVIOLATION, diff --git a/src/lib/routes/links.ts b/src/lib/routes/links.ts index 9dfdab909b..56b716677b 100644 --- a/src/lib/routes/links.ts +++ b/src/lib/routes/links.ts @@ -1,3 +1,5 @@ +import {AppBskyGraphDefs, AtUri} from '@atproto/api' + import {isInvalidHandle} from 'lib/strings/handles' export function makeProfileLink( @@ -35,3 +37,18 @@ export function makeSearchLink(props: {query: string; from?: 'me' | string}) { props.query + (props.from ? ` from:${props.from}` : ''), )}` } + +export function makeStarterPackLink( + starterPackOrName: + | AppBskyGraphDefs.StarterPackViewBasic + | AppBskyGraphDefs.StarterPackView + | string, + rkey?: string, +) { + if (typeof starterPackOrName === 'string') { + return `https://bsky.app/start/${starterPackOrName}/${rkey}` + } else { + const uriRkey = new AtUri(starterPackOrName.uri).rkey + return `https://bsky.app/start/${starterPackOrName.creator.handle}/${uriRkey}` + } +} diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 403c2bb675..8a173b6756 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -42,6 +42,12 @@ export type CommonNavigatorParams = { MessagesConversation: {conversation: string; embed?: string} MessagesSettings: undefined Feeds: undefined + Start: {name: string; rkey: string} + StarterPack: {name: string; rkey: string; new?: boolean} + StarterPackWizard: undefined + StarterPackEdit: { + rkey?: string + } } export type BottomTabNavigatorParams = CommonNavigatorParams & { @@ -93,6 +99,12 @@ export type AllNavigatorParams = CommonNavigatorParams & { Hashtag: {tag: string; author?: string} MessagesTab: undefined Messages: {animation?: 'push' | 'pop'} + Start: {name: string; rkey: string} + StarterPack: {name: string; rkey: string; new?: boolean} + StarterPackWizard: undefined + StarterPackEdit: { + rkey?: string + } } // NOTE diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index 2e8cedb54b..07ed8c0ca7 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -53,7 +53,14 @@ export type LogEvents = { } 'onboarding:moderation:nextPressed': {} 'onboarding:profile:nextPressed': {} - 'onboarding:finished:nextPressed': {} + 'onboarding:finished:nextPressed': { + usedStarterPack: boolean + starterPackName?: string + starterPackCreator?: string + starterPackUri?: string + profilesFollowed: number + feedsPinned: number + } 'onboarding:finished:avatarResult': { avatarResult: 'default' | 'created' | 'uploaded' } @@ -61,7 +68,12 @@ export type LogEvents = { feedUrl: string feedType: string index: number - reason: 'focus' | 'tabbar-click' | 'pager-swipe' | 'desktop-sidebar-click' + reason: + | 'focus' + | 'tabbar-click' + | 'pager-swipe' + | 'desktop-sidebar-click' + | 'starter-pack-initial-feed' } 'feed:endReached:sampled': { feedUrl: string @@ -134,6 +146,7 @@ export type LogEvents = { | 'ProfileMenu' | 'ProfileHoverCard' | 'AvatarButton' + | 'StarterPackProfilesList' } 'profile:unfollow': { logContext: @@ -146,6 +159,7 @@ export type LogEvents = { | 'ProfileHoverCard' | 'Chat' | 'AvatarButton' + | 'StarterPackProfilesList' } 'chat:create': { logContext: 'ProfileHeader' | 'NewChatDialog' | 'SendViaChatDialog' @@ -157,6 +171,23 @@ export type LogEvents = { | 'ChatsList' | 'SendViaChatDialog' } + 'starterPack:share': { + starterPack: string + shareType: 'link' | 'qrcode' + qrShareType?: 'save' | 'copy' | 'share' + } + 'starterPack:followAll': { + logContext: 'StarterPackProfilesList' | 'Onboarding' + starterPack: string + count: number + } + 'starterPack:delete': {} + 'starterPack:create': { + setName: boolean + setDescription: boolean + profilesCount: number + feedsCount: number + } 'test:all:always': {} 'test:all:sometimes': {} diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index 46ef934ef6..bf2484ccb9 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -5,3 +5,4 @@ export type Gate = | 'request_notifications_permission_after_onboarding_v2' | 'show_avi_follow_button' | 'show_follow_back_label_v2' + | 'starter_packs_enabled' diff --git a/src/lib/strings/starter-pack.ts b/src/lib/strings/starter-pack.ts new file mode 100644 index 0000000000..489d0b9231 --- /dev/null +++ b/src/lib/strings/starter-pack.ts @@ -0,0 +1,101 @@ +import {AppBskyGraphDefs, AtUri} from '@atproto/api' + +export function createStarterPackLinkFromAndroidReferrer( + referrerQueryString: string, +): string | null { + try { + // The referrer string is just some URL parameters, so lets add them to a fake URL + const url = new URL('http://throwaway.com/?' + referrerQueryString) + const utmContent = url.searchParams.get('utm_content') + const utmSource = url.searchParams.get('utm_source') + + if (!utmContent) return null + if (utmSource !== 'bluesky') return null + + // This should be a string like `starterpack_haileyok.com_rkey` + const contentParts = utmContent.split('_') + + if (contentParts[0] !== 'starterpack') return null + if (contentParts.length !== 3) return null + + return `at://${contentParts[1]}/app.bsky.graph.starterpack/${contentParts[2]}` + } catch (e) { + return null + } +} + +export function parseStarterPackUri(uri?: string): { + name: string + rkey: string +} | null { + if (!uri) return null + + try { + if (uri.startsWith('at://')) { + const atUri = new AtUri(uri) + if (atUri.collection !== 'app.bsky.graph.starterpack') return null + if (atUri.rkey) { + return { + name: atUri.hostname, + rkey: atUri.rkey, + } + } + return null + } else { + const url = new URL(uri) + const parts = url.pathname.split('/') + const [_, path, name, rkey] = parts + + if (parts.length !== 4) return null + if (path !== 'starter-pack' && path !== 'start') return null + if (!name || !rkey) return null + return { + name, + rkey, + } + } + } catch (e) { + return null + } +} + +export function createStarterPackGooglePlayUri( + name: string, + rkey: string, +): string | null { + if (!name || !rkey) return null + return `https://play.google.com/store/apps/details?id=xyz.blueskyweb.app&referrer=utm_source%3Dbluesky%26utm_medium%3Dstarterpack%26utm_content%3Dstarterpack_${name}_${rkey}` +} + +export function httpStarterPackUriToAtUri(httpUri?: string): string | null { + if (!httpUri) return null + + const parsed = parseStarterPackUri(httpUri) + if (!parsed) return null + + if (httpUri.startsWith('at://')) return httpUri + + return `at://${parsed.name}/app.bsky.graph.starterpack/${parsed.rkey}` +} + +export function getStarterPackOgCard( + didOrStarterPack: AppBskyGraphDefs.StarterPackView | string, + rkey?: string, +) { + if (typeof didOrStarterPack === 'string') { + return `https://ogcard.cdn.bsky.app/start/${didOrStarterPack}/${rkey}` + } else { + const rkey = new AtUri(didOrStarterPack.uri).rkey + return `https://ogcard.cdn.bsky.app/start/${didOrStarterPack.creator.did}/${rkey}` + } +} + +export function createStarterPackUri({ + did, + rkey, +}: { + did: string + rkey: string +}): string | null { + return new AtUri(`at://${did}/app.bsky.graph.starterpack/${rkey}`).toString() +} diff --git a/src/routes.ts b/src/routes.ts index de711f5dc2..f241d37a07 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -41,4 +41,8 @@ export const router = new Router({ Messages: '/messages', MessagesSettings: '/messages/settings', MessagesConversation: '/messages/:conversation', + Start: '/start/:name/:rkey', + StarterPackEdit: '/starter-pack/edit/:rkey', + StarterPack: '/starter-pack/:name/:rkey', + StarterPackWizard: '/starter-pack/create', }) diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx index dfa10668b6..7cfd38e34f 100644 --- a/src/screens/Login/LoginForm.tsx +++ b/src/screens/Login/LoginForm.tsx @@ -21,6 +21,7 @@ import {logger} from '#/logger' import {useSessionApi} from '#/state/session' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useRequestNotificationsPermission} from 'lib/notifications/notifications' +import {useSetHasCheckedForStarterPack} from 'state/preferences/used-starter-packs' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {FormError} from '#/components/forms/FormError' @@ -69,6 +70,7 @@ export const LoginForm = ({ const {login} = useSessionApi() const requestNotificationsPermission = useRequestNotificationsPermission() const {setShowLoggedOut} = useLoggedOutViewControls() + const setHasCheckedForStarterPack = useSetHasCheckedForStarterPack() const onPressSelectService = React.useCallback(() => { Keyboard.dismiss() @@ -116,6 +118,7 @@ export const LoginForm = ({ 'LoginForm', ) setShowLoggedOut(false) + setHasCheckedForStarterPack(true) requestNotificationsPermission('Login') } catch (e: any) { const errMsg = e.toString() diff --git a/src/screens/Login/ScreenTransition.tsx b/src/screens/Login/ScreenTransition.tsx index ab0a223678..6fad266802 100644 --- a/src/screens/Login/ScreenTransition.tsx +++ b/src/screens/Login/ScreenTransition.tsx @@ -1,9 +1,16 @@ import React from 'react' +import {StyleProp, ViewStyle} from 'react-native' import Animated, {FadeInRight, FadeOutLeft} from 'react-native-reanimated' -export function ScreenTransition({children}: {children: React.ReactNode}) { +export function ScreenTransition({ + style, + children, +}: { + style?: StyleProp + children: React.ReactNode +}) { return ( - + {children} ) diff --git a/src/screens/Onboarding/StepFinished.tsx b/src/screens/Onboarding/StepFinished.tsx index c75dd4fa74..c7a459659f 100644 --- a/src/screens/Onboarding/StepFinished.tsx +++ b/src/screens/Onboarding/StepFinished.tsx @@ -1,11 +1,18 @@ import React from 'react' import {View} from 'react-native' +import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' +import {SavedFeed} from '@atproto/api/dist/client/types/app/bsky/actor/defs' +import {TID} from '@atproto/common-web' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' import {useAnalytics} from '#/lib/analytics/analytics' -import {BSKY_APP_ACCOUNT_DID} from '#/lib/constants' +import { + BSKY_APP_ACCOUNT_DID, + DISCOVER_SAVED_FEED, + TIMELINE_SAVED_FEED, +} from '#/lib/constants' import {logEvent} from '#/lib/statsig/statsig' import {logger} from '#/logger' import {preferencesQueryKey} from '#/state/queries/preferences' @@ -14,6 +21,11 @@ import {useAgent} from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' import {uploadBlob} from 'lib/api' import {useRequestNotificationsPermission} from 'lib/notifications/notifications' +import {useSetHasCheckedForStarterPack} from 'state/preferences/used-starter-packs' +import { + useActiveStarterPack, + useSetActiveStarterPack, +} from 'state/shell/starter-pack' import { DescriptionText, OnboardingControls, @@ -41,17 +53,74 @@ export function StepFinished() { const queryClient = useQueryClient() const agent = useAgent() const requestNotificationsPermission = useRequestNotificationsPermission() + const activeStarterPack = useActiveStarterPack() + const setActiveStarterPack = useSetActiveStarterPack() + const setHasCheckedForStarterPack = useSetHasCheckedForStarterPack() const finishOnboarding = React.useCallback(async () => { setSaving(true) - const {interestsStepResults, profileStepResults} = state - const {selectedInterests} = interestsStepResults + let starterPack: AppBskyGraphDefs.StarterPackView | undefined + let listItems: AppBskyGraphDefs.ListItemView[] | undefined + + if (activeStarterPack?.uri) { + try { + const spRes = await agent.app.bsky.graph.getStarterPack({ + starterPack: activeStarterPack.uri, + }) + starterPack = spRes.data.starterPack + + if (starterPack.list) { + const listRes = await agent.app.bsky.graph.getList({ + list: starterPack.list.uri, + limit: 50, + }) + listItems = listRes.data.items + } + } catch (e) { + logger.error('Failed to fetch starter pack', {safeMessage: e}) + // don't tell the user, just get them through onboarding. + } + } + try { + const {interestsStepResults, profileStepResults} = state + const {selectedInterests} = interestsStepResults + await Promise.all([ - bulkWriteFollows(agent, [BSKY_APP_ACCOUNT_DID]), + bulkWriteFollows(agent, [ + BSKY_APP_ACCOUNT_DID, + ...(listItems?.map(i => i.subject.did) ?? []), + ]), (async () => { + // Interests need to get saved first, then we can write the feeds to prefs await agent.setInterestsPref({tags: selectedInterests}) + + // Default feeds that every user should have pinned when landing in the app + const feedsToSave: SavedFeed[] = [ + { + ...DISCOVER_SAVED_FEED, + id: TID.nextStr(), + }, + { + ...TIMELINE_SAVED_FEED, + id: TID.nextStr(), + }, + ] + + // Any starter pack feeds will be pinned _after_ the defaults + if (starterPack && starterPack.feeds?.length) { + feedsToSave.concat( + starterPack.feeds.map(f => ({ + type: 'feed', + value: f.uri, + pinned: true, + id: TID.nextStr(), + })), + ) + } + + await agent.overwriteSavedFeeds(feedsToSave) })(), (async () => { const {imageUri, imageMime} = profileStepResults @@ -63,9 +132,24 @@ export function StepFinished() { if (res.data.blob) { existing.avatar = res.data.blob } + + if (starterPack) { + existing.joinedViaStarterPack = { + uri: starterPack.uri, + cid: starterPack.cid, + } + } + + existing.displayName = '' + // HACKFIX + // creating a bunch of identical profile objects is breaking the relay + // tossing this unspecced field onto it to reduce the size of the problem + // -prf + existing.createdAt = new Date().toISOString() return existing }) } + logEvent('onboarding:finished:avatarResult', { avatarResult: profileStepResults.isCreatedAvatar ? 'created' @@ -96,19 +180,40 @@ export function StepFinished() { }) setSaving(false) + setActiveStarterPack(undefined) + setHasCheckedForStarterPack(true) dispatch({type: 'finish'}) onboardDispatch({type: 'finish'}) track('OnboardingV2:StepFinished:End') track('OnboardingV2:Complete') - logEvent('onboarding:finished:nextPressed', {}) + logEvent('onboarding:finished:nextPressed', { + usedStarterPack: Boolean(starterPack), + starterPackName: AppBskyGraphStarterpack.isRecord(starterPack?.record) + ? starterPack.record.name + : undefined, + starterPackCreator: starterPack?.creator.did, + starterPackUri: starterPack?.uri, + profilesFollowed: listItems?.length ?? 0, + feedsPinned: starterPack?.feeds?.length ?? 0, + }) + if (starterPack && listItems?.length) { + logEvent('starterPack:followAll', { + logContext: 'Onboarding', + starterPack: starterPack.uri, + count: listItems?.length, + }) + } }, [ - state, queryClient, agent, dispatch, onboardDispatch, track, + activeStarterPack, + state, requestNotificationsPermission, + setActiveStarterPack, + setHasCheckedForStarterPack, ]) React.useEffect(() => { diff --git a/src/screens/Profile/Header/DisplayName.tsx b/src/screens/Profile/Header/DisplayName.tsx index b6d88db712..c63658a44a 100644 --- a/src/screens/Profile/Header/DisplayName.tsx +++ b/src/screens/Profile/Header/DisplayName.tsx @@ -1,10 +1,10 @@ import React from 'react' import {View} from 'react-native' import {AppBskyActorDefs, ModerationDecision} from '@atproto/api' -import {sanitizeHandle} from 'lib/strings/handles' -import {sanitizeDisplayName} from 'lib/strings/display-names' -import {Shadow} from '#/state/cache/types' +import {Shadow} from '#/state/cache/types' +import {sanitizeDisplayName} from 'lib/strings/display-names' +import {sanitizeHandle} from 'lib/strings/handles' import {atoms as a, useTheme} from '#/alf' import {Text} from '#/components/Typography' diff --git a/src/screens/Signup/index.tsx b/src/screens/Signup/index.tsx index 2cc1bcab0b..3203d443cc 100644 --- a/src/screens/Signup/index.tsx +++ b/src/screens/Signup/index.tsx @@ -1,6 +1,11 @@ import React from 'react' import {View} from 'react-native' -import {LayoutAnimationConfig} from 'react-native-reanimated' +import Animated, { + FadeIn, + FadeOut, + LayoutAnimationConfig, +} from 'react-native-reanimated' +import {AppBskyGraphStarterpack} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -11,6 +16,8 @@ import {createFullHandle} from '#/lib/strings/handles' import {logger} from '#/logger' import {useServiceQuery} from '#/state/queries/service' import {useAgent} from '#/state/session' +import {useStarterPackQuery} from 'state/queries/starter-packs' +import {useActiveStarterPack} from 'state/shell/starter-pack' import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout' import { initialState, @@ -26,6 +33,7 @@ import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' import {Button, ButtonText} from '#/components/Button' import {Divider} from '#/components/Divider' +import {LinearGradientBackground} from '#/components/LinearGradientBackground' import {InlineLinkText} from '#/components/Link' import {Text} from '#/components/Typography' @@ -38,6 +46,11 @@ export function Signup({onPressBack}: {onPressBack: () => void}) { const {gtMobile} = useBreakpoints() const agent = useAgent() + const activeStarterPack = useActiveStarterPack() + const {data: starterPack} = useStarterPackQuery({ + uri: activeStarterPack?.uri, + }) + const { data: serviceInfo, isFetching, @@ -142,6 +155,31 @@ export function Signup({onPressBack}: {onPressBack: () => void}) { description={_(msg`We're so excited to have you join us!`)} scrollable> + {state.activeStep === SignupStep.INFO && + starterPack && + AppBskyGraphStarterpack.isRecord(starterPack.record) ? ( + + + + {starterPack.record.name} + + + {starterPack.feeds?.length ? ( + + You'll follow the suggested users and feeds once you + finish creating your account! + + ) : ( + + You'll follow the suggested users once you finish creating + your account! + + )} + + + + ) : null} void +}) { + const moderationOpts = useModerationOpts() + const activeStarterPack = useActiveStarterPack() + + const {data: starterPack, isError: isErrorStarterPack} = useStarterPackQuery({ + uri: activeStarterPack?.uri, + }) + + const isValid = + starterPack && + starterPack.list && + AppBskyGraphDefs.validateStarterPackView(starterPack) && + AppBskyGraphStarterpack.validateRecord(starterPack.record) + + React.useEffect(() => { + if (isErrorStarterPack || (starterPack && !isValid)) { + setScreenState(LoggedOutScreenState.S_LoginOrCreateAccount) + } + }, [isErrorStarterPack, setScreenState, isValid, starterPack]) + + if (!starterPack || !isValid || !moderationOpts) { + return + } + + return ( + + ) +} + +function LandingScreenLoaded({ + starterPack, + setScreenState, + // TODO apply this to profile card + + moderationOpts, +}: { + starterPack: AppBskyGraphDefs.StarterPackView + setScreenState: (state: LoggedOutScreenState) => void + moderationOpts: ModerationOpts +}) { + const {record, creator, listItemsSample, feeds, joinedWeekCount} = starterPack + const {_} = useLingui() + const t = useTheme() + const activeStarterPack = useActiveStarterPack() + const setActiveStarterPack = useSetActiveStarterPack() + const {isTabletOrDesktop} = useWebMediaQueries() + const androidDialogControl = useDialogControl() + + const [appClipOverlayVisible, setAppClipOverlayVisible] = + React.useState(false) + + const listItemsCount = starterPack.list?.listItemCount ?? 0 + + const onContinue = () => { + setActiveStarterPack({ + uri: starterPack.uri, + }) + setScreenState(LoggedOutScreenState.S_CreateAccount) + } + + const onJoinPress = () => { + if (activeStarterPack?.isClip) { + setAppClipOverlayVisible(true) + postAppClipMessage({ + action: 'present', + }) + } else if (isAndroidWeb) { + androidDialogControl.open() + } else { + onContinue() + } + } + + const onJoinWithoutPress = () => { + if (activeStarterPack?.isClip) { + setAppClipOverlayVisible(true) + postAppClipMessage({ + action: 'present', + }) + } else { + setActiveStarterPack(undefined) + setScreenState(LoggedOutScreenState.S_CreateAccount) + } + } + + if (!AppBskyGraphStarterpack.isRecord(record)) { + return null + } + + return ( + + + + + + + + {record.name} + + + Starter pack by {`@${creator.handle}`} + + + + {record.description ? ( + + {record.description} + + ) : null} + + + {joinedWeekCount && joinedWeekCount >= 25 ? ( + + + + 123,659 joined this week + + + ) : null} + + + {Boolean(listItemsSample?.length) && ( + + + {listItemsCount <= 8 ? ( + You'll follow these people right away + ) : ( + + You'll follow these people and {listItemsCount - 8} others + + )} + + + {starterPack.listItemsSample?.slice(0, 8).map(item => ( + + + + ))} + + + )} + {feeds?.length ? ( + + + You'll stay updated with these feeds + + + + {feeds?.map(feed => ( + + + + ))} + + + ) : null} + + + + + + + + Download Bluesky + + + + The experience is better in the app. Download Bluesky now and we'll + pick back up where you left off. + + + + { + const rkey = new AtUri(starterPack.uri).rkey + if (!rkey) return + + const googlePlayUri = createStarterPackGooglePlayUri( + creator.handle, + rkey, + ) + if (!googlePlayUri) return + + window.location.href = googlePlayUri + }} + /> + + + + {isWeb && ( + + )} + + ) +} + +function AppClipOverlay({ + visible, + setIsVisible, +}: { + visible: boolean + setIsVisible: (visible: boolean) => void +}) { + if (!visible) return + + return ( + setIsVisible(false)}> + + {/* Webkit needs this to have a zindex of 2? */} + + + Download Bluesky to get started! + + + We'll remember the starter pack you chose and use it when you create + an account in the app. + + + + + ) +} diff --git a/src/screens/StarterPack/StarterPackScreen.tsx b/src/screens/StarterPack/StarterPackScreen.tsx new file mode 100644 index 0000000000..46ce252364 --- /dev/null +++ b/src/screens/StarterPack/StarterPackScreen.tsx @@ -0,0 +1,627 @@ +import React from 'react' +import {View} from 'react-native' +import {Image} from 'expo-image' +import { + AppBskyGraphDefs, + AppBskyGraphGetList, + AppBskyGraphStarterpack, + AtUri, + ModerationOpts, +} from '@atproto/api' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useNavigation} from '@react-navigation/native' +import {NativeStackScreenProps} from '@react-navigation/native-stack' +import { + InfiniteData, + UseInfiniteQueryResult, + useQueryClient, +} from '@tanstack/react-query' + +import {cleanError} from '#/lib/strings/errors' +import {logger} from '#/logger' +import {useDeleteStarterPackMutation} from '#/state/queries/starter-packs' +import {HITSLOP_20} from 'lib/constants' +import {makeProfileLink, makeStarterPackLink} from 'lib/routes/links' +import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types' +import {logEvent} from 'lib/statsig/statsig' +import {getStarterPackOgCard} from 'lib/strings/starter-pack' +import {isWeb} from 'platform/detection' +import {useModerationOpts} from 'state/preferences/moderation-opts' +import {RQKEY, useListMembersQuery} from 'state/queries/list-members' +import {useResolveDidQuery} from 'state/queries/resolve-uri' +import {useShortenLink} from 'state/queries/shorten-link' +import {useStarterPackQuery} from 'state/queries/starter-packs' +import {useAgent, useSession} from 'state/session' +import * as Toast from '#/view/com/util/Toast' +import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' +import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader' +import {CenteredView} from 'view/com/util/Views' +import {bulkWriteFollows} from '#/screens/Onboarding/util' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {useDialogControl} from '#/components/Dialog' +import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ArrowOutOfBox} from '#/components/icons/ArrowOutOfBox' +import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' +import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid' +import {Pencil_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil' +import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' +import {ListMaybePlaceholder} from '#/components/Lists' +import {Loader} from '#/components/Loader' +import * as Menu from '#/components/Menu' +import * as Prompt from '#/components/Prompt' +import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' +import {FeedsList} from '#/components/StarterPack/Main/FeedsList' +import {ProfilesList} from '#/components/StarterPack/Main/ProfilesList' +import {QrCodeDialog} from '#/components/StarterPack/QrCodeDialog' +import {ShareDialog} from '#/components/StarterPack/ShareDialog' +import {Text} from '#/components/Typography' + +type StarterPackScreeProps = NativeStackScreenProps< + CommonNavigatorParams, + 'StarterPack' +> + +export function StarterPackScreen({route}: StarterPackScreeProps) { + const {_} = useLingui() + const {currentAccount} = useSession() + + const {name, rkey} = route.params + const moderationOpts = useModerationOpts() + const { + data: did, + isLoading: isLoadingDid, + isError: isErrorDid, + } = useResolveDidQuery(name) + const { + data: starterPack, + isLoading: isLoadingStarterPack, + isError: isErrorStarterPack, + } = useStarterPackQuery({did, rkey}) + const listMembersQuery = useListMembersQuery(starterPack?.list?.uri, 50) + + const isValid = + starterPack && + (starterPack.list || starterPack?.creator?.did === currentAccount?.did) && + AppBskyGraphDefs.validateStarterPackView(starterPack) && + AppBskyGraphStarterpack.validateRecord(starterPack.record) + + if (!did || !starterPack || !isValid || !moderationOpts) { + return ( + + ) + } + + if (!starterPack.list && starterPack.creator.did === currentAccount?.did) { + return + } + + return ( + + ) +} + +function StarterPackScreenInner({ + starterPack, + routeParams, + listMembersQuery, + moderationOpts, +}: { + starterPack: AppBskyGraphDefs.StarterPackView + routeParams: StarterPackScreeProps['route']['params'] + listMembersQuery: UseInfiniteQueryResult< + InfiniteData + > + moderationOpts: ModerationOpts +}) { + const tabs = [ + ...(starterPack.list ? ['People'] : []), + ...(starterPack.feeds?.length ? ['Feeds'] : []), + ] + + const qrCodeDialogControl = useDialogControl() + const shareDialogControl = useDialogControl() + + const shortenLink = useShortenLink() + const [link, setLink] = React.useState() + const [imageLoaded, setImageLoaded] = React.useState(false) + + const onOpenShareDialog = React.useCallback(() => { + const rkey = new AtUri(starterPack.uri).rkey + shortenLink(makeStarterPackLink(starterPack.creator.did, rkey)).then( + res => { + setLink(res.url) + }, + ) + Image.prefetch(getStarterPackOgCard(starterPack)) + .then(() => { + setImageLoaded(true) + }) + .catch(() => { + setImageLoaded(true) + }) + shareDialogControl.open() + }, [shareDialogControl, shortenLink, starterPack]) + + React.useEffect(() => { + if (routeParams.new) { + onOpenShareDialog() + } + }, [onOpenShareDialog, routeParams.new, shareDialogControl]) + + return ( + + + ( +
+ )}> + {starterPack.list != null + ? ({headerHeight, scrollElRef}) => ( + + ) + : null} + {starterPack.feeds != null + ? ({headerHeight, scrollElRef}) => ( + + ) + : null} + + + + + + + ) +} + +function Header({ + starterPack, + routeParams, + onOpenShareDialog, +}: { + starterPack: AppBskyGraphDefs.StarterPackView + routeParams: StarterPackScreeProps['route']['params'] + onOpenShareDialog: () => void +}) { + const {_} = useLingui() + const t = useTheme() + const {currentAccount} = useSession() + const agent = useAgent() + const queryClient = useQueryClient() + + const [isProcessing, setIsProcessing] = React.useState(false) + + const {record, creator} = starterPack + const isOwn = creator?.did === currentAccount?.did + const joinedAllTimeCount = starterPack.joinedAllTimeCount ?? 0 + + const onFollowAll = async () => { + if (!starterPack.list) return + + setIsProcessing(true) + + try { + const list = await agent.app.bsky.graph.getList({ + list: starterPack.list.uri, + }) + const dids = list.data.items + .filter(li => !li.subject.viewer?.following) + .map(li => li.subject.did) + + await bulkWriteFollows(agent, dids) + + await queryClient.refetchQueries({ + queryKey: RQKEY(starterPack.list.uri), + }) + + logEvent('starterPack:followAll', { + logContext: 'StarterPackProfilesList', + starterPack: starterPack.uri, + count: dids.length, + }) + Toast.show(_(msg`All accounts have been followed!`)) + } catch (e) { + Toast.show(_(msg`An error occurred while trying to follow all`)) + } finally { + setIsProcessing(false) + } + } + + if (!AppBskyGraphStarterpack.isRecord(record)) { + return null + } + + return ( + <> + + + {isOwn ? ( + + ) : ( + + )} + + + + {record.description || joinedAllTimeCount >= 25 ? ( + + {record.description ? ( + + {record.description} + + ) : null} + {joinedAllTimeCount >= 25 ? ( + + + + + {starterPack.joinedAllTimeCount || 0} people have used this + starter pack! + + + + ) : null} + + ) : null} + + ) +} + +function OverflowMenu({ + starterPack, + routeParams, + onOpenShareDialog, +}: { + starterPack: AppBskyGraphDefs.StarterPackView + routeParams: StarterPackScreeProps['route']['params'] + onOpenShareDialog: () => void +}) { + const t = useTheme() + const {_} = useLingui() + const {gtMobile} = useBreakpoints() + const {currentAccount} = useSession() + const reportDialogControl = useReportDialogControl() + const deleteDialogControl = useDialogControl() + const navigation = useNavigation() + + const { + mutate: deleteStarterPack, + isPending: isDeletePending, + error: deleteError, + } = useDeleteStarterPackMutation({ + onSuccess: () => { + logEvent('starterPack:delete', {}) + deleteDialogControl.close(() => { + if (navigation.canGoBack()) { + navigation.popToTop() + } else { + navigation.navigate('Home') + } + }) + }, + onError: e => { + logger.error('Failed to delete starter pack', {safeMessage: e}) + }, + }) + + const isOwn = starterPack.creator.did === currentAccount?.did + + const onDeleteStarterPack = async () => { + if (!starterPack.list) { + logger.error(`Unable to delete starterpack because list is missing`) + return + } + + deleteStarterPack({ + rkey: routeParams.rkey, + listUri: starterPack.list.uri, + }) + logEvent('starterPack:delete', {}) + } + + return ( + <> + + + {({props}) => ( + + )} + + + {isOwn ? ( + <> + { + navigation.navigate('StarterPackEdit', { + rkey: routeParams.rkey, + }) + }}> + + Edit + + + + { + deleteDialogControl.open() + }}> + + Delete + + + + + ) : ( + <> + + + + Share link + + + + + + + + Report starter pack + + + + + )} + + + + {starterPack.list && ( + + )} + + + + Delete starter pack? + + + Are you sure you want delete this starter pack? + + {deleteError && ( + + + + Unable to delete + + {cleanError(deleteError)} + + + + )} + + + + + + + ) +} + +function InvalidStarterPack({rkey}: {rkey: string}) { + const {_} = useLingui() + const t = useTheme() + const navigation = useNavigation() + const {gtMobile} = useBreakpoints() + const [isProcessing, setIsProcessing] = React.useState(false) + + const goBack = () => { + if (navigation.canGoBack()) { + navigation.goBack() + } else { + navigation.replace('Home') + } + } + + const {mutate: deleteStarterPack} = useDeleteStarterPackMutation({ + onSuccess: () => { + setIsProcessing(false) + goBack() + }, + onError: e => { + setIsProcessing(false) + logger.error('Failed to delete invalid starter pack', {safeMessage: e}) + Toast.show(_(msg`Failed to delete starter pack`)) + }, + }) + + return ( + + + + Starter pack is invalid + + + + The starter pack that you are trying to view is invalid. You may + delete this starter pack instead. + + + + + + + + + ) +} diff --git a/src/screens/StarterPack/Wizard/State.tsx b/src/screens/StarterPack/Wizard/State.tsx new file mode 100644 index 0000000000..ea9bbf9d33 --- /dev/null +++ b/src/screens/StarterPack/Wizard/State.tsx @@ -0,0 +1,163 @@ +import React from 'react' +import { + AppBskyActorDefs, + AppBskyGraphDefs, + AppBskyGraphStarterpack, +} from '@atproto/api' +import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' +import {msg} from '@lingui/macro' + +import {useSession} from 'state/session' +import * as Toast from '#/view/com/util/Toast' + +const steps = ['Details', 'Profiles', 'Feeds'] as const +type Step = (typeof steps)[number] + +type Action = + | {type: 'Next'} + | {type: 'Back'} + | {type: 'SetCanNext'; canNext: boolean} + | {type: 'SetName'; name: string} + | {type: 'SetDescription'; description: string} + | {type: 'AddProfile'; profile: AppBskyActorDefs.ProfileViewBasic} + | {type: 'RemoveProfile'; profileDid: string} + | {type: 'AddFeed'; feed: GeneratorView} + | {type: 'RemoveFeed'; feedUri: string} + | {type: 'SetProcessing'; processing: boolean} + | {type: 'SetError'; error: string} + +interface State { + canNext: boolean + currentStep: Step + name?: string + description?: string + profiles: AppBskyActorDefs.ProfileViewBasic[] + feeds: GeneratorView[] + processing: boolean + error?: string + transitionDirection: 'Backward' | 'Forward' +} + +type TStateContext = [State, (action: Action) => void] + +const StateContext = React.createContext([ + {} as State, + (_: Action) => {}, +]) +export const useWizardState = () => React.useContext(StateContext) + +function reducer(state: State, action: Action): State { + let updatedState = state + + // -- Navigation + const currentIndex = steps.indexOf(state.currentStep) + if (action.type === 'Next' && state.currentStep !== 'Feeds') { + updatedState = { + ...state, + currentStep: steps[currentIndex + 1], + transitionDirection: 'Forward', + } + } else if (action.type === 'Back' && state.currentStep !== 'Details') { + updatedState = { + ...state, + currentStep: steps[currentIndex - 1], + transitionDirection: 'Backward', + } + } + + switch (action.type) { + case 'SetName': + updatedState = {...state, name: action.name.slice(0, 50)} + break + case 'SetDescription': + updatedState = {...state, description: action.description} + break + case 'AddProfile': + if (state.profiles.length >= 51) { + Toast.show(msg`You may only add up to 50 profiles`.message ?? '') + } else { + updatedState = {...state, profiles: [...state.profiles, action.profile]} + } + break + case 'RemoveProfile': + updatedState = { + ...state, + profiles: state.profiles.filter( + profile => profile.did !== action.profileDid, + ), + } + break + case 'AddFeed': + if (state.feeds.length >= 50) { + Toast.show(msg`You may only add up to 50 feeds`.message ?? '') + } else { + updatedState = {...state, feeds: [...state.feeds, action.feed]} + } + break + case 'RemoveFeed': + updatedState = { + ...state, + feeds: state.feeds.filter(f => f.uri !== action.feedUri), + } + break + case 'SetProcessing': + updatedState = {...state, processing: action.processing} + break + } + + return updatedState +} + +// TODO supply the initial state to this component +export function Provider({ + starterPack, + listItems, + children, +}: { + starterPack?: AppBskyGraphDefs.StarterPackView + listItems?: AppBskyGraphDefs.ListItemView[] + children: React.ReactNode +}) { + const {currentAccount} = useSession() + + const createInitialState = (): State => { + if (starterPack && AppBskyGraphStarterpack.isRecord(starterPack.record)) { + return { + canNext: true, + currentStep: 'Details', + name: starterPack.record.name, + description: starterPack.record.description, + profiles: + listItems + ?.map(i => i.subject) + .filter(p => p.did !== currentAccount?.did) ?? [], + feeds: starterPack.feeds ?? [], + processing: false, + transitionDirection: 'Forward', + } + } + + return { + canNext: true, + currentStep: 'Details', + profiles: [], + feeds: [], + processing: false, + transitionDirection: 'Forward', + } + } + + const [state, dispatch] = React.useReducer(reducer, null, createInitialState) + + return ( + + {children} + + ) +} + +export { + type Action as WizardAction, + type State as WizardState, + type Step as WizardStep, +} diff --git a/src/screens/StarterPack/Wizard/StepDetails.tsx b/src/screens/StarterPack/Wizard/StepDetails.tsx new file mode 100644 index 0000000000..24c992c60b --- /dev/null +++ b/src/screens/StarterPack/Wizard/StepDetails.tsx @@ -0,0 +1,84 @@ +import React from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useProfileQuery} from 'state/queries/profile' +import {useSession} from 'state/session' +import {useWizardState} from '#/screens/StarterPack/Wizard/State' +import {atoms as a, useTheme} from '#/alf' +import * as TextField from '#/components/forms/TextField' +import {StarterPack} from '#/components/icons/StarterPack' +import {ScreenTransition} from '#/components/StarterPack/Wizard/ScreenTransition' +import {Text} from '#/components/Typography' + +export function StepDetails() { + const {_} = useLingui() + const t = useTheme() + const [state, dispatch] = useWizardState() + + const {currentAccount} = useSession() + const {data: currentProfile} = useProfileQuery({ + did: currentAccount?.did, + staleTime: 300, + }) + + return ( + + + + + + Invites, but personal + + + + Invite your friends to follow your favorite feeds and people + + + + + + What do you want to call your starter pack? + + + dispatch({type: 'SetName', name: text})} + /> + + + {state.name?.length ?? 0}/50 + + + + + + + Tell us a little more + + + + dispatch({type: 'SetDescription', description: text}) + } + multiline + style={{minHeight: 150}} + /> + + + + + ) +} diff --git a/src/screens/StarterPack/Wizard/StepFeeds.tsx b/src/screens/StarterPack/Wizard/StepFeeds.tsx new file mode 100644 index 0000000000..6752a95db3 --- /dev/null +++ b/src/screens/StarterPack/Wizard/StepFeeds.tsx @@ -0,0 +1,113 @@ +import React, {useState} from 'react' +import {ListRenderItemInfo, View} from 'react-native' +import {KeyboardAwareScrollView} from 'react-native-keyboard-controller' +import {AppBskyFeedDefs, ModerationOpts} from '@atproto/api' +import {Trans} from '@lingui/macro' + +import {useA11y} from '#/state/a11y' +import {DISCOVER_FEED_URI} from 'lib/constants' +import { + useGetPopularFeedsQuery, + useSavedFeeds, + useSearchPopularFeedsQuery, +} from 'state/queries/feed' +import {SearchInput} from 'view/com/util/forms/SearchInput' +import {List} from 'view/com/util/List' +import {useWizardState} from '#/screens/StarterPack/Wizard/State' +import {atoms as a, useTheme} from '#/alf' +import {useThrottledValue} from '#/components/hooks/useThrottledValue' +import {Loader} from '#/components/Loader' +import {ScreenTransition} from '#/components/StarterPack/Wizard/ScreenTransition' +import {WizardFeedCard} from '#/components/StarterPack/Wizard/WizardListCard' +import {Text} from '#/components/Typography' + +function keyExtractor(item: AppBskyFeedDefs.GeneratorView) { + return item.uri +} + +export function StepFeeds({moderationOpts}: {moderationOpts: ModerationOpts}) { + const t = useTheme() + const [state, dispatch] = useWizardState() + const [query, setQuery] = useState('') + const throttledQuery = useThrottledValue(query, 500) + const {screenReaderEnabled} = useA11y() + + const {data: savedFeedsAndLists} = useSavedFeeds() + const savedFeeds = savedFeedsAndLists?.feeds + .filter(f => f.type === 'feed' && f.view.uri !== DISCOVER_FEED_URI) + .map(f => f.view) as AppBskyFeedDefs.GeneratorView[] + + const {data: popularFeedsPages, fetchNextPage} = useGetPopularFeedsQuery({ + limit: 30, + }) + const popularFeeds = + popularFeedsPages?.pages + .flatMap(page => page.feeds) + .filter(f => !savedFeeds?.some(sf => sf?.uri === f.uri)) ?? [] + + const suggestedFeeds = savedFeeds?.concat(popularFeeds) + + const {data: searchedFeeds, isLoading: isLoadingSearch} = + useSearchPopularFeedsQuery({q: throttledQuery}) + + const renderItem = ({ + item, + }: ListRenderItemInfo) => { + return ( + + ) + } + + return ( + + + + setQuery(t)} + onPressCancelSearch={() => setQuery('')} + onSubmitQuery={() => {}} + /> + + + fetchNextPage() : undefined + } + onEndReachedThreshold={2} + renderScrollComponent={props => } + keyboardShouldPersistTaps="handled" + containWeb={true} + sideBorders={false} + style={{flex: 1}} + ListEmptyComponent={ + + {isLoadingSearch ? ( + + ) : ( + + No feeds found. Try searching for something else. + + )} + + } + /> + + ) +} diff --git a/src/screens/StarterPack/Wizard/StepFinished.tsx b/src/screens/StarterPack/Wizard/StepFinished.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/screens/StarterPack/Wizard/StepProfiles.tsx b/src/screens/StarterPack/Wizard/StepProfiles.tsx new file mode 100644 index 0000000000..8fe7f52fed --- /dev/null +++ b/src/screens/StarterPack/Wizard/StepProfiles.tsx @@ -0,0 +1,101 @@ +import React, {useState} from 'react' +import {ListRenderItemInfo, View} from 'react-native' +import {KeyboardAwareScrollView} from 'react-native-keyboard-controller' +import {AppBskyActorDefs, ModerationOpts} from '@atproto/api' +import {Trans} from '@lingui/macro' + +import {useA11y} from '#/state/a11y' +import {isNative} from 'platform/detection' +import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete' +import {useActorSearchPaginated} from 'state/queries/actor-search' +import {SearchInput} from 'view/com/util/forms/SearchInput' +import {List} from 'view/com/util/List' +import {useWizardState} from '#/screens/StarterPack/Wizard/State' +import {atoms as a, useTheme} from '#/alf' +import {Loader} from '#/components/Loader' +import {ScreenTransition} from '#/components/StarterPack/Wizard/ScreenTransition' +import {WizardProfileCard} from '#/components/StarterPack/Wizard/WizardListCard' +import {Text} from '#/components/Typography' + +function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic) { + return item?.did ?? '' +} + +export function StepProfiles({ + moderationOpts, +}: { + moderationOpts: ModerationOpts +}) { + const t = useTheme() + const [state, dispatch] = useWizardState() + const [query, setQuery] = useState('') + const {screenReaderEnabled} = useA11y() + + const {data: topPages, fetchNextPage} = useActorSearchPaginated({ + query: encodeURIComponent('*'), + }) + const topFollowers = topPages?.pages.flatMap(p => p.actors) + + const {data: results, isLoading: isLoadingResults} = + useActorAutocompleteQuery(query, true, 12) + + const renderItem = ({ + item, + }: ListRenderItemInfo) => { + return ( + + ) + } + + return ( + + + + setQuery('')} + onSubmitQuery={() => {}} + /> + + + } + keyboardShouldPersistTaps="handled" + containWeb={true} + sideBorders={false} + style={[a.flex_1]} + onEndReached={ + !query && !screenReaderEnabled ? () => fetchNextPage() : undefined + } + onEndReachedThreshold={isNative ? 2 : 0.25} + ListEmptyComponent={ + + {isLoadingResults ? ( + + ) : ( + + Nobody was found. Try searching for someone else. + + )} + + } + /> + + ) +} diff --git a/src/screens/StarterPack/Wizard/index.tsx b/src/screens/StarterPack/Wizard/index.tsx new file mode 100644 index 0000000000..76691dc985 --- /dev/null +++ b/src/screens/StarterPack/Wizard/index.tsx @@ -0,0 +1,575 @@ +import React from 'react' +import {Keyboard, TouchableOpacity, View} from 'react-native' +import { + KeyboardAwareScrollView, + useKeyboardController, +} from 'react-native-keyboard-controller' +import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {Image} from 'expo-image' +import { + AppBskyActorDefs, + AppBskyGraphDefs, + AtUri, + ModerationOpts, +} from '@atproto/api' +import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {msg, Plural, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useFocusEffect, useNavigation} from '@react-navigation/native' +import {NativeStackScreenProps} from '@react-navigation/native-stack' + +import {logger} from '#/logger' +import {HITSLOP_10} from 'lib/constants' +import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types' +import {logEvent} from 'lib/statsig/statsig' +import {sanitizeDisplayName} from 'lib/strings/display-names' +import {sanitizeHandle} from 'lib/strings/handles' +import {enforceLen} from 'lib/strings/helpers' +import { + getStarterPackOgCard, + parseStarterPackUri, +} from 'lib/strings/starter-pack' +import {isAndroid, isNative, isWeb} from 'platform/detection' +import {useModerationOpts} from 'state/preferences/moderation-opts' +import {useListMembersQuery} from 'state/queries/list-members' +import {useProfileQuery} from 'state/queries/profile' +import { + useCreateStarterPackMutation, + useEditStarterPackMutation, + useStarterPackQuery, +} from 'state/queries/starter-packs' +import {useSession} from 'state/session' +import {useSetMinimalShellMode} from 'state/shell' +import * as Toast from '#/view/com/util/Toast' +import {UserAvatar} from 'view/com/util/UserAvatar' +import {CenteredView} from 'view/com/util/Views' +import {useWizardState, WizardStep} from '#/screens/StarterPack/Wizard/State' +import {StepDetails} from '#/screens/StarterPack/Wizard/StepDetails' +import {StepFeeds} from '#/screens/StarterPack/Wizard/StepFeeds' +import {StepProfiles} from '#/screens/StarterPack/Wizard/StepProfiles' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import {useDialogControl} from '#/components/Dialog' +import {ListMaybePlaceholder} from '#/components/Lists' +import {Loader} from '#/components/Loader' +import {WizardEditListDialog} from '#/components/StarterPack/Wizard/WizardEditListDialog' +import {Text} from '#/components/Typography' +import {Provider} from './State' + +export function Wizard({ + route, +}: NativeStackScreenProps< + CommonNavigatorParams, + 'StarterPackEdit' | 'StarterPackWizard' +>) { + const {rkey} = route.params ?? {} + const {currentAccount} = useSession() + const moderationOpts = useModerationOpts() + + const {_} = useLingui() + + const { + data: starterPack, + isLoading: isLoadingStarterPack, + isError: isErrorStarterPack, + } = useStarterPackQuery({did: currentAccount!.did, rkey}) + const listUri = starterPack?.list?.uri + + const { + data: profilesData, + isLoading: isLoadingProfiles, + isError: isErrorProfiles, + } = useListMembersQuery(listUri, 50) + const listItems = profilesData?.pages.flatMap(p => p.items) + + const { + data: profile, + isLoading: isLoadingProfile, + isError: isErrorProfile, + } = useProfileQuery({did: currentAccount?.did}) + + const isEdit = Boolean(rkey) + const isReady = + (!isEdit || (isEdit && starterPack && listItems)) && + profile && + moderationOpts + + if (!isReady) { + return ( + + ) + } else if (isEdit && starterPack?.creator.did !== currentAccount?.did) { + return ( + + ) + } + + return ( + + + + ) +} + +function WizardInner({ + currentStarterPack, + currentListItems, + profile, + moderationOpts, +}: { + currentStarterPack?: AppBskyGraphDefs.StarterPackView + currentListItems?: AppBskyGraphDefs.ListItemView[] + profile: AppBskyActorDefs.ProfileViewBasic + moderationOpts: ModerationOpts +}) { + const navigation = useNavigation() + const {_} = useLingui() + const t = useTheme() + const setMinimalShellMode = useSetMinimalShellMode() + const {setEnabled} = useKeyboardController() + const [state, dispatch] = useWizardState() + const {currentAccount} = useSession() + const {data: currentProfile} = useProfileQuery({ + did: currentAccount?.did, + staleTime: 0, + }) + const parsed = parseStarterPackUri(currentStarterPack?.uri) + + React.useEffect(() => { + navigation.setOptions({ + gestureEnabled: false, + }) + }, [navigation]) + + useFocusEffect( + React.useCallback(() => { + setEnabled(true) + setMinimalShellMode(true) + + return () => { + setMinimalShellMode(false) + setEnabled(false) + } + }, [setMinimalShellMode, setEnabled]), + ) + + const getDefaultName = () => { + let displayName + if ( + currentProfile?.displayName != null && + currentProfile?.displayName !== '' + ) { + displayName = sanitizeDisplayName(currentProfile.displayName) + } else { + displayName = sanitizeHandle(currentProfile!.handle) + } + return _(msg`${displayName}'s Starter Pack`).slice(0, 50) + } + + const wizardUiStrings: Record< + WizardStep, + {header: string; nextBtn: string; subtitle?: string} + > = { + Details: { + header: _(msg`Starter Pack`), + nextBtn: _(msg`Next`), + }, + Profiles: { + header: _(msg`People`), + nextBtn: _(msg`Next`), + subtitle: _( + msg`Add people to your starter pack that you think others will enjoy following`, + ), + }, + Feeds: { + header: _(msg`Feeds`), + nextBtn: state.feeds.length === 0 ? _(msg`Skip`) : _(msg`Finish`), + subtitle: _(msg`Some subtitle`), + }, + } + const currUiStrings = wizardUiStrings[state.currentStep] + + const onSuccessCreate = (data: {uri: string; cid: string}) => { + const rkey = new AtUri(data.uri).rkey + logEvent('starterPack:create', { + setName: state.name != null, + setDescription: state.description != null, + profilesCount: state.profiles.length, + feedsCount: state.feeds.length, + }) + Image.prefetch([getStarterPackOgCard(currentProfile!.did, rkey)]) + dispatch({type: 'SetProcessing', processing: false}) + navigation.replace('StarterPack', { + name: currentAccount!.handle, + rkey, + new: true, + }) + } + + const onSuccessEdit = () => { + if (navigation.canGoBack()) { + navigation.goBack() + } else { + navigation.replace('StarterPack', { + name: currentAccount!.handle, + rkey: parsed!.rkey, + }) + } + } + + const {mutate: createStarterPack} = useCreateStarterPackMutation({ + onSuccess: onSuccessCreate, + onError: e => { + logger.error('Failed to create starter pack', {safeMessage: e}) + dispatch({type: 'SetProcessing', processing: false}) + Toast.show(_(msg`Failed to create starter pack`)) + }, + }) + const {mutate: editStarterPack} = useEditStarterPackMutation({ + onSuccess: onSuccessEdit, + onError: e => { + logger.error('Failed to edit starter pack', {safeMessage: e}) + dispatch({type: 'SetProcessing', processing: false}) + Toast.show(_(msg`Failed to create starter pack`)) + }, + }) + + const submit = async () => { + dispatch({type: 'SetProcessing', processing: true}) + if (currentStarterPack && currentListItems) { + editStarterPack({ + name: state.name ?? getDefaultName(), + description: state.description, + descriptionFacets: [], + profiles: state.profiles, + feeds: state.feeds, + currentStarterPack: currentStarterPack, + currentListItems: currentListItems, + }) + } else { + createStarterPack({ + name: state.name ?? getDefaultName(), + description: state.description, + descriptionFacets: [], + profiles: state.profiles, + feeds: state.feeds, + }) + } + } + + const onNext = () => { + if (state.currentStep === 'Feeds') { + submit() + return + } + + const keyboardVisible = Keyboard.isVisible() + Keyboard.dismiss() + setTimeout( + () => { + dispatch({type: 'Next'}) + }, + keyboardVisible ? 16 : 0, + ) + } + + return ( + + + + { + if (state.currentStep === 'Details') { + navigation.pop() + } else { + dispatch({type: 'Back'}) + } + }}> + + + + + {currUiStrings.header} + + + + + + {state.currentStep === 'Details' ? ( + + ) : state.currentStep === 'Profiles' ? ( + + ) : state.currentStep === 'Feeds' ? ( + + ) : null} + + + {state.currentStep !== 'Details' && ( +