diff options
Diffstat (limited to 'src/lib')
-rw-r--r-- | src/lib/api/feed-manip.ts | 6 | ||||
-rw-r--r-- | src/lib/api/hack-add-deleted-embed.ts | 24 | ||||
-rw-r--r-- | src/lib/api/index.ts | 38 | ||||
-rw-r--r-- | src/lib/icons.tsx | 38 | ||||
-rw-r--r-- | src/lib/moderation.ts | 107 | ||||
-rw-r--r-- | src/lib/sentry.ts | 5 | ||||
-rw-r--r-- | src/lib/strings/display-names.ts | 11 | ||||
-rw-r--r-- | src/lib/strings/handles.ts | 2 | ||||
-rw-r--r-- | src/lib/strings/rich-text-manip.ts | 34 | ||||
-rw-r--r-- | src/lib/strings/url-helpers.ts | 15 |
10 files changed, 264 insertions, 16 deletions
diff --git a/src/lib/api/feed-manip.ts b/src/lib/api/feed-manip.ts index f4bf6cdff..472289b40 100644 --- a/src/lib/api/feed-manip.ts +++ b/src/lib/api/feed-manip.ts @@ -17,6 +17,12 @@ export class FeedViewPostsSlice { constructor(public items: FeedViewPost[] = []) {} + get _reactKey() { + return `slice-${this.items[0].post.uri}-${ + this.items[0].reason?.indexedAt || this.items[0].post.indexedAt + }` + } + get uri() { if (this.isFlattenedReply) { return this.items[1].post.uri diff --git a/src/lib/api/hack-add-deleted-embed.ts b/src/lib/api/hack-add-deleted-embed.ts new file mode 100644 index 000000000..59aad21a2 --- /dev/null +++ b/src/lib/api/hack-add-deleted-embed.ts @@ -0,0 +1,24 @@ +import { + AppBskyFeedDefs, + AppBskyFeedPost, + ComAtprotoRepoStrongRef, +} from '@atproto/api' + +/** + * HACK + * The server doesnt seem to be correctly giving the notFound view yet + * so I'm adding it manually for now + * -prf + */ +export function hackAddDeletedEmbed(post: AppBskyFeedDefs.PostView) { + const record = post.record as AppBskyFeedPost.Record + if (record.embed?.$type === 'app.bsky.embed.record' && !post.embed) { + post.embed = { + $type: 'app.bsky.embed.record#view', + record: { + $type: 'app.bsky.embed.record#viewNotFound', + uri: (record.embed.record as ComAtprotoRepoStrongRef.Main).uri, + }, + } + } +} diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 458ef7baa..4ecd32046 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -4,6 +4,7 @@ import { AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, AppBskyRichtextFacet, + ComAtprotoLabelDefs, ComAtprotoRepoUploadBlob, RichText, } from '@atproto/api' @@ -13,6 +14,7 @@ import {isNetworkError} from 'lib/strings/errors' import {LinkMeta} from '../link-meta/link-meta' import {isWeb} from 'platform/detection' import {ImageModel} from 'state/models/media/image' +import {shortenLinks} from 'lib/strings/rich-text-manip' export interface ExternalEmbedDraft { uri: string @@ -29,10 +31,24 @@ export async function resolveName(store: RootStoreModel, didOrHandle: string) { if (didOrHandle.startsWith('did:')) { return didOrHandle } - const res = await store.agent.resolveHandle({ - handle: didOrHandle, - }) - return res.data.did + + // we run the resolution always to ensure freshness + const promise = store.agent + .resolveHandle({ + handle: didOrHandle, + }) + .then(res => { + store.handleResolutions.cache.set(didOrHandle, res.data.did) + return res.data.did + }) + + // but we can return immediately if it's cached + const cached = store.handleResolutions.cache.get(didOrHandle) + if (cached) { + return cached + } + + return promise } export async function uploadBlob( @@ -63,6 +79,7 @@ interface PostOpts { } extLink?: ExternalEmbedDraft images?: ImageModel[] + labels?: string[] knownHandles?: Set<string> onStateChange?: (state: string) => void langs?: string[] @@ -76,7 +93,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) { | AppBskyEmbedRecordWithMedia.Main | undefined let reply - const rt = new RichText( + let rt = new RichText( {text: opts.rawText.trim()}, { cleanNewlines: true, @@ -85,6 +102,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) { opts.onStateChange?.('Processing...') await rt.detectFacets(store.agent) + rt = shortenLinks(rt) // filter out any mention facets that didn't map to a user rt.facets = rt.facets?.filter(facet => { @@ -220,6 +238,15 @@ export async function post(store: RootStoreModel, opts: PostOpts) { } } + // set labels + let labels: ComAtprotoLabelDefs.SelfLabels | undefined + if (opts.labels?.length) { + labels = { + $type: 'com.atproto.label.defs#selfLabels', + values: opts.labels.map(val => ({val})), + } + } + // add top 3 languages from user preferences if langs is provided let langs = opts.langs if (opts.langs) { @@ -234,6 +261,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) { reply, embed, langs, + labels, }) } catch (e: any) { console.error(`Failed to create post: ${e.toString()}`) diff --git a/src/lib/icons.tsx b/src/lib/icons.tsx index 4ea3b4d65..233f8a473 100644 --- a/src/lib/icons.tsx +++ b/src/lib/icons.tsx @@ -957,3 +957,41 @@ export function SatelliteDishIcon({ </Svg> ) } + +// Copyright (c) 2020 Refactoring UI Inc. +// https://github.com/tailwindlabs/heroicons/blob/master/LICENSE +export function ShieldExclamation({ + style, + size, + strokeWidth = 1.5, +}: { + style?: StyleProp<TextStyle> + size?: string | number + strokeWidth?: number +}) { + let color = 'currentColor' + if ( + style && + typeof style === 'object' && + 'color' in style && + typeof style.color === 'string' + ) { + color = style.color + } + return ( + <Svg + width={size} + height={size} + fill="none" + viewBox="0 0 24 24" + strokeWidth={strokeWidth || 1.5} + stroke={color} + style={style}> + <Path + strokeLinecap="round" + strokeLinejoin="round" + d="M12 9v3.75m0-10.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.75c0 5.592 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.57-.598-3.75h-.152c-3.196 0-6.1-1.249-8.25-3.286zm0 13.036h.008v.008H12v-.008z" + /> + </Svg> + ) +} diff --git a/src/lib/moderation.ts b/src/lib/moderation.ts new file mode 100644 index 000000000..aadee0e74 --- /dev/null +++ b/src/lib/moderation.ts @@ -0,0 +1,107 @@ +import {ModerationCause, ProfileModeration} from '@atproto/api' + +export interface ModerationCauseDescription { + name: string + description: string +} + +export function describeModerationCause( + cause: ModerationCause | undefined, + context: 'account' | 'content', +): ModerationCauseDescription { + if (!cause) { + return { + name: 'Content Warning', + description: + 'Moderator has chosen to set a general warning on the content.', + } + } + if (cause.type === 'blocking') { + return { + name: 'User Blocked', + description: 'You have blocked this user. You cannot view their content.', + } + } + if (cause.type === 'blocked-by') { + return { + name: 'User Blocking You', + description: 'This user has blocked you. You cannot view their content.', + } + } + if (cause.type === 'block-other') { + return { + name: 'Content Not Available', + description: + 'This content is not available because one of the users involved has blocked the other.', + } + } + if (cause.type === 'muted') { + if (cause.source.type === 'list') { + return { + name: + context === 'account' + ? `Muted by "${cause.source.list.name}"` + : `Post by muted user ("${cause.source.list.name}")`, + description: 'You have muted this user', + } + } else { + return { + name: context === 'account' ? 'Muted User' : 'Post by muted user', + description: 'You have muted this user', + } + } + } + return cause.labelDef.strings[context].en +} + +export function getProfileModerationCauses( + moderation: ProfileModeration, +): ModerationCause[] { + /* + Gather everything on profile and account that blurs or alerts + */ + return [ + moderation.decisions.profile.cause, + ...moderation.decisions.profile.additionalCauses, + moderation.decisions.account.cause, + ...moderation.decisions.account.additionalCauses, + ].filter(cause => { + if (!cause) { + return false + } + if (cause?.type === 'label') { + if ( + cause.labelDef.onwarn === 'blur' || + cause.labelDef.onwarn === 'alert' + ) { + return true + } else { + return false + } + } + return true + }) as ModerationCause[] +} + +export function isCauseALabelOnUri( + cause: ModerationCause | undefined, + uri: string, +): boolean { + if (cause?.type !== 'label') { + return false + } + return cause.label.uri === uri +} + +export function getModerationCauseKey(cause: ModerationCause): string { + const source = + cause.source.type === 'labeler' + ? cause.source.labeler.did + : cause.source.type === 'list' + ? cause.source.list.uri + : 'user' + if (cause.type === 'label') { + return `label:${cause.label.val}:${source}` + } + return `${cause.type}:${source}` +} diff --git a/src/lib/sentry.ts b/src/lib/sentry.ts index c5d1d3eb6..5448415ff 100644 --- a/src/lib/sentry.ts +++ b/src/lib/sentry.ts @@ -15,6 +15,11 @@ Sentry.init({ environment: __DEV__ ? 'development' : 'production', // Set the environment enableAutoPerformanceTracking: true, // Enable auto performance tracking tracesSampleRate: 0.5, // Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring. // TODO: this might be too much in production + _experiments: { + // The sampling rate for profiling is relative to TracesSampleRate. + // In this case, we'll capture profiles for 50% of transactions. + profilesSampleRate: 0.5, + }, integrations: isNative ? [ new Sentry.Native.ReactNativeTracing({ diff --git a/src/lib/strings/display-names.ts b/src/lib/strings/display-names.ts index b98153732..29b7c3b50 100644 --- a/src/lib/strings/display-names.ts +++ b/src/lib/strings/display-names.ts @@ -1,10 +1,19 @@ +import {ModerationUI} from '@atproto/api' +import {describeModerationCause} from '../moderation' + // \u2705 = ✅ // \u2713 = ✓ // \u2714 = ✔ // \u2611 = ☑ const CHECK_MARKS_RE = /[\u2705\u2713\u2714\u2611]/gu -export function sanitizeDisplayName(str: string): string { +export function sanitizeDisplayName( + str: string, + moderation?: ModerationUI, +): string { + if (moderation?.blur) { + return `⚠${describeModerationCause(moderation.cause, 'account').name}` + } if (typeof str === 'string') { return str.replace(CHECK_MARKS_RE, '').trim() } diff --git a/src/lib/strings/handles.ts b/src/lib/strings/handles.ts index 3c01d9345..6ce462435 100644 --- a/src/lib/strings/handles.ts +++ b/src/lib/strings/handles.ts @@ -3,7 +3,7 @@ export function makeValidHandle(str: string): string { str = str.slice(0, 20) } str = str.toLowerCase() - return str.replace(/^[^a-z]+/g, '').replace(/[^a-z0-9-]/g, '') + return str.replace(/^[^a-z0-9]+/g, '').replace(/[^a-z0-9-]/g, '') } export function createFullHandle(name: string, domain: string): string { diff --git a/src/lib/strings/rich-text-manip.ts b/src/lib/strings/rich-text-manip.ts new file mode 100644 index 000000000..d9cd8c071 --- /dev/null +++ b/src/lib/strings/rich-text-manip.ts @@ -0,0 +1,34 @@ +import {RichText, UnicodeString} from '@atproto/api' +import {toShortUrl} from './url-helpers' + +export function shortenLinks(rt: RichText): RichText { + if (!rt.facets?.length) { + return rt + } + rt = rt.clone() + // enumerate the link facets + if (rt.facets) { + for (const facet of rt.facets) { + const isLink = !!facet.features.find( + f => f.$type === 'app.bsky.richtext.facet#link', + ) + if (!isLink) { + continue + } + + // extract and shorten the URL + const {byteStart, byteEnd} = facet.index + const url = rt.unicodeText.slice(byteStart, byteEnd) + const shortened = new UnicodeString(toShortUrl(url)) + + // insert the shorten URL + rt.insert(byteStart, shortened.utf16) + // update the facet to cover the new shortened URL + facet.index.byteStart = byteStart + facet.index.byteEnd = byteStart + shortened.length + // remove the old URL + rt.delete(byteStart + shortened.length, byteEnd + shortened.length) + } + } + return rt +} diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts index 105c631bf..b509aad01 100644 --- a/src/lib/strings/url-helpers.ts +++ b/src/lib/strings/url-helpers.ts @@ -30,7 +30,7 @@ export function toNiceDomain(url: string): string { if (`https://${urlp.host}` === PROD_SERVICE) { return 'Bluesky Social' } - return urlp.host + return urlp.host ? urlp.host : url } catch (e) { return url } @@ -42,15 +42,12 @@ export function toShortUrl(url: string): string { if (urlp.protocol !== 'http:' && urlp.protocol !== 'https:') { return url } - const shortened = - urlp.host + - (urlp.pathname === '/' ? '' : urlp.pathname) + - urlp.search + - urlp.hash - if (shortened.length > 30) { - return shortened.slice(0, 27) + '...' + const path = + (urlp.pathname === '/' ? '' : urlp.pathname) + urlp.search + urlp.hash + if (path.length > 15) { + return urlp.host + path.slice(0, 13) + '...' } - return shortened ? shortened : url + return urlp.host + path } catch (e) { return url } |