3p moderation services [WIP] (#2550)
* Add modservice screen and profile-header-card * Drop the guidelines for now * Remove ununsed constants * Add label & label group descriptions * Not found state * Reorg, add icon * Subheader * Header * Complete header * Clean up * Add all groups * Fix scroll view * Dialogs side quest * Remove log * Add (WIP) debug mod page * Dialog solution * Add note * Clean up and reorganize localized moderation strings * Memoize * Add example * Add first ReportDialog screen * Report dialog step 2 * Submit * Integrate updates * Move moderation screen * Migrate buttons * Migrate everything * Rough sketch * Fix types * Update atoms values * Abstract ModerationServiceCard * Hook up data to settings page * Handle subscription * Rough enablement * Rough enablement * Some validation, fixes * More work on the mod debug screen * Hook up data * Update invalidation * Hook up data to ReportDialog * Fix native error * Refactor/rewrite the entire moderation-application system * Fix toggles * Add copyright and other option to report * Handle reports on profile vs content * Little cleanup * Get post hiding back in gear * Better loading flow on Mod screen * Clean up Mod screen * Clean up ProfileMod screen * Handle muting correctly * Update enablement on ProfileMod screen * Improve Moderation screen and dialog * Styling, handle disabled labelers * Rework list of labels on own content * Use moderateNotification() * ReportDialog updates * Fix button overflow * Simplify the ProfileModerationService ui * Mod screen design * Move moderation card from the profile header to a tab * Small tweaks to the moderation screen * Enable toggle on mod page * Add notifs to debugmod and dont filter notifs from followed users * Add moderator-service profile view * Wire up more of the modservice data to profiles * A bunch of speculative non-working UI * Cleanup: delete old code * Update ModerationDetailsDialog * Update ReportDialog * Update LabelsOnMe dialog * Handle ReportDialog load better * Rename LabelsOnMeDialog, fix close * Experiment to put labeling under a tab of a normal profile * Moderator variation of profile * Remove dead code and start moving toward latest modsdk * Remove a bunch of now-dead label strings * Update ModDebug to be a bit more intuitive and support custom labels * Minor ui tweaks * Improve consistency of display name blurring * Fix profile-card warning rendering * More debugmod UI tuning * Update to use new labeler semantics * Delete some dead code and do some refactoring * Update profile to pull from labeler definition * Implement new label config controls (wip) * Tweak ui * Implement preference controls on labelers * Rework label pref ui * Get moderation screen working * Add asyncstorage query persistence * Implement label handling * Small cleanup * Implement Likes dialog * Fix: remove text outside of text element * Cleanup * Fix likes dialog on mobile * Implement the label appeal flow * Get report flow working again with temporarily fixed report options * Update onboarding * Enforce limit of ten labeler subscriptions * Fix type errors * Fix lint errors * Improve types of RQ * Some work on Likes dialog, needs discussion * Bit of ReportDialog cleanup * Replace non-single-path SVG * Update nudity descriptions * Update to use new sdk updates * Add adult-content-enabled behavior to label config * Use the default setting of custom labels * Handle global moderation label prefs with the global settings * Fix missing postAuthor * Fix empty moderation page * Add mutewords control back to Mod screen * Tweak adult setting styles * Remove deprecated global labels * Handle underage users on mod screen * Adjust font sizes * Swap in RichText * Like button improvements * Tweaks to Labeler profile * Design tweaks for mod pref dialog * Add tertiary button color * Switch moderation UIs to tertiary color * Update mutewords and hiddenposts to use the new sdk * Add test-environment mod authority * Switch 'gore' to 'graphic-media' * Move nudity out of the adult content control * Remove focus styles from buttons - let the browser behavior handle it * Fixes to the adult content age-gating in moderaiton * Ditch tertiary button color, lighten secondary button * Fix some colors * Remove focused overrides from toggles * Liked by screen * Rework the moderationlabelpref * Fix optimistic like * Cleanup * Change how onboarding handles adult content enabled/disabled * Add special handling of the mod authorities * Tweaks * Update the default labeler avatar to a shield * Add route to go server * Avoid dups due to bad config * Fix attrs * Fix: dont try to detect link/label mismatches on post meta * Correctly show the label behavior when adult content is disabled * Readd the local hiddenPosts handling * WIP * Fix bad merge * Conten hider design tweaks * Fix text string breakage * Adjust source text in ContentHider * Fix link bug * Design tweaks to ContentHider and ModDetailsDialog * Adjust spacing of inform badges * Adjust spacing of embeds in posts * Style tweaks to post/profile alerts * Labels on me and dialog * Remove bad focus styles from post dropdown * Better spacing solution * Tune moderation UIs * Moderation UI tweaks for mobile * Move labelers query on Mod screen * Update to use new SDK appLabelers semantics * Implement report submission * Replace the report modal entirely with the report dialog * Add @ to mod details dialog handle * Bump SDK package * Remove silly type * Add to AWS build CI * Fix ToggleButton overflow * Clean up ModServiceCard, rename to LabelingServiceCard * Hackfix to translate gore labels to graphic-media * Tune content hider sizing on web desktop * Handle self labels * Fix spacing below text-only posts * Fix: send appeals to the right labeler * Give mod page links interactive states * Fix references * Remove focus handling * Remove remnant * Remove the like count from the subscribed labeler listing * Bump @atproto/api@0.11.1 * Remove extra @ * Fix: persist labels to local storage to reduce coverage gaps * update dipendencies * revert dipendencies * Add some explainers on how blocking affects labelers * Tweak copy * Fix underline color in header * Fix profile menu * Handle card overflow * Remove metrics from header * Mute 'account' not 'user' * Show metrics if self * Show the labels tab on logged out view * Fix bad merge * Use purple theming on labelers * Tighten space on LabelerCard * Set staleTime to 6hrs for labeler details * Memoize the memoizers * Drop staleTime to 60s * Move label defs into a context to reduce recomputes * Submit view tweaks * Move labeler fetch below auth * Mitigation: hardcode the bluesky moderation labeler name * Bump sdk * Add missing translated string Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Add missing translated string Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Hailey's fix for incorrect profile tabs Co-authored-by: Hailey <me@haileyok.com> * Feedback * Fix borders, add bottom space * Hailey's fix pt 2 Co-authored-by: Hailey <me@haileyok.com> * Fix post tabs * Integrate feedback pt 1 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 2 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 3 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 4 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 5 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 6 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 7 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 8 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Format * Integrate new bday modal * Use public agent for getServices * Update casing --------- Co-authored-by: Eric Bailey <git@esb.lol> Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> Co-authored-by: Hailey <me@haileyok.com>
This commit is contained in:
parent
d5ebbeb3fc
commit
20d463ff2f
165 changed files with 7034 additions and 5009 deletions
|
@ -1,5 +1,5 @@
|
|||
import React from 'react'
|
||||
import {AppBskyActorDefs, AppBskyGraphDefs, ModerationUI} from '@atproto/api'
|
||||
import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
|
||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
|
||||
import {ImageModel} from '#/state/models/media/image'
|
||||
|
@ -14,32 +14,6 @@ export interface EditProfileModal {
|
|||
onUpdate?: () => void
|
||||
}
|
||||
|
||||
export interface ModerationDetailsModal {
|
||||
name: 'moderation-details'
|
||||
context: 'account' | 'content'
|
||||
moderation: ModerationUI
|
||||
}
|
||||
|
||||
export type ReportModal = {
|
||||
name: 'report'
|
||||
} & (
|
||||
| {
|
||||
uri: string
|
||||
cid: string
|
||||
}
|
||||
| {did: string}
|
||||
)
|
||||
|
||||
export type AppealLabelModal = {
|
||||
name: 'appeal-label'
|
||||
} & (
|
||||
| {
|
||||
uri: string
|
||||
cid: string
|
||||
}
|
||||
| {did: string}
|
||||
)
|
||||
|
||||
export interface CreateOrEditListModal {
|
||||
name: 'create-or-edit-list'
|
||||
purpose?: string
|
||||
|
@ -123,10 +97,6 @@ export interface AddAppPasswordModal {
|
|||
name: 'add-app-password'
|
||||
}
|
||||
|
||||
export interface ContentFilteringSettingsModal {
|
||||
name: 'content-filtering-settings'
|
||||
}
|
||||
|
||||
export interface ContentLanguagesSettingsModal {
|
||||
name: 'content-languages-settings'
|
||||
}
|
||||
|
@ -181,15 +151,9 @@ export type Modal =
|
|||
| SwitchAccountModal
|
||||
|
||||
// Curation
|
||||
| ContentFilteringSettingsModal
|
||||
| ContentLanguagesSettingsModal
|
||||
| PostLanguagesSettingsModal
|
||||
|
||||
// Moderation
|
||||
| ModerationDetailsModal
|
||||
| ReportModal
|
||||
| AppealLabelModal
|
||||
|
||||
// Lists
|
||||
| CreateOrEditListModal
|
||||
| UserAddRemoveListsModal
|
||||
|
|
|
@ -15,6 +15,7 @@ export {
|
|||
useSetExternalEmbedPref,
|
||||
} from './external-embeds-prefs'
|
||||
export * from './hidden-posts'
|
||||
export {useLabelDefinitions} from './label-defs'
|
||||
|
||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
return (
|
||||
|
|
25
src/state/preferences/label-defs.tsx
Normal file
25
src/state/preferences/label-defs.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import React from 'react'
|
||||
import {InterpretedLabelValueDefinition, AppBskyLabelerDefs} from '@atproto/api'
|
||||
import {useLabelDefinitionsQuery} from '../queries/preferences'
|
||||
|
||||
interface StateContext {
|
||||
labelDefs: Record<string, InterpretedLabelValueDefinition[]>
|
||||
labelers: AppBskyLabelerDefs.LabelerViewDetailed[]
|
||||
}
|
||||
|
||||
const stateContext = React.createContext<StateContext>({
|
||||
labelDefs: {},
|
||||
labelers: [],
|
||||
})
|
||||
|
||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
const {labelDefs, labelers} = useLabelDefinitionsQuery()
|
||||
|
||||
const state = {labelDefs, labelers}
|
||||
|
||||
return <stateContext.Provider value={state}>{children}</stateContext.Provider>
|
||||
}
|
||||
|
||||
export function useLabelDefinitions() {
|
||||
return React.useContext(stateContext)
|
||||
}
|
|
@ -6,17 +6,14 @@ import {logger} from '#/logger'
|
|||
import {getAgent} from '#/state/session'
|
||||
import {useMyFollowsQuery} from '#/state/queries/my-follows'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {
|
||||
DEFAULT_LOGGED_OUT_PREFERENCES,
|
||||
getModerationOpts,
|
||||
useModerationOpts,
|
||||
} from './preferences'
|
||||
import {DEFAULT_LOGGED_OUT_PREFERENCES, useModerationOpts} from './preferences'
|
||||
import {isInvalidHandle} from '#/lib/strings/handles'
|
||||
import {isJustAMute} from '#/lib/moderation'
|
||||
|
||||
const DEFAULT_MOD_OPTS = getModerationOpts({
|
||||
userDid: '',
|
||||
preferences: DEFAULT_LOGGED_OUT_PREFERENCES,
|
||||
})
|
||||
const DEFAULT_MOD_OPTS = {
|
||||
userDid: undefined,
|
||||
prefs: DEFAULT_LOGGED_OUT_PREFERENCES.moderationPrefs,
|
||||
}
|
||||
|
||||
export const RQKEY = (prefix: string) => ['actor-autocomplete', prefix]
|
||||
|
||||
|
@ -114,8 +111,8 @@ function computeSuggestions(
|
|||
}
|
||||
}
|
||||
return items.filter(profile => {
|
||||
const mod = moderateProfile(profile, moderationOpts)
|
||||
return !mod.account.filter && mod.account.cause?.type !== 'muted'
|
||||
const modui = moderateProfile(profile, moderationOpts).ui('profileList')
|
||||
return !modui.filter || isJustAMute(modui)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
89
src/state/queries/labeler.ts
Normal file
89
src/state/queries/labeler.ts
Normal file
|
@ -0,0 +1,89 @@
|
|||
import {z} from 'zod'
|
||||
import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
import {AppBskyLabelerDefs} from '@atproto/api'
|
||||
|
||||
import {getAgent} from '#/state/session'
|
||||
import {preferencesQueryKey} from '#/state/queries/preferences'
|
||||
import {STALE, PUBLIC_BSKY_AGENT} from '#/state/queries'
|
||||
|
||||
export const labelerInfoQueryKey = (did: string) => ['labeler-info', did]
|
||||
export const labelersInfoQueryKey = (dids: string[]) => [
|
||||
'labelers-info',
|
||||
dids.sort(),
|
||||
]
|
||||
export const labelersDetailedInfoQueryKey = (dids: string[]) => [
|
||||
'labelers-detailed-info',
|
||||
dids,
|
||||
]
|
||||
|
||||
export function useLabelerInfoQuery({
|
||||
did,
|
||||
enabled,
|
||||
}: {
|
||||
did?: string
|
||||
enabled?: boolean
|
||||
}) {
|
||||
return useQuery({
|
||||
enabled: !!did && enabled !== false,
|
||||
queryKey: labelerInfoQueryKey(did as string),
|
||||
queryFn: async () => {
|
||||
const res = await PUBLIC_BSKY_AGENT.app.bsky.labeler.getServices({
|
||||
dids: [did as string],
|
||||
detailed: true,
|
||||
})
|
||||
return res.data.views[0] as AppBskyLabelerDefs.LabelerViewDetailed
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useLabelersInfoQuery({dids}: {dids: string[]}) {
|
||||
return useQuery({
|
||||
enabled: !!dids.length,
|
||||
queryKey: labelersInfoQueryKey(dids),
|
||||
queryFn: async () => {
|
||||
const res = await PUBLIC_BSKY_AGENT.app.bsky.labeler.getServices({dids})
|
||||
return res.data.views as AppBskyLabelerDefs.LabelerView[]
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useLabelersDetailedInfoQuery({dids}: {dids: string[]}) {
|
||||
return useQuery({
|
||||
enabled: !!dids.length,
|
||||
queryKey: labelersDetailedInfoQueryKey(dids),
|
||||
gcTime: 1000 * 60 * 60 * 6, // 6 hours
|
||||
staleTime: STALE.MINUTES.ONE,
|
||||
queryFn: async () => {
|
||||
const res = await PUBLIC_BSKY_AGENT.app.bsky.labeler.getServices({
|
||||
dids,
|
||||
detailed: true,
|
||||
})
|
||||
return res.data.views as AppBskyLabelerDefs.LabelerViewDetailed[]
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useLabelerSubscriptionMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
async mutationFn({did, subscribe}: {did: string; subscribe: boolean}) {
|
||||
// TODO
|
||||
z.object({
|
||||
did: z.string(),
|
||||
subscribe: z.boolean(),
|
||||
}).parse({did, subscribe})
|
||||
|
||||
if (subscribe) {
|
||||
await getAgent().addLabeler(did)
|
||||
} else {
|
||||
await getAgent().removeLabeler(did)
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: preferencesQueryKey,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
|
@ -1,14 +1,13 @@
|
|||
import {
|
||||
AppBskyNotificationListNotifications,
|
||||
ModerationOpts,
|
||||
moderateProfile,
|
||||
moderateNotification,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
AppBskyFeedRepost,
|
||||
AppBskyFeedLike,
|
||||
AppBskyEmbedRecord,
|
||||
} from '@atproto/api'
|
||||
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
||||
import chunk from 'lodash.chunk'
|
||||
import {QueryClient} from '@tanstack/react-query'
|
||||
import {getAgent} from '../../session'
|
||||
|
@ -88,37 +87,20 @@ export async function fetchPage({
|
|||
// internal methods
|
||||
// =
|
||||
|
||||
// TODO this should be in the sdk as moderateNotification -prf
|
||||
function shouldFilterNotif(
|
||||
export function shouldFilterNotif(
|
||||
notif: AppBskyNotificationListNotifications.Notification,
|
||||
moderationOpts: ModerationOpts | undefined,
|
||||
): boolean {
|
||||
if (!moderationOpts) {
|
||||
return false
|
||||
}
|
||||
const profile = moderateProfile(notif.author, moderationOpts)
|
||||
if (
|
||||
profile.account.filter ||
|
||||
profile.profile.filter ||
|
||||
notif.author.viewer?.muted
|
||||
) {
|
||||
return true
|
||||
if (notif.author.viewer?.following) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
notif.type === 'reply' ||
|
||||
notif.type === 'quote' ||
|
||||
notif.type === 'mention'
|
||||
) {
|
||||
// NOTE: the notification overlaps the post enough for this to work
|
||||
const post = moderatePost(notif, moderationOpts)
|
||||
if (post.content.filter) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return moderateNotification(notif, moderationOpts).ui('contentList').filter
|
||||
}
|
||||
|
||||
function groupNotifications(
|
||||
export function groupNotifications(
|
||||
notifs: AppBskyNotificationListNotifications.Notification[],
|
||||
): FeedNotification[] {
|
||||
const groupedNotifs: FeedNotification[] = []
|
||||
|
|
|
@ -3,8 +3,8 @@ import {AppState} from 'react-native'
|
|||
import {
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
ModerationDecision,
|
||||
AtUri,
|
||||
PostModeration,
|
||||
} from '@atproto/api'
|
||||
import {
|
||||
useInfiniteQuery,
|
||||
|
@ -29,7 +29,6 @@ import {STALE} from '#/state/queries'
|
|||
import {precacheFeedPostProfiles} from './profile'
|
||||
import {getAgent} from '#/state/session'
|
||||
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const'
|
||||
import {getModerationOpts} from '#/state/queries/preferences/moderation'
|
||||
import {KnownError} from '#/view/com/posts/FeedErrorMessage'
|
||||
import {embedViewRecordToPostView, getEmbeddedPost} from './util'
|
||||
import {useModerationOpts} from './preferences'
|
||||
|
@ -69,7 +68,7 @@ export interface FeedPostSliceItem {
|
|||
post: AppBskyFeedDefs.PostView
|
||||
record: AppBskyFeedPost.Record
|
||||
reason?: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource
|
||||
moderation: PostModeration
|
||||
moderation: ModerationDecision
|
||||
}
|
||||
|
||||
export interface FeedPostSlice {
|
||||
|
@ -250,9 +249,17 @@ export function usePostFeedQuery(
|
|||
|
||||
// apply moderation filter
|
||||
for (let i = 0; i < slice.items.length; i++) {
|
||||
const ignoreFilter =
|
||||
slice.items[i].post.author.did === ignoreFilterFor
|
||||
if (ignoreFilter) {
|
||||
// remove mutes to avoid confused UIs
|
||||
moderations[i].causes = moderations[i].causes.filter(
|
||||
cause => cause.type !== 'muted',
|
||||
)
|
||||
}
|
||||
if (
|
||||
moderations[i]?.content.filter &&
|
||||
slice.items[i].post.author.did !== ignoreFilterFor
|
||||
!ignoreFilter &&
|
||||
moderations[i]?.ui('contentList').filter
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
|
@ -435,13 +442,12 @@ function assertSomePostsPassModeration(feed: AppBskyFeedDefs.FeedViewPost[]) {
|
|||
let somePostsPassModeration = false
|
||||
|
||||
for (const item of feed) {
|
||||
const moderationOpts = getModerationOpts({
|
||||
userDid: '',
|
||||
preferences: DEFAULT_LOGGED_OUT_PREFERENCES,
|
||||
const moderation = moderatePost(item.post, {
|
||||
userDid: undefined,
|
||||
prefs: DEFAULT_LOGGED_OUT_PREFERENCES.moderationPrefs,
|
||||
})
|
||||
const moderation = moderatePost(item.post, moderationOpts)
|
||||
|
||||
if (!moderation.content.filter) {
|
||||
if (!moderation.ui('contentList').filter) {
|
||||
// we have a sfw post
|
||||
somePostsPassModeration = true
|
||||
}
|
||||
|
|
|
@ -12,9 +12,9 @@ const PAGE_SIZE = 30
|
|||
type RQPageParam = string | undefined
|
||||
|
||||
// TODO refactor invalidate on mutate?
|
||||
export const RQKEY = (resolvedUri: string) => ['post-liked-by', resolvedUri]
|
||||
export const RQKEY = (resolvedUri: string) => ['liked-by', resolvedUri]
|
||||
|
||||
export function usePostLikedByQuery(resolvedUri: string | undefined) {
|
||||
export function useLikedByQuery(resolvedUri: string | undefined) {
|
||||
return useInfiniteQuery<
|
||||
AppBskyFeedGetLikes.OutputSchema,
|
||||
Error,
|
||||
|
|
|
@ -29,26 +29,20 @@ export const DEFAULT_PROD_FEEDS = {
|
|||
|
||||
export const DEFAULT_LOGGED_OUT_PREFERENCES: UsePreferencesQueryResponse = {
|
||||
birthDate: new Date('2022-11-17'), // TODO(pwi)
|
||||
adultContentEnabled: false,
|
||||
feeds: {
|
||||
saved: [],
|
||||
pinned: [],
|
||||
unpinned: [],
|
||||
},
|
||||
// labels are undefined until set by user
|
||||
contentLabels: {
|
||||
nsfw: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.nsfw,
|
||||
nudity: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.nudity,
|
||||
suggestive: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.suggestive,
|
||||
gore: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.gore,
|
||||
hate: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.hate,
|
||||
spam: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.spam,
|
||||
impersonation: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.impersonation,
|
||||
moderationPrefs: {
|
||||
adultContentEnabled: false,
|
||||
labels: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES,
|
||||
labelers: [],
|
||||
mutedWords: [],
|
||||
hiddenPosts: [],
|
||||
},
|
||||
feedViewPrefs: DEFAULT_HOME_FEED_PREFS,
|
||||
threadViewPrefs: DEFAULT_THREAD_VIEW_PREFS,
|
||||
userAge: 13, // TODO(pwi)
|
||||
interests: {tags: []},
|
||||
mutedWords: [],
|
||||
hiddenPosts: [],
|
||||
}
|
||||
|
|
|
@ -1,29 +1,27 @@
|
|||
import {useMemo} from 'react'
|
||||
import {useMemo, createContext, useContext} from 'react'
|
||||
import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
import {
|
||||
LabelPreference,
|
||||
BskyFeedViewPreference,
|
||||
ModerationOpts,
|
||||
AppBskyActorDefs,
|
||||
} from '@atproto/api'
|
||||
|
||||
import {track} from '#/lib/analytics/analytics'
|
||||
import {getAge} from '#/lib/strings/time'
|
||||
import {useSession, getAgent} from '#/state/session'
|
||||
import {DEFAULT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation'
|
||||
import {getAgent, useSession} from '#/state/session'
|
||||
import {
|
||||
ConfigurableLabelGroup,
|
||||
UsePreferencesQueryResponse,
|
||||
ThreadViewPreferences,
|
||||
} from '#/state/queries/preferences/types'
|
||||
import {temp__migrateLabelPref} from '#/state/queries/preferences/util'
|
||||
import {
|
||||
DEFAULT_HOME_FEED_PREFS,
|
||||
DEFAULT_THREAD_VIEW_PREFS,
|
||||
DEFAULT_LOGGED_OUT_PREFERENCES,
|
||||
} from '#/state/queries/preferences/const'
|
||||
import {getModerationOpts} from '#/state/queries/preferences/moderation'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {useHiddenPosts} from '#/state/preferences/hidden-posts'
|
||||
import {useHiddenPosts, useLabelDefinitions} from '#/state/preferences'
|
||||
import {saveLabelers} from '#/state/session/agent-config'
|
||||
|
||||
export * from '#/state/queries/preferences/types'
|
||||
export * from '#/state/queries/preferences/moderation'
|
||||
|
@ -44,6 +42,13 @@ export function usePreferencesQuery() {
|
|||
return DEFAULT_LOGGED_OUT_PREFERENCES
|
||||
} else {
|
||||
const res = await agent.getPreferences()
|
||||
|
||||
// save to local storage to ensure there are labels on initial requests
|
||||
saveLabelers(
|
||||
agent.session.did,
|
||||
res.moderationPrefs.labelers.map(l => l.did),
|
||||
)
|
||||
|
||||
const preferences: UsePreferencesQueryResponse = {
|
||||
...res,
|
||||
feeds: {
|
||||
|
@ -54,32 +59,6 @@ export function usePreferencesQuery() {
|
|||
return !res.feeds.pinned?.includes(f)
|
||||
}) || [],
|
||||
},
|
||||
// labels are undefined until set by user
|
||||
contentLabels: {
|
||||
nsfw: temp__migrateLabelPref(
|
||||
res.contentLabels?.nsfw || DEFAULT_LABEL_PREFERENCES.nsfw,
|
||||
),
|
||||
nudity: temp__migrateLabelPref(
|
||||
res.contentLabels?.nudity || DEFAULT_LABEL_PREFERENCES.nudity,
|
||||
),
|
||||
suggestive: temp__migrateLabelPref(
|
||||
res.contentLabels?.suggestive ||
|
||||
DEFAULT_LABEL_PREFERENCES.suggestive,
|
||||
),
|
||||
gore: temp__migrateLabelPref(
|
||||
res.contentLabels?.gore || DEFAULT_LABEL_PREFERENCES.gore,
|
||||
),
|
||||
hate: temp__migrateLabelPref(
|
||||
res.contentLabels?.hate || DEFAULT_LABEL_PREFERENCES.hate,
|
||||
),
|
||||
spam: temp__migrateLabelPref(
|
||||
res.contentLabels?.spam || DEFAULT_LABEL_PREFERENCES.spam,
|
||||
),
|
||||
impersonation: temp__migrateLabelPref(
|
||||
res.contentLabels?.impersonation ||
|
||||
DEFAULT_LABEL_PREFERENCES.impersonation,
|
||||
),
|
||||
},
|
||||
feedViewPrefs: {
|
||||
...DEFAULT_HOME_FEED_PREFS,
|
||||
...(res.feedViewPrefs.home || {}),
|
||||
|
@ -96,25 +75,30 @@ export function usePreferencesQuery() {
|
|||
})
|
||||
}
|
||||
|
||||
// used in the moderation state devtool
|
||||
export const moderationOptsOverrideContext = createContext<
|
||||
ModerationOpts | undefined
|
||||
>(undefined)
|
||||
|
||||
export function useModerationOpts() {
|
||||
const override = useContext(moderationOptsOverrideContext)
|
||||
const {currentAccount} = useSession()
|
||||
const prefs = usePreferencesQuery()
|
||||
const hiddenPosts = useHiddenPosts()
|
||||
const opts = useMemo(() => {
|
||||
const {labelDefs} = useLabelDefinitions()
|
||||
const hiddenPosts = useHiddenPosts() // TODO move this into pds-stored prefs
|
||||
const opts = useMemo<ModerationOpts | undefined>(() => {
|
||||
if (override) {
|
||||
return override
|
||||
}
|
||||
if (!prefs.data) {
|
||||
return
|
||||
}
|
||||
const moderationOpts = getModerationOpts({
|
||||
userDid: currentAccount?.did || '',
|
||||
preferences: prefs.data,
|
||||
})
|
||||
|
||||
return {
|
||||
...moderationOpts,
|
||||
hiddenPosts,
|
||||
mutedWords: prefs.data.mutedWords || [],
|
||||
userDid: currentAccount?.did,
|
||||
prefs: {...prefs.data.moderationPrefs, hiddenPosts: hiddenPosts || []},
|
||||
labelDefs,
|
||||
}
|
||||
}, [currentAccount?.did, prefs.data, hiddenPosts])
|
||||
}, [override, currentAccount, labelDefs, prefs.data, hiddenPosts])
|
||||
return opts
|
||||
}
|
||||
|
||||
|
@ -138,10 +122,32 @@ export function usePreferencesSetContentLabelMutation() {
|
|||
return useMutation<
|
||||
void,
|
||||
unknown,
|
||||
{labelGroup: ConfigurableLabelGroup; visibility: LabelPreference}
|
||||
{label: string; visibility: LabelPreference; labelerDid: string | undefined}
|
||||
>({
|
||||
mutationFn: async ({labelGroup, visibility}) => {
|
||||
await getAgent().setContentLabelPref(labelGroup, visibility)
|
||||
mutationFn: async ({label, visibility, labelerDid}) => {
|
||||
await getAgent().setContentLabelPref(label, visibility, labelerDid)
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: preferencesQueryKey,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useSetContentLabelMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
label,
|
||||
visibility,
|
||||
labelerDid,
|
||||
}: {
|
||||
label: string
|
||||
visibility: LabelPreference
|
||||
labelerDid?: string
|
||||
}) => {
|
||||
await getAgent().setContentLabelPref(label, visibility, labelerDid)
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: preferencesQueryKey,
|
||||
|
|
|
@ -1,181 +1,53 @@
|
|||
import React from 'react'
|
||||
import {
|
||||
LabelPreference,
|
||||
ComAtprotoLabelDefs,
|
||||
ModerationOpts,
|
||||
DEFAULT_LABEL_SETTINGS,
|
||||
BskyAgent,
|
||||
interpretLabelValueDefinitions,
|
||||
} from '@atproto/api'
|
||||
|
||||
import {
|
||||
LabelGroup,
|
||||
ConfigurableLabelGroup,
|
||||
UsePreferencesQueryResponse,
|
||||
} from '#/state/queries/preferences/types'
|
||||
|
||||
export type Label = ComAtprotoLabelDefs.Label
|
||||
|
||||
export type LabelGroupConfig = {
|
||||
id: LabelGroup
|
||||
title: string
|
||||
isAdultImagery?: boolean
|
||||
subtitle?: string
|
||||
warning: string
|
||||
values: string[]
|
||||
}
|
||||
|
||||
export const DEFAULT_LABEL_PREFERENCES: Record<
|
||||
ConfigurableLabelGroup,
|
||||
LabelPreference
|
||||
> = {
|
||||
nsfw: 'hide',
|
||||
nudity: 'warn',
|
||||
suggestive: 'warn',
|
||||
gore: 'warn',
|
||||
hate: 'hide',
|
||||
spam: 'hide',
|
||||
impersonation: 'hide',
|
||||
}
|
||||
import {usePreferencesQuery} from './index'
|
||||
import {useLabelersDetailedInfoQuery} from '../labeler'
|
||||
|
||||
/**
|
||||
* More strict than our default settings for logged in users.
|
||||
*
|
||||
* TODO(pwi)
|
||||
*/
|
||||
export const DEFAULT_LOGGED_OUT_LABEL_PREFERENCES: Record<
|
||||
ConfigurableLabelGroup,
|
||||
LabelPreference
|
||||
> = {
|
||||
nsfw: 'hide',
|
||||
nudity: 'hide',
|
||||
suggestive: 'hide',
|
||||
gore: 'hide',
|
||||
hate: 'hide',
|
||||
spam: 'hide',
|
||||
impersonation: 'hide',
|
||||
export const DEFAULT_LOGGED_OUT_LABEL_PREFERENCES: typeof DEFAULT_LABEL_SETTINGS =
|
||||
Object.fromEntries(
|
||||
Object.entries(DEFAULT_LABEL_SETTINGS).map(([key, _pref]) => [key, 'hide']),
|
||||
)
|
||||
|
||||
export function useMyLabelersQuery() {
|
||||
const prefs = usePreferencesQuery()
|
||||
const dids = Array.from(
|
||||
new Set(
|
||||
BskyAgent.appLabelers.concat(
|
||||
prefs.data?.moderationPrefs.labelers.map(l => l.did) || [],
|
||||
),
|
||||
),
|
||||
)
|
||||
const labelers = useLabelersDetailedInfoQuery({dids})
|
||||
const isLoading = prefs.isLoading || labelers.isLoading
|
||||
const error = prefs.error || labelers.error
|
||||
return React.useMemo(() => {
|
||||
return {
|
||||
isLoading,
|
||||
error,
|
||||
data: labelers.data,
|
||||
}
|
||||
}, [labelers, isLoading, error])
|
||||
}
|
||||
|
||||
export const ILLEGAL_LABEL_GROUP: LabelGroupConfig = {
|
||||
id: 'illegal',
|
||||
title: 'Illegal Content',
|
||||
warning: 'Illegal Content',
|
||||
values: ['csam', 'dmca-violation', 'nudity-nonconsensual'],
|
||||
}
|
||||
|
||||
export const ALWAYS_FILTER_LABEL_GROUP: LabelGroupConfig = {
|
||||
id: 'always-filter',
|
||||
title: 'Content Warning',
|
||||
warning: 'Content Warning',
|
||||
values: ['!filter'],
|
||||
}
|
||||
|
||||
export const ALWAYS_WARN_LABEL_GROUP: LabelGroupConfig = {
|
||||
id: 'always-warn',
|
||||
title: 'Content Warning',
|
||||
warning: 'Content Warning',
|
||||
values: ['!warn', 'account-security'],
|
||||
}
|
||||
|
||||
export const UNKNOWN_LABEL_GROUP: LabelGroupConfig = {
|
||||
id: 'unknown',
|
||||
title: 'Unknown Label',
|
||||
warning: 'Content Warning',
|
||||
values: [],
|
||||
}
|
||||
|
||||
export const CONFIGURABLE_LABEL_GROUPS: Record<
|
||||
ConfigurableLabelGroup,
|
||||
LabelGroupConfig
|
||||
> = {
|
||||
nsfw: {
|
||||
id: 'nsfw',
|
||||
title: 'Explicit Sexual Images',
|
||||
subtitle: 'i.e. pornography',
|
||||
warning: 'Sexually Explicit',
|
||||
values: ['porn', 'nsfl'],
|
||||
isAdultImagery: true,
|
||||
},
|
||||
nudity: {
|
||||
id: 'nudity',
|
||||
title: 'Other Nudity',
|
||||
subtitle: 'Including non-sexual and artistic',
|
||||
warning: 'Nudity',
|
||||
values: ['nudity'],
|
||||
isAdultImagery: true,
|
||||
},
|
||||
suggestive: {
|
||||
id: 'suggestive',
|
||||
title: 'Sexually Suggestive',
|
||||
subtitle: 'Does not include nudity',
|
||||
warning: 'Sexually Suggestive',
|
||||
values: ['sexual'],
|
||||
isAdultImagery: true,
|
||||
},
|
||||
gore: {
|
||||
id: 'gore',
|
||||
title: 'Violent / Bloody',
|
||||
subtitle: 'Gore, self-harm, torture',
|
||||
warning: 'Violence',
|
||||
values: ['gore', 'self-harm', 'torture', 'nsfl', 'corpse'],
|
||||
isAdultImagery: true,
|
||||
},
|
||||
hate: {
|
||||
id: 'hate',
|
||||
title: 'Hate Group Iconography',
|
||||
subtitle: 'Images of terror groups, articles covering events, etc.',
|
||||
warning: 'Hate Groups',
|
||||
values: ['icon-kkk', 'icon-nazi', 'icon-intolerant', 'behavior-intolerant'],
|
||||
},
|
||||
spam: {
|
||||
id: 'spam',
|
||||
title: 'Spam',
|
||||
subtitle: 'Excessive unwanted interactions',
|
||||
warning: 'Spam',
|
||||
values: ['spam'],
|
||||
},
|
||||
impersonation: {
|
||||
id: 'impersonation',
|
||||
title: 'Impersonation',
|
||||
subtitle: 'Accounts falsely claiming to be people or orgs',
|
||||
warning: 'Impersonation',
|
||||
values: ['impersonation'],
|
||||
},
|
||||
}
|
||||
|
||||
export function getModerationOpts({
|
||||
userDid,
|
||||
preferences,
|
||||
}: {
|
||||
userDid: string
|
||||
preferences: UsePreferencesQueryResponse
|
||||
}): ModerationOpts {
|
||||
return {
|
||||
userDid: userDid,
|
||||
adultContentEnabled: preferences.adultContentEnabled,
|
||||
labels: {
|
||||
porn: preferences.contentLabels.nsfw,
|
||||
sexual: preferences.contentLabels.suggestive,
|
||||
nudity: preferences.contentLabels.nudity,
|
||||
nsfl: preferences.contentLabels.gore,
|
||||
corpse: preferences.contentLabels.gore,
|
||||
gore: preferences.contentLabels.gore,
|
||||
torture: preferences.contentLabels.gore,
|
||||
'self-harm': preferences.contentLabels.gore,
|
||||
'intolerant-race': preferences.contentLabels.hate,
|
||||
'intolerant-gender': preferences.contentLabels.hate,
|
||||
'intolerant-sexual-orientation': preferences.contentLabels.hate,
|
||||
'intolerant-religion': preferences.contentLabels.hate,
|
||||
intolerant: preferences.contentLabels.hate,
|
||||
'icon-intolerant': preferences.contentLabels.hate,
|
||||
spam: preferences.contentLabels.spam,
|
||||
impersonation: preferences.contentLabels.impersonation,
|
||||
scam: 'warn',
|
||||
},
|
||||
labelers: [
|
||||
{
|
||||
labeler: {
|
||||
did: '',
|
||||
displayName: 'Bluesky Social',
|
||||
},
|
||||
labels: {},
|
||||
},
|
||||
],
|
||||
}
|
||||
export function useLabelDefinitionsQuery() {
|
||||
const labelers = useMyLabelersQuery()
|
||||
return React.useMemo(() => {
|
||||
return {
|
||||
labelDefs: Object.fromEntries(
|
||||
(labelers.data || []).map(labeler => [
|
||||
labeler.creator.did,
|
||||
interpretLabelValueDefinitions(labeler),
|
||||
]),
|
||||
),
|
||||
labelers: labelers.data || [],
|
||||
}
|
||||
}, [labelers])
|
||||
}
|
||||
|
|
|
@ -1,46 +1,13 @@
|
|||
import {
|
||||
BskyPreferences,
|
||||
LabelPreference,
|
||||
BskyThreadViewPreference,
|
||||
BskyFeedViewPreference,
|
||||
} from '@atproto/api'
|
||||
|
||||
export const configurableAdultLabelGroups = [
|
||||
'nsfw',
|
||||
'nudity',
|
||||
'suggestive',
|
||||
'gore',
|
||||
] as const
|
||||
|
||||
export const configurableOtherLabelGroups = [
|
||||
'hate',
|
||||
'spam',
|
||||
'impersonation',
|
||||
] as const
|
||||
|
||||
export const configurableLabelGroups = [
|
||||
...configurableAdultLabelGroups,
|
||||
...configurableOtherLabelGroups,
|
||||
] as const
|
||||
export type ConfigurableLabelGroup = (typeof configurableLabelGroups)[number]
|
||||
|
||||
export type LabelGroup =
|
||||
| ConfigurableLabelGroup
|
||||
| 'illegal'
|
||||
| 'always-filter'
|
||||
| 'always-warn'
|
||||
| 'unknown'
|
||||
|
||||
export type UsePreferencesQueryResponse = Omit<
|
||||
BskyPreferences,
|
||||
'contentLabels' | 'feedViewPrefs' | 'feeds'
|
||||
> & {
|
||||
/*
|
||||
* Content labels previously included 'show', which has been deprecated in
|
||||
* favor of 'ignore'. The API can return legacy data from the database, and
|
||||
* we clean up the data in `usePreferencesQuery`.
|
||||
*/
|
||||
contentLabels: Record<ConfigurableLabelGroup, LabelPreference>
|
||||
feedViewPrefs: BskyFeedViewPreference & {
|
||||
lab_mergeFeedEnabled?: boolean
|
||||
}
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
import {LabelPreference} from '@atproto/api'
|
||||
|
||||
/**
|
||||
* Content labels previously included 'show', which has been deprecated in
|
||||
* favor of 'ignore'. The API can return legacy data from the database, and
|
||||
* we clean up the data in `usePreferencesQuery`.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
export function temp__migrateLabelPref(
|
||||
pref: LabelPreference | 'show',
|
||||
): LabelPreference {
|
||||
// @ts-ignore
|
||||
if (pref === 'show') return 'ignore'
|
||||
return pref
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
import {useQuery} from '@tanstack/react-query'
|
||||
|
||||
import {getAgent} from '#/state/session'
|
||||
import {STALE} from '#/state/queries'
|
||||
|
||||
// TODO refactor invalidate on mutate?
|
||||
export const RQKEY = (did: string) => ['profile-extra-info', did]
|
||||
|
||||
/**
|
||||
* Fetches some additional information for the profile screen which
|
||||
* is not available in the API's ProfileView
|
||||
*/
|
||||
export function useProfileExtraInfoQuery(did: string) {
|
||||
return useQuery({
|
||||
staleTime: STALE.MINUTES.ONE,
|
||||
queryKey: RQKEY(did),
|
||||
async queryFn() {
|
||||
const [listsRes, feedsRes] = await Promise.all([
|
||||
getAgent().app.bsky.graph.getLists({
|
||||
actor: did,
|
||||
limit: 1,
|
||||
}),
|
||||
getAgent().app.bsky.feed.getActorFeeds({
|
||||
actor: did,
|
||||
limit: 1,
|
||||
}),
|
||||
])
|
||||
return {
|
||||
hasLists: listsRes.data.lists.length > 0,
|
||||
hasFeedgens: feedsRes.data.feeds.length > 0,
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
|
@ -46,7 +46,8 @@ export function useSuggestedFollowsQuery() {
|
|||
|
||||
res.data.actors = res.data.actors
|
||||
.filter(
|
||||
actor => !moderateProfile(actor, moderationOpts!).account.filter,
|
||||
actor =>
|
||||
!moderateProfile(actor, moderationOpts!).ui('profileList').filter,
|
||||
)
|
||||
.filter(actor => {
|
||||
const viewer = actor.viewer
|
||||
|
|
12
src/state/session/agent-config.ts
Normal file
12
src/state/session/agent-config.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
|
||||
const PREFIX = 'agent-labelers'
|
||||
|
||||
export async function saveLabelers(did: string, value: string[]) {
|
||||
await AsyncStorage.setItem(`${PREFIX}:${did}`, JSON.stringify(value))
|
||||
}
|
||||
|
||||
export async function readLabelers(did: string): Promise<string[] | undefined> {
|
||||
const rawData = await AsyncStorage.getItem(`${PREFIX}:${did}`)
|
||||
return rawData ? JSON.parse(rawData) : undefined
|
||||
}
|
|
@ -1,8 +1,15 @@
|
|||
import React from 'react'
|
||||
import {BskyAgent, AtpPersistSessionHandler} from '@atproto/api'
|
||||
import {
|
||||
BskyAgent,
|
||||
AtpPersistSessionHandler,
|
||||
BSKY_LABELER_DID,
|
||||
} from '@atproto/api'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
import {jwtDecode} from 'jwt-decode'
|
||||
|
||||
import {IS_DEV} from '#/env'
|
||||
import {IS_TEST_USER} from '#/lib/constants'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {networkRetry} from '#/lib/async/retry'
|
||||
import {logger} from '#/logger'
|
||||
import * as persisted from '#/state/persisted'
|
||||
|
@ -12,6 +19,7 @@ import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
|||
import {useCloseAllActiveElements} from '#/state/util'
|
||||
import {track} from '#/lib/analytics/analytics'
|
||||
import {hasProp} from '#/lib/type-guards'
|
||||
import {readLabelers} from './agent-config'
|
||||
|
||||
let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT
|
||||
|
||||
|
@ -255,6 +263,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||
deactivated,
|
||||
}
|
||||
|
||||
await configureModeration(agent, account)
|
||||
|
||||
agent.setPersistSessionHandler(
|
||||
createPersistSessionHandler(
|
||||
account,
|
||||
|
@ -298,6 +308,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||
deactivated: isSessionDeactivated(agent.session.accessJwt),
|
||||
}
|
||||
|
||||
await configureModeration(agent, account)
|
||||
|
||||
agent.setPersistSessionHandler(
|
||||
createPersistSessionHandler(
|
||||
account,
|
||||
|
@ -309,6 +321,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||
)
|
||||
|
||||
__globalAgent = agent
|
||||
// @ts-ignore
|
||||
if (IS_DEV && isWeb) window.agent = agent
|
||||
queryClient.clear()
|
||||
upsertAccount(account)
|
||||
|
||||
|
@ -348,6 +362,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||
{networkErrorCallback: clearCurrentAccount},
|
||||
),
|
||||
})
|
||||
// @ts-ignore
|
||||
if (IS_DEV && isWeb) window.agent = agent
|
||||
await configureModeration(agent, account)
|
||||
|
||||
let canReusePrevSession = false
|
||||
try {
|
||||
|
@ -643,6 +660,28 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||
)
|
||||
}
|
||||
|
||||
async function configureModeration(agent: BskyAgent, account: SessionAccount) {
|
||||
if (IS_TEST_USER(account.handle)) {
|
||||
const did = (
|
||||
await agent
|
||||
.resolveHandle({handle: 'mod-authority.test'})
|
||||
.catch(_ => undefined)
|
||||
)?.data.did
|
||||
if (did) {
|
||||
console.warn('USING TEST ENV MODERATION')
|
||||
BskyAgent.configure({appLabelers: [did]})
|
||||
}
|
||||
} else {
|
||||
BskyAgent.configure({appLabelers: [BSKY_LABELER_DID]})
|
||||
const labelerDids = await readLabelers(account.did).catch(_ => {})
|
||||
if (labelerDids) {
|
||||
agent.configureLabelersHeader(
|
||||
labelerDids.filter(did => did !== BSKY_LABELER_DID),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function useSession() {
|
||||
return React.useContext(StateContext)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
|||
import {
|
||||
AppBskyEmbedRecord,
|
||||
AppBskyRichtextFacet,
|
||||
PostModeration,
|
||||
ModerationDecision,
|
||||
} from '@atproto/api'
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
|
||||
|
@ -16,7 +16,7 @@ export interface ComposerOptsPostRef {
|
|||
avatar?: string
|
||||
}
|
||||
embed?: AppBskyEmbedRecord.ViewRecord['embed']
|
||||
moderation?: PostModeration
|
||||
moderation?: ModerationDecision
|
||||
}
|
||||
export interface ComposerOptsQuote {
|
||||
uri: string
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue