diff options
Diffstat (limited to 'src/lib/strings')
-rw-r--r-- | src/lib/strings/display-names.ts | 3 | ||||
-rw-r--r-- | src/lib/strings/embed-player.ts | 41 | ||||
-rw-r--r-- | src/lib/strings/handles.ts | 31 | ||||
-rw-r--r-- | src/lib/strings/helpers.ts | 21 | ||||
-rw-r--r-- | src/lib/strings/time.ts | 2 | ||||
-rw-r--r-- | src/lib/strings/url-helpers.ts | 70 |
6 files changed, 130 insertions, 38 deletions
diff --git a/src/lib/strings/display-names.ts b/src/lib/strings/display-names.ts index 75383dd4f..e0f23fa2c 100644 --- a/src/lib/strings/display-names.ts +++ b/src/lib/strings/display-names.ts @@ -1,5 +1,4 @@ import {ModerationUI} from '@atproto/api' -import {describeModerationCause} from '../moderation' // \u2705 = ✅ // \u2713 = ✓ @@ -14,7 +13,7 @@ export function sanitizeDisplayName( moderation?: ModerationUI, ): string { if (moderation?.blur) { - return `⚠${describeModerationCause(moderation.cause, 'account').name}` + return '' } if (typeof str === 'string') { return str.replace(CHECK_MARKS_RE, '').replace(CONTROL_CHARS_RE, '').trim() diff --git a/src/lib/strings/embed-player.ts b/src/lib/strings/embed-player.ts index 21a575b91..ee7328478 100644 --- a/src/lib/strings/embed-player.ts +++ b/src/lib/strings/embed-player.ts @@ -2,6 +2,15 @@ import {Dimensions} from 'react-native' import {isWeb} from 'platform/detection' const {height: SCREEN_HEIGHT} = Dimensions.get('window') +const IFRAME_HOST = isWeb + ? // @ts-ignore only for web + window.location.host === 'localhost:8100' + ? 'http://localhost:8100' + : 'https://bsky.app' + : __DEV__ && !process.env.JEST_WORKER_ID + ? 'http://localhost:8100' + : 'https://bsky.app' + export const embedPlayerSources = [ 'youtube', 'youtubeShorts', @@ -74,7 +83,7 @@ export function parseEmbedPlayerFromUrl( return { type: 'youtube_video', source: 'youtube', - playerUri: `https://bsky.app/iframe/youtube.html?videoId=${videoId}&start=${seek}`, + playerUri: `${IFRAME_HOST}/iframe/youtube.html?videoId=${videoId}&start=${seek}`, } } } @@ -93,7 +102,7 @@ export function parseEmbedPlayerFromUrl( type: page === 'shorts' ? 'youtube_short' : 'youtube_video', source: page === 'shorts' ? 'youtubeShorts' : 'youtube', hideDetails: page === 'shorts' ? true : undefined, - playerUri: `https://bsky.app/iframe/youtube.html?videoId=${videoId}&start=${seek}`, + playerUri: `${IFRAME_HOST}/iframe/youtube.html?videoId=${videoId}&start=${seek}`, } } } @@ -343,45 +352,45 @@ export function parseEmbedPlayerFromUrl( } } -export function getPlayerHeight({ +export function getPlayerAspect({ type, - width, hasThumb, + width, }: { type: EmbedPlayerParams['type'] - width: number hasThumb: boolean -}) { - if (!hasThumb) return (width / 16) * 9 + width: number +}): {aspectRatio?: number; height?: number} { + if (!hasThumb) return {aspectRatio: 16 / 9} switch (type) { case 'youtube_video': case 'twitch_video': case 'vimeo_video': - return (width / 16) * 9 + return {aspectRatio: 16 / 9} case 'youtube_short': if (SCREEN_HEIGHT < 600) { - return ((width / 9) * 16) / 1.75 + return {aspectRatio: (9 / 16) * 1.75} } else { - return ((width / 9) * 16) / 1.5 + return {aspectRatio: (9 / 16) * 1.5} } case 'spotify_album': case 'apple_music_album': case 'apple_music_playlist': case 'spotify_playlist': case 'soundcloud_set': - return 380 + return {height: 380} case 'spotify_song': if (width <= 300) { - return 155 + return {height: 155} } - return 232 + return {height: 232} case 'soundcloud_track': - return 165 + return {height: 165} case 'apple_music_song': - return 150 + return {height: 150} default: - return width + return {aspectRatio: 16 / 9} } } diff --git a/src/lib/strings/handles.ts b/src/lib/strings/handles.ts index 6ce462435..bc07b32ec 100644 --- a/src/lib/strings/handles.ts +++ b/src/lib/strings/handles.ts @@ -1,3 +1,8 @@ +// Regex from the go implementation +// https://github.com/bluesky-social/indigo/blob/main/atproto/syntax/handle.go#L10 +const VALIDATE_REGEX = + /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/ + export function makeValidHandle(str: string): string { if (str.length > 20) { str = str.slice(0, 20) @@ -19,3 +24,29 @@ export function isInvalidHandle(handle: string): boolean { export function sanitizeHandle(handle: string, prefix = ''): string { return isInvalidHandle(handle) ? '⚠Invalid Handle' : `${prefix}${handle}` } + +export interface IsValidHandle { + handleChars: boolean + hyphenStartOrEnd: boolean + frontLength: boolean + totalLength: boolean + overall: boolean +} + +// More checks from https://github.com/bluesky-social/atproto/blob/main/packages/pds/src/handle/index.ts#L72 +export function validateHandle(str: string, userDomain: string): IsValidHandle { + const fullHandle = createFullHandle(str, userDomain) + + const results = { + handleChars: + !str || (VALIDATE_REGEX.test(fullHandle) && !str.includes('.')), + hyphenStartOrEnd: !str.startsWith('-') && !str.endsWith('-'), + frontLength: str.length >= 3, + totalLength: fullHandle.length <= 253, + } + + return { + ...results, + overall: !Object.values(results).includes(false), + } +} diff --git a/src/lib/strings/helpers.ts b/src/lib/strings/helpers.ts index e2abe9019..de4562d2c 100644 --- a/src/lib/strings/helpers.ts +++ b/src/lib/strings/helpers.ts @@ -8,10 +8,27 @@ export function pluralize(n: number, base: string, plural?: string): string { return base + 's' } -export function enforceLen(str: string, len: number, ellipsis = false): string { +export function enforceLen( + str: string, + len: number, + ellipsis = false, + mode: 'end' | 'middle' = 'end', +): string { str = str || '' if (str.length > len) { - return str.slice(0, len) + (ellipsis ? '...' : '') + if (ellipsis) { + if (mode === 'end') { + return str.slice(0, len) + '…' + } else if (mode === 'middle') { + const half = Math.floor(len / 2) + return str.slice(0, half) + '…' + str.slice(-half) + } else { + // fallback + return str.slice(0, len) + } + } else { + return str.slice(0, len) + } } return str } diff --git a/src/lib/strings/time.ts b/src/lib/strings/time.ts index 05a60e94b..3e162af1a 100644 --- a/src/lib/strings/time.ts +++ b/src/lib/strings/time.ts @@ -23,7 +23,7 @@ export function ago(date: number | string | Date): string { } else if (diffSeconds < DAY) { return `${Math.floor(diffSeconds / HOUR)}h` } else if (diffSeconds < MONTH) { - return `${Math.floor(diffSeconds / DAY)}d` + return `${Math.round(diffSeconds / DAY)}d` } else if (diffSeconds < YEAR) { return `${Math.floor(diffSeconds / MONTH)}mo` } else { diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts index 8a71718c8..70a2b7069 100644 --- a/src/lib/strings/url-helpers.ts +++ b/src/lib/strings/url-helpers.ts @@ -1,8 +1,27 @@ import {AtUri} from '@atproto/api' -import {PROD_SERVICE} from 'lib/constants' +import {BSKY_SERVICE} from 'lib/constants' import TLDs from 'tlds' import psl from 'psl' +export const BSKY_APP_HOST = 'https://bsky.app' +const BSKY_TRUSTED_HOSTS = [ + 'bsky.app', + 'bsky.social', + 'blueskyweb.xyz', + 'blueskyweb.zendesk.com', + ...(__DEV__ ? ['localhost:19006', 'localhost:8100'] : []), +] + +/* + * This will allow any BSKY_TRUSTED_HOSTS value by itself or with a subdomain. + * It will also allow relative paths like /profile as well as #. + */ +const TRUSTED_REGEX = new RegExp( + `^(http(s)?://(([\\w-]+\\.)?${BSKY_TRUSTED_HOSTS.join( + '|([\\w-]+\\.)?', + )})|/|#)`, +) + export function isValidDomain(str: string): boolean { return !!TLDs.find(tld => { let i = str.lastIndexOf(tld) @@ -28,7 +47,7 @@ export function makeRecordUri( export function toNiceDomain(url: string): string { try { const urlp = new URL(url) - if (`https://${urlp.host}` === PROD_SERVICE) { + if (`https://${urlp.host}` === BSKY_SERVICE) { return 'Bluesky Social' } return urlp.host ? urlp.host : url @@ -67,8 +86,25 @@ 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 isTrustedUrl(url: string): boolean { + return TRUSTED_REGEX.test(url) } export function isBskyPostUrl(url: string): boolean { @@ -148,6 +184,11 @@ export function feedUriToHref(url: string): string { export function linkRequiresWarning(uri: string, label: string) { const labelDomain = labelToDomain(label) + // We should trust any relative URL or a # since we know it links to internal content + if (isRelativeUrl(uri) || uri === '#') { + return false + } + let urip try { urip = new URL(uri) @@ -156,21 +197,11 @@ export function linkRequiresWarning(uri: string, label: string) { } const host = urip.hostname.toLowerCase() - - if (host === 'bsky.app') { - // if this is a link to internal content, - // warn if it represents itself as a URL to another app - if ( - labelDomain && - labelDomain !== 'bsky.app' && - isPossiblyAUrl(labelDomain) - ) { - return true - } - return false + if (isTrustedUrl(uri)) { + // 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) } else { - // if this is a link to external content, - // warn if the label doesnt match the target + // if this is a link to external content, warn if the label doesnt match the target if (!labelDomain) { return true } @@ -220,3 +251,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}` +} |