Moderation settings fixes (#1336)

* Default isAdultContentEnabled to false on all devices.

The original intent of setting the default based on the device was
to make the maximally-permissive choice. It turns out this was a
mistake as it created sync issues between devices; users would be
confused by the lack of congruity between them. We have to go with
false by default to ensure sync is retained.

* Update preferences model to use new sdk api

* Delete dead code

* Dont show the iOS adult content warning in content filtering settings if adult content is enabled

* Bump @atproto/api@0.6.8

* Codebase style consistency
zio/stable
Paul Frazee 2023-08-30 15:19:19 -07:00 committed by GitHub
parent 3a90b479fd
commit a29f10aefe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 80 additions and 684 deletions

View File

@ -24,7 +24,7 @@
"e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all"
},
"dependencies": {
"@atproto/api": "^0.6.6",
"@atproto/api": "^0.6.8",
"@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2",
"@emoji-mart/react": "^1.1.1",

View File

@ -1,436 +0,0 @@
import {
AppBskyActorDefs,
AppBskyGraphDefs,
AppBskyEmbedRecordWithMedia,
AppBskyEmbedRecord,
AppBskyEmbedImages,
AppBskyEmbedExternal,
} from '@atproto/api'
import {
CONFIGURABLE_LABEL_GROUPS,
ILLEGAL_LABEL_GROUP,
ALWAYS_FILTER_LABEL_GROUP,
ALWAYS_WARN_LABEL_GROUP,
UNKNOWN_LABEL_GROUP,
} from './const'
import {
Label,
LabelValGroup,
ModerationBehaviorCode,
ModerationBehavior,
PostModeration,
ProfileModeration,
PostLabelInfo,
ProfileLabelInfo,
} from './types'
import {RootStoreModel} from 'state/index'
type Embed =
| AppBskyEmbedRecord.View
| AppBskyEmbedImages.View
| AppBskyEmbedExternal.View
| AppBskyEmbedRecordWithMedia.View
| {$type: string; [k: string]: unknown}
export function getLabelValueGroup(labelVal: string): LabelValGroup {
let id: keyof typeof CONFIGURABLE_LABEL_GROUPS
for (id in CONFIGURABLE_LABEL_GROUPS) {
if (ILLEGAL_LABEL_GROUP.values.includes(labelVal)) {
return ILLEGAL_LABEL_GROUP
}
if (ALWAYS_FILTER_LABEL_GROUP.values.includes(labelVal)) {
return ALWAYS_FILTER_LABEL_GROUP
}
if (ALWAYS_WARN_LABEL_GROUP.values.includes(labelVal)) {
return ALWAYS_WARN_LABEL_GROUP
}
if (CONFIGURABLE_LABEL_GROUPS[id].values.includes(labelVal)) {
return CONFIGURABLE_LABEL_GROUPS[id]
}
}
return UNKNOWN_LABEL_GROUP
}
export function getPostModeration(
store: RootStoreModel,
postInfo: PostLabelInfo,
): PostModeration {
const accountPref = store.preferences.getLabelPreference(
postInfo.accountLabels,
)
const profilePref = store.preferences.getLabelPreference(
postInfo.profileLabels,
)
const postPref = store.preferences.getLabelPreference(postInfo.postLabels)
// avatar
let avatar = {
warn: accountPref.pref === 'hide' || accountPref.pref === 'warn',
blur:
postInfo.isBlocking ||
accountPref.pref === 'hide' ||
accountPref.pref === 'warn' ||
profilePref.pref === 'hide' ||
profilePref.pref === 'warn',
}
// hide no-override cases
if (accountPref.pref === 'hide' && accountPref.desc.id === 'illegal') {
return hidePostNoOverride(accountPref.desc.warning)
}
if (profilePref.pref === 'hide' && profilePref.desc.id === 'illegal') {
return hidePostNoOverride(profilePref.desc.warning)
}
if (postPref.pref === 'hide' && postPref.desc.id === 'illegal') {
return hidePostNoOverride(postPref.desc.warning)
}
// hide cases
if (postInfo.isBlocking) {
return {
avatar,
list: hide('Post from an account you blocked.'),
thread: hide('Post from an account you blocked.'),
view: warn('Post from an account you blocked.'),
}
}
if (postInfo.isBlockedBy) {
return {
avatar,
list: hide('Post from an account that has blocked you.'),
thread: hide('Post from an account that has blocked you.'),
view: warn('Post from an account that has blocked you.'),
}
}
if (accountPref.pref === 'hide') {
return {
avatar,
list: hide(accountPref.desc.warning),
thread: hide(accountPref.desc.warning),
view: warn(accountPref.desc.warning),
}
}
if (profilePref.pref === 'hide') {
return {
avatar,
list: hide(profilePref.desc.warning),
thread: hide(profilePref.desc.warning),
view: warn(profilePref.desc.warning),
}
}
if (postPref.pref === 'hide') {
return {
avatar,
list: hide(postPref.desc.warning),
thread: hide(postPref.desc.warning),
view: warn(postPref.desc.warning),
}
}
// muting
if (postInfo.isMuted) {
let msg = 'Post from an account you muted.'
if (postInfo.mutedByList) {
msg = `Muted by ${postInfo.mutedByList.name}`
}
return {
avatar,
list: isMute(hide(msg)),
thread: isMute(warn(msg)),
view: isMute(warn(msg)),
}
}
// warning cases
if (postPref.pref === 'warn') {
if (postPref.desc.isAdultImagery) {
return {
avatar,
list: warnImages(postPref.desc.warning),
thread: warnImages(postPref.desc.warning),
view: warnImages(postPref.desc.warning),
}
}
return {
avatar,
list: warnContent(postPref.desc.warning),
thread: warnContent(postPref.desc.warning),
view: warnContent(postPref.desc.warning),
}
}
if (accountPref.pref === 'warn') {
return {
avatar,
list: warnContent(accountPref.desc.warning),
thread: warnContent(accountPref.desc.warning),
view: warnContent(accountPref.desc.warning),
}
}
return {
avatar,
list: show(),
thread: show(),
view: show(),
}
}
export function mergePostModerations(
moderations: PostModeration[],
): PostModeration {
const merged: PostModeration = {
avatar: {warn: false, blur: false},
list: show(),
thread: show(),
view: show(),
}
for (const mod of moderations) {
if (mod.list.behavior === ModerationBehaviorCode.Hide) {
merged.list = mod.list
}
if (mod.thread.behavior === ModerationBehaviorCode.Hide) {
merged.thread = mod.thread
}
if (mod.view.behavior === ModerationBehaviorCode.Hide) {
merged.view = mod.view
}
}
return merged
}
export function getProfileModeration(
store: RootStoreModel,
profileInfo: ProfileLabelInfo,
): ProfileModeration {
const accountPref = store.preferences.getLabelPreference(
profileInfo.accountLabels,
)
const profilePref = store.preferences.getLabelPreference(
profileInfo.profileLabels,
)
// avatar
let avatar = {
warn: accountPref.pref === 'hide' || accountPref.pref === 'warn',
blur:
profileInfo.isBlocking ||
accountPref.pref === 'hide' ||
accountPref.pref === 'warn' ||
profilePref.pref === 'hide' ||
profilePref.pref === 'warn',
}
// hide no-override cases
if (accountPref.pref === 'hide' && accountPref.desc.id === 'illegal') {
return hideProfileNoOverride(accountPref.desc.warning)
}
if (profilePref.pref === 'hide' && profilePref.desc.id === 'illegal') {
return hideProfileNoOverride(profilePref.desc.warning)
}
// hide cases
if (accountPref.pref === 'hide') {
return {
avatar,
list: hide(accountPref.desc.warning),
view: hide(accountPref.desc.warning),
}
}
if (profilePref.pref === 'hide') {
return {
avatar,
list: hide(profilePref.desc.warning),
view: hide(profilePref.desc.warning),
}
}
// warn cases
if (accountPref.pref === 'warn') {
return {
avatar,
list:
profileInfo.isBlocking || profileInfo.isBlockedBy
? hide('Blocked account')
: warn(accountPref.desc.warning),
view: warn(accountPref.desc.warning),
}
}
// we don't warn for this
// if (profilePref.pref === 'warn') {
// return {
// avatar,
// list: warn(profilePref.desc.warning),
// view: warn(profilePref.desc.warning),
// }
// }
return {
avatar,
list: profileInfo.isBlocking ? hide('Blocked account') : show(),
view: show(),
}
}
export function getProfileViewBasicLabelInfo(
profile: AppBskyActorDefs.ProfileViewBasic,
): ProfileLabelInfo {
return {
accountLabels: filterAccountLabels(profile.labels),
profileLabels: filterProfileLabels(profile.labels),
isMuted: profile.viewer?.muted || false,
isBlocking: !!profile.viewer?.blocking || false,
isBlockedBy: !!profile.viewer?.blockedBy || false,
}
}
export function getEmbedLabels(embed?: Embed): Label[] {
if (!embed) {
return []
}
if (
AppBskyEmbedRecord.isView(embed) &&
AppBskyEmbedRecord.isViewRecord(embed.record)
) {
return embed.record.labels || []
}
return []
}
export function getEmbedMuted(embed?: Embed): boolean {
if (!embed) {
return false
}
if (
AppBskyEmbedRecord.isView(embed) &&
AppBskyEmbedRecord.isViewRecord(embed.record)
) {
return !!embed.record.author.viewer?.muted
}
return false
}
export function getEmbedMutedByList(
embed?: Embed,
): AppBskyGraphDefs.ListViewBasic | undefined {
if (!embed) {
return undefined
}
if (
AppBskyEmbedRecord.isView(embed) &&
AppBskyEmbedRecord.isViewRecord(embed.record)
) {
return embed.record.author.viewer?.mutedByList
}
return undefined
}
export function getEmbedBlocking(embed?: Embed): boolean {
if (!embed) {
return false
}
if (
AppBskyEmbedRecord.isView(embed) &&
AppBskyEmbedRecord.isViewRecord(embed.record)
) {
return !!embed.record.author.viewer?.blocking
}
return false
}
export function getEmbedBlockedBy(embed?: Embed): boolean {
if (!embed) {
return false
}
if (
AppBskyEmbedRecord.isView(embed) &&
AppBskyEmbedRecord.isViewRecord(embed.record)
) {
return !!embed.record.author.viewer?.blockedBy
}
return false
}
export function filterAccountLabels(labels?: Label[]): Label[] {
if (!labels) {
return []
}
return labels.filter(
label => !label.uri.endsWith('/app.bsky.actor.profile/self'),
)
}
export function filterProfileLabels(labels?: Label[]): Label[] {
if (!labels) {
return []
}
return labels.filter(label =>
label.uri.endsWith('/app.bsky.actor.profile/self'),
)
}
// internal methods
// =
function show() {
return {
behavior: ModerationBehaviorCode.Show,
}
}
function hidePostNoOverride(reason: string) {
return {
avatar: {warn: true, blur: true},
list: hideNoOverride(reason),
thread: hideNoOverride(reason),
view: hideNoOverride(reason),
}
}
function hideProfileNoOverride(reason: string) {
return {
avatar: {warn: true, blur: true},
list: hideNoOverride(reason),
view: hideNoOverride(reason),
}
}
function hideNoOverride(reason: string) {
return {
behavior: ModerationBehaviorCode.Hide,
reason,
noOverride: true,
}
}
function hide(reason: string) {
return {
behavior: ModerationBehaviorCode.Hide,
reason,
}
}
function warn(reason: string) {
return {
behavior: ModerationBehaviorCode.Warn,
reason,
}
}
function warnContent(reason: string) {
return {
behavior: ModerationBehaviorCode.WarnContent,
reason,
}
}
function isMute(behavior: ModerationBehavior): ModerationBehavior {
behavior.isMute = true
return behavior
}
function warnImages(reason: string) {
return {
behavior: ModerationBehaviorCode.WarnImages,
reason,
}
}

View File

@ -1,4 +1,4 @@
import {ComAtprotoLabelDefs, AppBskyGraphDefs} from '@atproto/api'
import {ComAtprotoLabelDefs} from '@atproto/api'
import {LabelPreferencesModel} from 'state/models/ui/preferences'
export type Label = ComAtprotoLabelDefs.Label
@ -16,54 +16,3 @@ export interface LabelValGroup {
warning: string
values: string[]
}
export interface PostLabelInfo {
postLabels: Label[]
accountLabels: Label[]
profileLabels: Label[]
isMuted: boolean
mutedByList?: AppBskyGraphDefs.ListViewBasic
isBlocking: boolean
isBlockedBy: boolean
}
export interface ProfileLabelInfo {
accountLabels: Label[]
profileLabels: Label[]
isMuted: boolean
isBlocking: boolean
isBlockedBy: boolean
}
export enum ModerationBehaviorCode {
Show,
Hide,
Warn,
WarnContent,
WarnImages,
}
export interface ModerationBehavior {
behavior: ModerationBehaviorCode
isMute?: boolean
noOverride?: boolean
reason?: string
}
export interface AvatarModeration {
warn: boolean
blur: boolean
}
export interface PostModeration {
avatar: AvatarModeration
list: ModerationBehavior
thread: ModerationBehavior
view: ModerationBehavior
}
export interface ProfileModeration {
avatar: AvatarModeration
list: ModerationBehavior
view: ModerationBehavior
}

View File

@ -4,21 +4,9 @@ import AwaitLock from 'await-lock'
import isEqual from 'lodash.isequal'
import {isObj, hasProp} from 'lib/type-guards'
import {RootStoreModel} from '../root-store'
import {
ComAtprotoLabelDefs,
AppBskyActorDefs,
ModerationOpts,
} from '@atproto/api'
import {LabelValGroup} from 'lib/labeling/types'
import {getLabelValueGroup} from 'lib/labeling/helpers'
import {
UNKNOWN_LABEL_GROUP,
ILLEGAL_LABEL_GROUP,
ALWAYS_FILTER_LABEL_GROUP,
ALWAYS_WARN_LABEL_GROUP,
} from 'lib/labeling/const'
import {ModerationOpts} from '@atproto/api'
import {DEFAULT_FEEDS} from 'lib/constants'
import {isIOS, deviceLocales} from 'platform/detection'
import {deviceLocales} from 'platform/detection'
import {LANGUAGES} from '../../../locale/languages'
// TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf
@ -32,7 +20,7 @@ const LABEL_GROUPS = [
'spam',
'impersonation',
]
const VISIBILITY_VALUES = ['show', 'warn', 'hide']
const VISIBILITY_VALUES = ['ignore', 'warn', 'hide']
const DEFAULT_LANG_CODES = (deviceLocales || [])
.concat(['en', 'ja', 'pt', 'de'])
.slice(0, 6)
@ -52,7 +40,7 @@ export class LabelPreferencesModel {
}
export class PreferencesModel {
adultContentEnabled = !isIOS
adultContentEnabled = false
contentLanguages: string[] = deviceLocales || []
postLanguage: string = deviceLocales[0] || 'en'
postLanguageHistory: string[] = DEFAULT_LANG_CODES
@ -189,43 +177,32 @@ export class PreferencesModel {
await this.lock.acquireAsync()
try {
// fetch preferences
let hasSavedFeedsPref = false
const res = await this.rootStore.agent.app.bsky.actor.getPreferences({})
const prefs = await this.rootStore.agent.getPreferences()
runInAction(() => {
for (const pref of res.data.preferences) {
this.adultContentEnabled = prefs.adultContentEnabled
for (const label in prefs.contentLabels) {
if (
AppBskyActorDefs.isAdultContentPref(pref) &&
AppBskyActorDefs.validateAdultContentPref(pref).success
LABEL_GROUPS.includes(label) &&
VISIBILITY_VALUES.includes(prefs.contentLabels[label])
) {
this.adultContentEnabled = pref.enabled
} else if (
AppBskyActorDefs.isContentLabelPref(pref) &&
AppBskyActorDefs.validateAdultContentPref(pref).success
) {
if (
LABEL_GROUPS.includes(pref.label) &&
VISIBILITY_VALUES.includes(pref.visibility)
) {
this.contentLabels[pref.label as keyof LabelPreferencesModel] =
pref.visibility as LabelPreference
}
} else if (
AppBskyActorDefs.isSavedFeedsPref(pref) &&
AppBskyActorDefs.validateSavedFeedsPref(pref).success
) {
if (!isEqual(this.savedFeeds, pref.saved)) {
this.savedFeeds = pref.saved
}
if (!isEqual(this.pinnedFeeds, pref.pinned)) {
this.pinnedFeeds = pref.pinned
}
hasSavedFeedsPref = true
this.contentLabels[label as keyof LabelPreferencesModel] =
prefs.contentLabels[label]
}
}
if (prefs.feeds.saved && !isEqual(this.savedFeeds, prefs.feeds.saved)) {
this.savedFeeds = prefs.feeds.saved
}
if (
prefs.feeds.pinned &&
!isEqual(this.pinnedFeeds, prefs.feeds.pinned)
) {
this.pinnedFeeds = prefs.feeds.pinned
}
})
// set defaults on missing items
if (!hasSavedFeedsPref) {
if (typeof prefs.feeds.saved === 'undefined') {
const {saved, pinned} = await DEFAULT_FEEDS(
this.rootStore.agent.service.toString(),
(handle: string) =>
@ -237,14 +214,7 @@ export class PreferencesModel {
this.savedFeeds = saved
this.pinnedFeeds = pinned
})
res.data.preferences.push({
$type: 'app.bsky.actor.defs#savedFeedsPref',
saved,
pinned,
})
await this.rootStore.agent.app.bsky.actor.putPreferences({
preferences: res.data.preferences,
})
await this.rootStore.agent.setSavedFeeds(saved, pinned)
}
} finally {
this.lock.release()
@ -253,35 +223,6 @@ export class PreferencesModel {
await this.rootStore.me.savedFeeds.updateCache(clearCache)
}
/**
* This function updates the preferences of a user and allows for a callback function to be executed
* before the update.
* @param cb - cb is a callback function that takes in a single parameter of type
* AppBskyActorDefs.Preferences and returns either a boolean or void. This callback function is used to
* update the preferences of the user. The function is called with the current preferences as an
* argument and if the callback returns false, the preferences are not updated.
* @returns void
*/
async update(
cb: (
prefs: AppBskyActorDefs.Preferences,
) => AppBskyActorDefs.Preferences | false,
) {
await this.lock.acquireAsync()
try {
const res = await this.rootStore.agent.app.bsky.actor.getPreferences({})
const newPrefs = cb(res.data.preferences)
if (newPrefs === false) {
return
}
await this.rootStore.agent.app.bsky.actor.putPreferences({
preferences: newPrefs,
})
} finally {
this.lock.release()
}
}
/**
* This function resets the preferences to an empty array of no preferences.
*/
@ -381,84 +322,12 @@ export class PreferencesModel {
value: LabelPreference,
) {
this.contentLabels[key] = value
await this.update((prefs: AppBskyActorDefs.Preferences) => {
const existing = prefs.find(
pref =>
AppBskyActorDefs.isContentLabelPref(pref) &&
AppBskyActorDefs.validateAdultContentPref(pref).success &&
pref.label === key,
)
if (existing) {
existing.visibility = value
} else {
prefs.push({
$type: 'app.bsky.actor.defs#contentLabelPref',
label: key,
visibility: value,
})
}
return prefs
})
await this.rootStore.agent.setContentLabelPref(key, value)
}
async setAdultContentEnabled(v: boolean) {
this.adultContentEnabled = v
await this.update((prefs: AppBskyActorDefs.Preferences) => {
const existing = prefs.find(
pref =>
AppBskyActorDefs.isAdultContentPref(pref) &&
AppBskyActorDefs.validateAdultContentPref(pref).success,
)
if (existing) {
existing.enabled = v
} else {
prefs.push({
$type: 'app.bsky.actor.defs#adultContentPref',
enabled: v,
})
}
return prefs
})
}
getLabelPreference(labels: ComAtprotoLabelDefs.Label[] | undefined): {
pref: LabelPreference
desc: LabelValGroup
} {
let res: {pref: LabelPreference; desc: LabelValGroup} = {
pref: 'show',
desc: UNKNOWN_LABEL_GROUP,
}
if (!labels?.length) {
return res
}
for (const label of labels) {
const group = getLabelValueGroup(label.val)
if (group.id === 'illegal') {
return {pref: 'hide', desc: ILLEGAL_LABEL_GROUP}
} else if (group.id === 'always-filter') {
return {pref: 'hide', desc: ALWAYS_FILTER_LABEL_GROUP}
} else if (group.id === 'always-warn') {
res.pref = 'warn'
res.desc = ALWAYS_WARN_LABEL_GROUP
continue
} else if (group.id === 'unknown') {
continue
}
let pref = this.contentLabels[group.id]
if (pref === 'hide') {
res.pref = 'hide'
res.desc = group
} else if (pref === 'warn' && res.pref === 'show') {
res.pref = 'warn'
res.desc = group
}
}
if (res.desc.isAdultImagery && !this.adultContentEnabled) {
res.pref = 'hide'
}
return res
await this.rootStore.agent.setAdultContentEnabled(v)
}
get moderationOpts(): ModerationOpts {
@ -499,31 +368,20 @@ export class PreferencesModel {
}
}
async setSavedFeeds(saved: string[], pinned: string[]) {
async _optimisticUpdateSavedFeeds(
saved: string[],
pinned: string[],
cb: () => Promise<{saved: string[]; pinned: string[]}>,
) {
const oldSaved = this.savedFeeds
const oldPinned = this.pinnedFeeds
this.savedFeeds = saved
this.pinnedFeeds = pinned
try {
await this.update((prefs: AppBskyActorDefs.Preferences) => {
let feedsPref = prefs.find(
pref =>
AppBskyActorDefs.isSavedFeedsPref(pref) &&
AppBskyActorDefs.validateSavedFeedsPref(pref).success,
)
if (feedsPref) {
feedsPref.saved = saved
feedsPref.pinned = pinned
} else {
feedsPref = {
$type: 'app.bsky.actor.defs#savedFeedsPref',
saved,
pinned,
}
}
return prefs
.filter(pref => !AppBskyActorDefs.isSavedFeedsPref(pref))
.concat([feedsPref])
const res = await cb()
runInAction(() => {
this.savedFeeds = res.saved
this.pinnedFeeds = res.pinned
})
} catch (e) {
runInAction(() => {
@ -534,25 +392,41 @@ export class PreferencesModel {
}
}
async setSavedFeeds(saved: string[], pinned: string[]) {
return this._optimisticUpdateSavedFeeds(saved, pinned, () =>
this.rootStore.agent.setSavedFeeds(saved, pinned),
)
}
async addSavedFeed(v: string) {
return this.setSavedFeeds([...this.savedFeeds, v], this.pinnedFeeds)
return this._optimisticUpdateSavedFeeds(
[...this.savedFeeds, v],
this.pinnedFeeds,
() => this.rootStore.agent.addSavedFeed(v),
)
}
async removeSavedFeed(v: string) {
return this.setSavedFeeds(
return this._optimisticUpdateSavedFeeds(
this.savedFeeds.filter(uri => uri !== v),
this.pinnedFeeds.filter(uri => uri !== v),
() => this.rootStore.agent.removeSavedFeed(v),
)
}
async addPinnedFeed(v: string) {
return this.setSavedFeeds(this.savedFeeds, [...this.pinnedFeeds, v])
return this._optimisticUpdateSavedFeeds(
this.savedFeeds,
[...this.pinnedFeeds, v],
() => this.rootStore.agent.addPinnedFeed(v),
)
}
async removePinnedFeed(v: string) {
return this.setSavedFeeds(
return this._optimisticUpdateSavedFeeds(
this.savedFeeds,
this.pinnedFeeds.filter(uri => uri !== v),
() => this.rootStore.agent.removePinnedFeed(v),
)
}

View File

@ -48,15 +48,17 @@ export const Component = observer(({}: {}) => {
<ScrollView style={styles.scrollContainer}>
<View style={s.mb10}>
{isIOS ? (
<Text type="md" style={pal.textLight}>
Adult content can only be enabled via the Web at{' '}
<TextLink
style={pal.link}
href="https://bsky.app"
text="bsky.app"
/>
.
</Text>
store.preferences.adultContentEnabled ? null : (
<Text type="md" style={pal.textLight}>
Adult content can only be enabled via the Web at{' '}
<TextLink
style={pal.link}
href="https://bsky.app"
text="bsky.app"
/>
.
</Text>
)
) : (
<ToggleButton
type="default-light"
@ -188,7 +190,7 @@ function SelectGroup({current, onChange, group}: SelectGroupProps) {
/>
<SelectableBtn
current={current}
value="show"
value="ignore"
label="Show"
right
onChange={onChange}

View File

@ -45,13 +45,13 @@
tlds "^1.234.0"
typed-emitter "^2.1.0"
"@atproto/api@^0.6.6":
version "0.6.6"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.6.6.tgz#c1bfdb6bc7dee9cdba1901cde0081c2d422d7c29"
integrity sha512-j+yNTjllVxuTc4bAegghTopju7MdhczLXWvWIli40uXwCzQ3JjS1mFr/47eETtysib2phWYQvfhtCrqQq6AAig==
"@atproto/api@^0.6.8":
version "0.6.8"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.6.8.tgz#fe77f3ab2e7a815edca1357b0a89a7496be8f764"
integrity sha512-WmXpIbO79f85UA8AzvvSqKibojBXN1HT3KEHhUOqXJRW8X1trYijgWIXwhnxhoBQXgiQfzKG7HdORvRjmRSLoQ==
dependencies:
"@atproto/common-web" "*"
"@atproto/uri" "*"
"@atproto/syntax" "*"
"@atproto/xrpc" "*"
tlds "^1.234.0"
typed-emitter "^2.1.0"
@ -317,6 +317,13 @@
uint8arrays "3.0.0"
zod "^3.21.4"
"@atproto/syntax@*":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.1.0.tgz#f13b9dad8d13342cc54295ecd702ea13c82c9bf0"
integrity sha512-Ktui0qvIXt1o1Px1KKC0eqn69MfRHQ9ok5EwjcxIEPbJ8OY5XqkeyJneFDIWRJZiR6vqPHfjFYRUpTB+jNPfRQ==
dependencies:
"@atproto/common-web" "*"
"@atproto/uri@*":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@atproto/uri/-/uri-0.1.0.tgz#1cb4695d2f1766ec8d542af6a495a416f6f6c214"