diff options
Diffstat (limited to 'src/lib')
-rw-r--r-- | src/lib/hooks/useInitialNumToRender.ts | 11 | ||||
-rw-r--r-- | src/lib/routes/router.ts | 10 | ||||
-rw-r--r-- | src/lib/statsig/statsig.tsx | 75 | ||||
-rw-r--r-- | src/lib/statsig/statsig.web.tsx | 75 | ||||
-rw-r--r-- | src/lib/strings/url-helpers.ts | 34 |
5 files changed, 200 insertions, 5 deletions
diff --git a/src/lib/hooks/useInitialNumToRender.ts b/src/lib/hooks/useInitialNumToRender.ts new file mode 100644 index 000000000..942f0404a --- /dev/null +++ b/src/lib/hooks/useInitialNumToRender.ts @@ -0,0 +1,11 @@ +import React from 'react' +import {Dimensions} from 'react-native' + +const MIN_POST_HEIGHT = 100 + +export function useInitialNumToRender(minItemHeight: number = MIN_POST_HEIGHT) { + return React.useMemo(() => { + const screenHeight = Dimensions.get('window').height + return Math.ceil(screenHeight / minItemHeight) + 1 + }, [minItemHeight]) +} diff --git a/src/lib/routes/router.ts b/src/lib/routes/router.ts index 00defaeda..8c8be3739 100644 --- a/src/lib/routes/router.ts +++ b/src/lib/routes/router.ts @@ -2,9 +2,15 @@ import {RouteParams, Route} from './types' export class Router { routes: [string, Route][] = [] - constructor(description: Record<string, string>) { + constructor(description: Record<string, string | string[]>) { for (const [screen, pattern] of Object.entries(description)) { - this.routes.push([screen, createRoute(pattern)]) + if (typeof pattern === 'string') { + this.routes.push([screen, createRoute(pattern)]) + } else { + pattern.forEach(subPattern => { + this.routes.push([screen, createRoute(subPattern)]) + }) + } } } diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx new file mode 100644 index 000000000..6d9ebeb09 --- /dev/null +++ b/src/lib/statsig/statsig.tsx @@ -0,0 +1,75 @@ +import React from 'react' +import { + Statsig, + StatsigProvider, + useGate as useStatsigGate, +} from 'statsig-react-native-expo' +import {useSession} from '../../state/session' +import {sha256} from 'js-sha256' + +const statsigOptions = { + environment: { + tier: process.env.NODE_ENV === 'development' ? 'development' : 'production', + }, + // Don't block on waiting for network. The fetched config will kick in on next load. + // This ensures the UI is always consistent and doesn't update mid-session. + // Note this makes cold load (no local storage) and private mode return `false` for all gates. + initTimeoutMs: 1, +} + +export function logEvent( + eventName: string, + value?: string | number | null, + metadata?: Record<string, string> | null, +) { + Statsig.logEvent(eventName, value, metadata) +} + +export function useGate(gateName: string) { + const {isLoading, value} = useStatsigGate(gateName) + if (isLoading) { + // This should not happen because of waitForInitialization={true}. + console.error('Did not expected isLoading to ever be true.') + } + return value +} + +function toStatsigUser(did: string | undefined) { + let userID: string | undefined + if (did) { + userID = sha256(did) + } + return {userID} +} + +export function Provider({children}: {children: React.ReactNode}) { + const {currentAccount} = useSession() + const currentStatsigUser = React.useMemo( + () => toStatsigUser(currentAccount?.did), + [currentAccount?.did], + ) + + React.useEffect(() => { + function refresh() { + // Intentionally refetching the config using the JS SDK rather than React SDK + // so that the new config is stored in cache but isn't used during this session. + // It will kick in for the next reload. + Statsig.updateUser(currentStatsigUser) + } + const id = setInterval(refresh, 3 * 60e3 /* 3 min */) + return () => clearInterval(id) + }, [currentStatsigUser]) + + return ( + <StatsigProvider + sdkKey="client-SXJakO39w9vIhl3D44u8UupyzFl4oZ2qPIkjwcvuPsV" + mountKey={currentStatsigUser.userID} + user={currentStatsigUser} + // This isn't really blocking due to short initTimeoutMs above. + // However, it ensures `isLoading` is always `false`. + waitForInitialization={true} + options={statsigOptions}> + {children} + </StatsigProvider> + ) +} diff --git a/src/lib/statsig/statsig.web.tsx b/src/lib/statsig/statsig.web.tsx new file mode 100644 index 000000000..d1c912019 --- /dev/null +++ b/src/lib/statsig/statsig.web.tsx @@ -0,0 +1,75 @@ +import React from 'react' +import { + Statsig, + StatsigProvider, + useGate as useStatsigGate, +} from 'statsig-react' +import {useSession} from '../../state/session' +import {sha256} from 'js-sha256' + +const statsigOptions = { + environment: { + tier: process.env.NODE_ENV === 'development' ? 'development' : 'production', + }, + // Don't block on waiting for network. The fetched config will kick in on next load. + // This ensures the UI is always consistent and doesn't update mid-session. + // Note this makes cold load (no local storage) and private mode return `false` for all gates. + initTimeoutMs: 1, +} + +export function logEvent( + eventName: string, + value?: string | number | null, + metadata?: Record<string, string> | null, +) { + Statsig.logEvent(eventName, value, metadata) +} + +export function useGate(gateName: string) { + const {isLoading, value} = useStatsigGate(gateName) + if (isLoading) { + // This should not happen because of waitForInitialization={true}. + console.error('Did not expected isLoading to ever be true.') + } + return value +} + +function toStatsigUser(did: string | undefined) { + let userID: string | undefined + if (did) { + userID = sha256(did) + } + return {userID} +} + +export function Provider({children}: {children: React.ReactNode}) { + const {currentAccount} = useSession() + const currentStatsigUser = React.useMemo( + () => toStatsigUser(currentAccount?.did), + [currentAccount?.did], + ) + + React.useEffect(() => { + function refresh() { + // Intentionally refetching the config using the JS SDK rather than React SDK + // so that the new config is stored in cache but isn't used during this session. + // It will kick in for the next reload. + Statsig.updateUser(currentStatsigUser) + } + const id = setInterval(refresh, 3 * 60e3 /* 3 min */) + return () => clearInterval(id) + }, [currentStatsigUser]) + + return ( + <StatsigProvider + sdkKey="client-SXJakO39w9vIhl3D44u8UupyzFl4oZ2qPIkjwcvuPsV" + mountKey={currentStatsigUser.userID} + user={currentStatsigUser} + // This isn't really blocking due to short initTimeoutMs above. + // However, it ensures `isLoading` is always `false`. + waitForInitialization={true} + options={statsigOptions}> + {children} + </StatsigProvider> + ) +} diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts index ba2cdb39b..820311e4e 100644 --- a/src/lib/strings/url-helpers.ts +++ b/src/lib/strings/url-helpers.ts @@ -3,6 +3,8 @@ import {BSKY_SERVICE} from 'lib/constants' import TLDs from 'tlds' import psl from 'psl' +export const BSKY_APP_HOST = 'https://bsky.app' + export function isValidDomain(str: string): boolean { return !!TLDs.find(tld => { let i = str.lastIndexOf(tld) @@ -67,8 +69,21 @@ export function isBskyAppUrl(url: string): boolean { return url.startsWith('https://bsky.app/') } +export function isRelativeUrl(url: string): boolean { + return /^\/[^/]/.test(url) +} + +export function isBskyRSSUrl(url: string): boolean { + return ( + (url.startsWith('https://bsky.app/') || isRelativeUrl(url)) && + /\/rss\/?$/.test(url) + ) +} + export function isExternalUrl(url: string): boolean { - return !isBskyAppUrl(url) && url.startsWith('http') + const external = !isBskyAppUrl(url) && url.startsWith('http') + const rss = isBskyRSSUrl(url) + return external || rss } export function isBskyPostUrl(url: string): boolean { @@ -148,6 +163,11 @@ export function feedUriToHref(url: string): string { export function linkRequiresWarning(uri: string, label: string) { const labelDomain = labelToDomain(label) + // If the uri started with a / we know it is internal. + if (isRelativeUrl(uri)) { + return false + } + let urip try { urip = new URL(uri) @@ -156,9 +176,12 @@ export function linkRequiresWarning(uri: string, label: string) { } const host = urip.hostname.toLowerCase() - // Hosts that end with bsky.app or bsky.social should be trusted by default. - if (host.endsWith('bsky.app') || host.endsWith('bsky.social')) { + if ( + host.endsWith('bsky.app') || + host.endsWith('bsky.social') || + host.endsWith('blueskyweb.xyz') + ) { // if this is a link to internal content, // warn if it represents itself as a URL to another app return !!labelDomain && labelDomain !== host && isPossiblyAUrl(labelDomain) @@ -214,3 +237,8 @@ export function splitApexDomain(hostname: string): [string, string] { hostnamep.domain, ] } + +export function createBskyAppAbsoluteUrl(path: string): string { + const sanitizedPath = path.replace(BSKY_APP_HOST, '').replace(/^\/+/, '') + return `${BSKY_APP_HOST.replace(/\/$/, '')}/${sanitizedPath}` +} |