Bluesky app fork with some witchin' additions 💫

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

authored by Paul Frazee and committed by GitHub a29f10ae 3a90b479

Changed files
+80 -684
src
lib
labeling
state
models
view
+1 -1
package.json
··· 24 24 "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all" 25 25 }, 26 26 "dependencies": { 27 - "@atproto/api": "^0.6.6", 27 + "@atproto/api": "^0.6.8", 28 28 "@bam.tech/react-native-image-resizer": "^3.0.4", 29 29 "@braintree/sanitize-url": "^6.0.2", 30 30 "@emoji-mart/react": "^1.1.1",
-436
src/lib/labeling/helpers.ts
··· 1 - import { 2 - AppBskyActorDefs, 3 - AppBskyGraphDefs, 4 - AppBskyEmbedRecordWithMedia, 5 - AppBskyEmbedRecord, 6 - AppBskyEmbedImages, 7 - AppBskyEmbedExternal, 8 - } from '@atproto/api' 9 - import { 10 - CONFIGURABLE_LABEL_GROUPS, 11 - ILLEGAL_LABEL_GROUP, 12 - ALWAYS_FILTER_LABEL_GROUP, 13 - ALWAYS_WARN_LABEL_GROUP, 14 - UNKNOWN_LABEL_GROUP, 15 - } from './const' 16 - import { 17 - Label, 18 - LabelValGroup, 19 - ModerationBehaviorCode, 20 - ModerationBehavior, 21 - PostModeration, 22 - ProfileModeration, 23 - PostLabelInfo, 24 - ProfileLabelInfo, 25 - } from './types' 26 - import {RootStoreModel} from 'state/index' 27 - 28 - type Embed = 29 - | AppBskyEmbedRecord.View 30 - | AppBskyEmbedImages.View 31 - | AppBskyEmbedExternal.View 32 - | AppBskyEmbedRecordWithMedia.View 33 - | {$type: string; [k: string]: unknown} 34 - 35 - export function getLabelValueGroup(labelVal: string): LabelValGroup { 36 - let id: keyof typeof CONFIGURABLE_LABEL_GROUPS 37 - for (id in CONFIGURABLE_LABEL_GROUPS) { 38 - if (ILLEGAL_LABEL_GROUP.values.includes(labelVal)) { 39 - return ILLEGAL_LABEL_GROUP 40 - } 41 - if (ALWAYS_FILTER_LABEL_GROUP.values.includes(labelVal)) { 42 - return ALWAYS_FILTER_LABEL_GROUP 43 - } 44 - if (ALWAYS_WARN_LABEL_GROUP.values.includes(labelVal)) { 45 - return ALWAYS_WARN_LABEL_GROUP 46 - } 47 - if (CONFIGURABLE_LABEL_GROUPS[id].values.includes(labelVal)) { 48 - return CONFIGURABLE_LABEL_GROUPS[id] 49 - } 50 - } 51 - return UNKNOWN_LABEL_GROUP 52 - } 53 - 54 - export function getPostModeration( 55 - store: RootStoreModel, 56 - postInfo: PostLabelInfo, 57 - ): PostModeration { 58 - const accountPref = store.preferences.getLabelPreference( 59 - postInfo.accountLabels, 60 - ) 61 - const profilePref = store.preferences.getLabelPreference( 62 - postInfo.profileLabels, 63 - ) 64 - const postPref = store.preferences.getLabelPreference(postInfo.postLabels) 65 - 66 - // avatar 67 - let avatar = { 68 - warn: accountPref.pref === 'hide' || accountPref.pref === 'warn', 69 - blur: 70 - postInfo.isBlocking || 71 - accountPref.pref === 'hide' || 72 - accountPref.pref === 'warn' || 73 - profilePref.pref === 'hide' || 74 - profilePref.pref === 'warn', 75 - } 76 - 77 - // hide no-override cases 78 - if (accountPref.pref === 'hide' && accountPref.desc.id === 'illegal') { 79 - return hidePostNoOverride(accountPref.desc.warning) 80 - } 81 - if (profilePref.pref === 'hide' && profilePref.desc.id === 'illegal') { 82 - return hidePostNoOverride(profilePref.desc.warning) 83 - } 84 - if (postPref.pref === 'hide' && postPref.desc.id === 'illegal') { 85 - return hidePostNoOverride(postPref.desc.warning) 86 - } 87 - 88 - // hide cases 89 - if (postInfo.isBlocking) { 90 - return { 91 - avatar, 92 - list: hide('Post from an account you blocked.'), 93 - thread: hide('Post from an account you blocked.'), 94 - view: warn('Post from an account you blocked.'), 95 - } 96 - } 97 - if (postInfo.isBlockedBy) { 98 - return { 99 - avatar, 100 - list: hide('Post from an account that has blocked you.'), 101 - thread: hide('Post from an account that has blocked you.'), 102 - view: warn('Post from an account that has blocked you.'), 103 - } 104 - } 105 - if (accountPref.pref === 'hide') { 106 - return { 107 - avatar, 108 - list: hide(accountPref.desc.warning), 109 - thread: hide(accountPref.desc.warning), 110 - view: warn(accountPref.desc.warning), 111 - } 112 - } 113 - if (profilePref.pref === 'hide') { 114 - return { 115 - avatar, 116 - list: hide(profilePref.desc.warning), 117 - thread: hide(profilePref.desc.warning), 118 - view: warn(profilePref.desc.warning), 119 - } 120 - } 121 - if (postPref.pref === 'hide') { 122 - return { 123 - avatar, 124 - list: hide(postPref.desc.warning), 125 - thread: hide(postPref.desc.warning), 126 - view: warn(postPref.desc.warning), 127 - } 128 - } 129 - 130 - // muting 131 - if (postInfo.isMuted) { 132 - let msg = 'Post from an account you muted.' 133 - if (postInfo.mutedByList) { 134 - msg = `Muted by ${postInfo.mutedByList.name}` 135 - } 136 - return { 137 - avatar, 138 - list: isMute(hide(msg)), 139 - thread: isMute(warn(msg)), 140 - view: isMute(warn(msg)), 141 - } 142 - } 143 - 144 - // warning cases 145 - if (postPref.pref === 'warn') { 146 - if (postPref.desc.isAdultImagery) { 147 - return { 148 - avatar, 149 - list: warnImages(postPref.desc.warning), 150 - thread: warnImages(postPref.desc.warning), 151 - view: warnImages(postPref.desc.warning), 152 - } 153 - } 154 - return { 155 - avatar, 156 - list: warnContent(postPref.desc.warning), 157 - thread: warnContent(postPref.desc.warning), 158 - view: warnContent(postPref.desc.warning), 159 - } 160 - } 161 - if (accountPref.pref === 'warn') { 162 - return { 163 - avatar, 164 - list: warnContent(accountPref.desc.warning), 165 - thread: warnContent(accountPref.desc.warning), 166 - view: warnContent(accountPref.desc.warning), 167 - } 168 - } 169 - 170 - return { 171 - avatar, 172 - list: show(), 173 - thread: show(), 174 - view: show(), 175 - } 176 - } 177 - 178 - export function mergePostModerations( 179 - moderations: PostModeration[], 180 - ): PostModeration { 181 - const merged: PostModeration = { 182 - avatar: {warn: false, blur: false}, 183 - list: show(), 184 - thread: show(), 185 - view: show(), 186 - } 187 - for (const mod of moderations) { 188 - if (mod.list.behavior === ModerationBehaviorCode.Hide) { 189 - merged.list = mod.list 190 - } 191 - if (mod.thread.behavior === ModerationBehaviorCode.Hide) { 192 - merged.thread = mod.thread 193 - } 194 - if (mod.view.behavior === ModerationBehaviorCode.Hide) { 195 - merged.view = mod.view 196 - } 197 - } 198 - return merged 199 - } 200 - 201 - export function getProfileModeration( 202 - store: RootStoreModel, 203 - profileInfo: ProfileLabelInfo, 204 - ): ProfileModeration { 205 - const accountPref = store.preferences.getLabelPreference( 206 - profileInfo.accountLabels, 207 - ) 208 - const profilePref = store.preferences.getLabelPreference( 209 - profileInfo.profileLabels, 210 - ) 211 - 212 - // avatar 213 - let avatar = { 214 - warn: accountPref.pref === 'hide' || accountPref.pref === 'warn', 215 - blur: 216 - profileInfo.isBlocking || 217 - accountPref.pref === 'hide' || 218 - accountPref.pref === 'warn' || 219 - profilePref.pref === 'hide' || 220 - profilePref.pref === 'warn', 221 - } 222 - 223 - // hide no-override cases 224 - if (accountPref.pref === 'hide' && accountPref.desc.id === 'illegal') { 225 - return hideProfileNoOverride(accountPref.desc.warning) 226 - } 227 - if (profilePref.pref === 'hide' && profilePref.desc.id === 'illegal') { 228 - return hideProfileNoOverride(profilePref.desc.warning) 229 - } 230 - 231 - // hide cases 232 - if (accountPref.pref === 'hide') { 233 - return { 234 - avatar, 235 - list: hide(accountPref.desc.warning), 236 - view: hide(accountPref.desc.warning), 237 - } 238 - } 239 - if (profilePref.pref === 'hide') { 240 - return { 241 - avatar, 242 - list: hide(profilePref.desc.warning), 243 - view: hide(profilePref.desc.warning), 244 - } 245 - } 246 - 247 - // warn cases 248 - if (accountPref.pref === 'warn') { 249 - return { 250 - avatar, 251 - list: 252 - profileInfo.isBlocking || profileInfo.isBlockedBy 253 - ? hide('Blocked account') 254 - : warn(accountPref.desc.warning), 255 - view: warn(accountPref.desc.warning), 256 - } 257 - } 258 - // we don't warn for this 259 - // if (profilePref.pref === 'warn') { 260 - // return { 261 - // avatar, 262 - // list: warn(profilePref.desc.warning), 263 - // view: warn(profilePref.desc.warning), 264 - // } 265 - // } 266 - 267 - return { 268 - avatar, 269 - list: profileInfo.isBlocking ? hide('Blocked account') : show(), 270 - view: show(), 271 - } 272 - } 273 - 274 - export function getProfileViewBasicLabelInfo( 275 - profile: AppBskyActorDefs.ProfileViewBasic, 276 - ): ProfileLabelInfo { 277 - return { 278 - accountLabels: filterAccountLabels(profile.labels), 279 - profileLabels: filterProfileLabels(profile.labels), 280 - isMuted: profile.viewer?.muted || false, 281 - isBlocking: !!profile.viewer?.blocking || false, 282 - isBlockedBy: !!profile.viewer?.blockedBy || false, 283 - } 284 - } 285 - 286 - export function getEmbedLabels(embed?: Embed): Label[] { 287 - if (!embed) { 288 - return [] 289 - } 290 - if ( 291 - AppBskyEmbedRecord.isView(embed) && 292 - AppBskyEmbedRecord.isViewRecord(embed.record) 293 - ) { 294 - return embed.record.labels || [] 295 - } 296 - return [] 297 - } 298 - 299 - export function getEmbedMuted(embed?: Embed): boolean { 300 - if (!embed) { 301 - return false 302 - } 303 - if ( 304 - AppBskyEmbedRecord.isView(embed) && 305 - AppBskyEmbedRecord.isViewRecord(embed.record) 306 - ) { 307 - return !!embed.record.author.viewer?.muted 308 - } 309 - return false 310 - } 311 - 312 - export function getEmbedMutedByList( 313 - embed?: Embed, 314 - ): AppBskyGraphDefs.ListViewBasic | undefined { 315 - if (!embed) { 316 - return undefined 317 - } 318 - if ( 319 - AppBskyEmbedRecord.isView(embed) && 320 - AppBskyEmbedRecord.isViewRecord(embed.record) 321 - ) { 322 - return embed.record.author.viewer?.mutedByList 323 - } 324 - return undefined 325 - } 326 - 327 - export function getEmbedBlocking(embed?: Embed): boolean { 328 - if (!embed) { 329 - return false 330 - } 331 - if ( 332 - AppBskyEmbedRecord.isView(embed) && 333 - AppBskyEmbedRecord.isViewRecord(embed.record) 334 - ) { 335 - return !!embed.record.author.viewer?.blocking 336 - } 337 - return false 338 - } 339 - 340 - export function getEmbedBlockedBy(embed?: Embed): boolean { 341 - if (!embed) { 342 - return false 343 - } 344 - if ( 345 - AppBskyEmbedRecord.isView(embed) && 346 - AppBskyEmbedRecord.isViewRecord(embed.record) 347 - ) { 348 - return !!embed.record.author.viewer?.blockedBy 349 - } 350 - return false 351 - } 352 - 353 - export function filterAccountLabels(labels?: Label[]): Label[] { 354 - if (!labels) { 355 - return [] 356 - } 357 - return labels.filter( 358 - label => !label.uri.endsWith('/app.bsky.actor.profile/self'), 359 - ) 360 - } 361 - 362 - export function filterProfileLabels(labels?: Label[]): Label[] { 363 - if (!labels) { 364 - return [] 365 - } 366 - return labels.filter(label => 367 - label.uri.endsWith('/app.bsky.actor.profile/self'), 368 - ) 369 - } 370 - 371 - // internal methods 372 - // = 373 - 374 - function show() { 375 - return { 376 - behavior: ModerationBehaviorCode.Show, 377 - } 378 - } 379 - 380 - function hidePostNoOverride(reason: string) { 381 - return { 382 - avatar: {warn: true, blur: true}, 383 - list: hideNoOverride(reason), 384 - thread: hideNoOverride(reason), 385 - view: hideNoOverride(reason), 386 - } 387 - } 388 - 389 - function hideProfileNoOverride(reason: string) { 390 - return { 391 - avatar: {warn: true, blur: true}, 392 - list: hideNoOverride(reason), 393 - view: hideNoOverride(reason), 394 - } 395 - } 396 - 397 - function hideNoOverride(reason: string) { 398 - return { 399 - behavior: ModerationBehaviorCode.Hide, 400 - reason, 401 - noOverride: true, 402 - } 403 - } 404 - 405 - function hide(reason: string) { 406 - return { 407 - behavior: ModerationBehaviorCode.Hide, 408 - reason, 409 - } 410 - } 411 - 412 - function warn(reason: string) { 413 - return { 414 - behavior: ModerationBehaviorCode.Warn, 415 - reason, 416 - } 417 - } 418 - 419 - function warnContent(reason: string) { 420 - return { 421 - behavior: ModerationBehaviorCode.WarnContent, 422 - reason, 423 - } 424 - } 425 - 426 - function isMute(behavior: ModerationBehavior): ModerationBehavior { 427 - behavior.isMute = true 428 - return behavior 429 - } 430 - 431 - function warnImages(reason: string) { 432 - return { 433 - behavior: ModerationBehaviorCode.WarnImages, 434 - reason, 435 - } 436 - }
+1 -52
src/lib/labeling/types.ts
··· 1 - import {ComAtprotoLabelDefs, AppBskyGraphDefs} from '@atproto/api' 1 + import {ComAtprotoLabelDefs} from '@atproto/api' 2 2 import {LabelPreferencesModel} from 'state/models/ui/preferences' 3 3 4 4 export type Label = ComAtprotoLabelDefs.Label ··· 16 16 warning: string 17 17 values: string[] 18 18 } 19 - 20 - export interface PostLabelInfo { 21 - postLabels: Label[] 22 - accountLabels: Label[] 23 - profileLabels: Label[] 24 - isMuted: boolean 25 - mutedByList?: AppBskyGraphDefs.ListViewBasic 26 - isBlocking: boolean 27 - isBlockedBy: boolean 28 - } 29 - 30 - export interface ProfileLabelInfo { 31 - accountLabels: Label[] 32 - profileLabels: Label[] 33 - isMuted: boolean 34 - isBlocking: boolean 35 - isBlockedBy: boolean 36 - } 37 - 38 - export enum ModerationBehaviorCode { 39 - Show, 40 - Hide, 41 - Warn, 42 - WarnContent, 43 - WarnImages, 44 - } 45 - 46 - export interface ModerationBehavior { 47 - behavior: ModerationBehaviorCode 48 - isMute?: boolean 49 - noOverride?: boolean 50 - reason?: string 51 - } 52 - 53 - export interface AvatarModeration { 54 - warn: boolean 55 - blur: boolean 56 - } 57 - 58 - export interface PostModeration { 59 - avatar: AvatarModeration 60 - list: ModerationBehavior 61 - thread: ModerationBehavior 62 - view: ModerationBehavior 63 - } 64 - 65 - export interface ProfileModeration { 66 - avatar: AvatarModeration 67 - list: ModerationBehavior 68 - view: ModerationBehavior 69 - }
+54 -180
src/state/models/ui/preferences.ts
··· 4 4 import isEqual from 'lodash.isequal' 5 5 import {isObj, hasProp} from 'lib/type-guards' 6 6 import {RootStoreModel} from '../root-store' 7 - import { 8 - ComAtprotoLabelDefs, 9 - AppBskyActorDefs, 10 - ModerationOpts, 11 - } from '@atproto/api' 12 - import {LabelValGroup} from 'lib/labeling/types' 13 - import {getLabelValueGroup} from 'lib/labeling/helpers' 14 - import { 15 - UNKNOWN_LABEL_GROUP, 16 - ILLEGAL_LABEL_GROUP, 17 - ALWAYS_FILTER_LABEL_GROUP, 18 - ALWAYS_WARN_LABEL_GROUP, 19 - } from 'lib/labeling/const' 7 + import {ModerationOpts} from '@atproto/api' 20 8 import {DEFAULT_FEEDS} from 'lib/constants' 21 - import {isIOS, deviceLocales} from 'platform/detection' 9 + import {deviceLocales} from 'platform/detection' 22 10 import {LANGUAGES} from '../../../locale/languages' 23 11 24 12 // TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf ··· 32 20 'spam', 33 21 'impersonation', 34 22 ] 35 - const VISIBILITY_VALUES = ['show', 'warn', 'hide'] 23 + const VISIBILITY_VALUES = ['ignore', 'warn', 'hide'] 36 24 const DEFAULT_LANG_CODES = (deviceLocales || []) 37 25 .concat(['en', 'ja', 'pt', 'de']) 38 26 .slice(0, 6) ··· 52 40 } 53 41 54 42 export class PreferencesModel { 55 - adultContentEnabled = !isIOS 43 + adultContentEnabled = false 56 44 contentLanguages: string[] = deviceLocales || [] 57 45 postLanguage: string = deviceLocales[0] || 'en' 58 46 postLanguageHistory: string[] = DEFAULT_LANG_CODES ··· 189 177 await this.lock.acquireAsync() 190 178 try { 191 179 // fetch preferences 192 - let hasSavedFeedsPref = false 193 - const res = await this.rootStore.agent.app.bsky.actor.getPreferences({}) 180 + const prefs = await this.rootStore.agent.getPreferences() 181 + 194 182 runInAction(() => { 195 - for (const pref of res.data.preferences) { 183 + this.adultContentEnabled = prefs.adultContentEnabled 184 + for (const label in prefs.contentLabels) { 196 185 if ( 197 - AppBskyActorDefs.isAdultContentPref(pref) && 198 - AppBskyActorDefs.validateAdultContentPref(pref).success 199 - ) { 200 - this.adultContentEnabled = pref.enabled 201 - } else if ( 202 - AppBskyActorDefs.isContentLabelPref(pref) && 203 - AppBskyActorDefs.validateAdultContentPref(pref).success 186 + LABEL_GROUPS.includes(label) && 187 + VISIBILITY_VALUES.includes(prefs.contentLabels[label]) 204 188 ) { 205 - if ( 206 - LABEL_GROUPS.includes(pref.label) && 207 - VISIBILITY_VALUES.includes(pref.visibility) 208 - ) { 209 - this.contentLabels[pref.label as keyof LabelPreferencesModel] = 210 - pref.visibility as LabelPreference 211 - } 212 - } else if ( 213 - AppBskyActorDefs.isSavedFeedsPref(pref) && 214 - AppBskyActorDefs.validateSavedFeedsPref(pref).success 215 - ) { 216 - if (!isEqual(this.savedFeeds, pref.saved)) { 217 - this.savedFeeds = pref.saved 218 - } 219 - if (!isEqual(this.pinnedFeeds, pref.pinned)) { 220 - this.pinnedFeeds = pref.pinned 221 - } 222 - hasSavedFeedsPref = true 189 + this.contentLabels[label as keyof LabelPreferencesModel] = 190 + prefs.contentLabels[label] 223 191 } 224 192 } 193 + if (prefs.feeds.saved && !isEqual(this.savedFeeds, prefs.feeds.saved)) { 194 + this.savedFeeds = prefs.feeds.saved 195 + } 196 + if ( 197 + prefs.feeds.pinned && 198 + !isEqual(this.pinnedFeeds, prefs.feeds.pinned) 199 + ) { 200 + this.pinnedFeeds = prefs.feeds.pinned 201 + } 225 202 }) 226 203 227 204 // set defaults on missing items 228 - if (!hasSavedFeedsPref) { 205 + if (typeof prefs.feeds.saved === 'undefined') { 229 206 const {saved, pinned} = await DEFAULT_FEEDS( 230 207 this.rootStore.agent.service.toString(), 231 208 (handle: string) => ··· 237 214 this.savedFeeds = saved 238 215 this.pinnedFeeds = pinned 239 216 }) 240 - res.data.preferences.push({ 241 - $type: 'app.bsky.actor.defs#savedFeedsPref', 242 - saved, 243 - pinned, 244 - }) 245 - await this.rootStore.agent.app.bsky.actor.putPreferences({ 246 - preferences: res.data.preferences, 247 - }) 217 + await this.rootStore.agent.setSavedFeeds(saved, pinned) 248 218 } 249 219 } finally { 250 220 this.lock.release() 251 221 } 252 222 253 223 await this.rootStore.me.savedFeeds.updateCache(clearCache) 254 - } 255 - 256 - /** 257 - * This function updates the preferences of a user and allows for a callback function to be executed 258 - * before the update. 259 - * @param cb - cb is a callback function that takes in a single parameter of type 260 - * AppBskyActorDefs.Preferences and returns either a boolean or void. This callback function is used to 261 - * update the preferences of the user. The function is called with the current preferences as an 262 - * argument and if the callback returns false, the preferences are not updated. 263 - * @returns void 264 - */ 265 - async update( 266 - cb: ( 267 - prefs: AppBskyActorDefs.Preferences, 268 - ) => AppBskyActorDefs.Preferences | false, 269 - ) { 270 - await this.lock.acquireAsync() 271 - try { 272 - const res = await this.rootStore.agent.app.bsky.actor.getPreferences({}) 273 - const newPrefs = cb(res.data.preferences) 274 - if (newPrefs === false) { 275 - return 276 - } 277 - await this.rootStore.agent.app.bsky.actor.putPreferences({ 278 - preferences: newPrefs, 279 - }) 280 - } finally { 281 - this.lock.release() 282 - } 283 224 } 284 225 285 226 /** ··· 381 322 value: LabelPreference, 382 323 ) { 383 324 this.contentLabels[key] = value 384 - 385 - await this.update((prefs: AppBskyActorDefs.Preferences) => { 386 - const existing = prefs.find( 387 - pref => 388 - AppBskyActorDefs.isContentLabelPref(pref) && 389 - AppBskyActorDefs.validateAdultContentPref(pref).success && 390 - pref.label === key, 391 - ) 392 - if (existing) { 393 - existing.visibility = value 394 - } else { 395 - prefs.push({ 396 - $type: 'app.bsky.actor.defs#contentLabelPref', 397 - label: key, 398 - visibility: value, 399 - }) 400 - } 401 - return prefs 402 - }) 325 + await this.rootStore.agent.setContentLabelPref(key, value) 403 326 } 404 327 405 328 async setAdultContentEnabled(v: boolean) { 406 329 this.adultContentEnabled = v 407 - await this.update((prefs: AppBskyActorDefs.Preferences) => { 408 - const existing = prefs.find( 409 - pref => 410 - AppBskyActorDefs.isAdultContentPref(pref) && 411 - AppBskyActorDefs.validateAdultContentPref(pref).success, 412 - ) 413 - if (existing) { 414 - existing.enabled = v 415 - } else { 416 - prefs.push({ 417 - $type: 'app.bsky.actor.defs#adultContentPref', 418 - enabled: v, 419 - }) 420 - } 421 - return prefs 422 - }) 423 - } 424 - 425 - getLabelPreference(labels: ComAtprotoLabelDefs.Label[] | undefined): { 426 - pref: LabelPreference 427 - desc: LabelValGroup 428 - } { 429 - let res: {pref: LabelPreference; desc: LabelValGroup} = { 430 - pref: 'show', 431 - desc: UNKNOWN_LABEL_GROUP, 432 - } 433 - if (!labels?.length) { 434 - return res 435 - } 436 - for (const label of labels) { 437 - const group = getLabelValueGroup(label.val) 438 - if (group.id === 'illegal') { 439 - return {pref: 'hide', desc: ILLEGAL_LABEL_GROUP} 440 - } else if (group.id === 'always-filter') { 441 - return {pref: 'hide', desc: ALWAYS_FILTER_LABEL_GROUP} 442 - } else if (group.id === 'always-warn') { 443 - res.pref = 'warn' 444 - res.desc = ALWAYS_WARN_LABEL_GROUP 445 - continue 446 - } else if (group.id === 'unknown') { 447 - continue 448 - } 449 - let pref = this.contentLabels[group.id] 450 - if (pref === 'hide') { 451 - res.pref = 'hide' 452 - res.desc = group 453 - } else if (pref === 'warn' && res.pref === 'show') { 454 - res.pref = 'warn' 455 - res.desc = group 456 - } 457 - } 458 - if (res.desc.isAdultImagery && !this.adultContentEnabled) { 459 - res.pref = 'hide' 460 - } 461 - return res 330 + await this.rootStore.agent.setAdultContentEnabled(v) 462 331 } 463 332 464 333 get moderationOpts(): ModerationOpts { ··· 499 368 } 500 369 } 501 370 502 - async setSavedFeeds(saved: string[], pinned: string[]) { 371 + async _optimisticUpdateSavedFeeds( 372 + saved: string[], 373 + pinned: string[], 374 + cb: () => Promise<{saved: string[]; pinned: string[]}>, 375 + ) { 503 376 const oldSaved = this.savedFeeds 504 377 const oldPinned = this.pinnedFeeds 505 378 this.savedFeeds = saved 506 379 this.pinnedFeeds = pinned 507 380 try { 508 - await this.update((prefs: AppBskyActorDefs.Preferences) => { 509 - let feedsPref = prefs.find( 510 - pref => 511 - AppBskyActorDefs.isSavedFeedsPref(pref) && 512 - AppBskyActorDefs.validateSavedFeedsPref(pref).success, 513 - ) 514 - if (feedsPref) { 515 - feedsPref.saved = saved 516 - feedsPref.pinned = pinned 517 - } else { 518 - feedsPref = { 519 - $type: 'app.bsky.actor.defs#savedFeedsPref', 520 - saved, 521 - pinned, 522 - } 523 - } 524 - return prefs 525 - .filter(pref => !AppBskyActorDefs.isSavedFeedsPref(pref)) 526 - .concat([feedsPref]) 381 + const res = await cb() 382 + runInAction(() => { 383 + this.savedFeeds = res.saved 384 + this.pinnedFeeds = res.pinned 527 385 }) 528 386 } catch (e) { 529 387 runInAction(() => { ··· 534 392 } 535 393 } 536 394 395 + async setSavedFeeds(saved: string[], pinned: string[]) { 396 + return this._optimisticUpdateSavedFeeds(saved, pinned, () => 397 + this.rootStore.agent.setSavedFeeds(saved, pinned), 398 + ) 399 + } 400 + 537 401 async addSavedFeed(v: string) { 538 - return this.setSavedFeeds([...this.savedFeeds, v], this.pinnedFeeds) 402 + return this._optimisticUpdateSavedFeeds( 403 + [...this.savedFeeds, v], 404 + this.pinnedFeeds, 405 + () => this.rootStore.agent.addSavedFeed(v), 406 + ) 539 407 } 540 408 541 409 async removeSavedFeed(v: string) { 542 - return this.setSavedFeeds( 410 + return this._optimisticUpdateSavedFeeds( 543 411 this.savedFeeds.filter(uri => uri !== v), 544 412 this.pinnedFeeds.filter(uri => uri !== v), 413 + () => this.rootStore.agent.removeSavedFeed(v), 545 414 ) 546 415 } 547 416 548 417 async addPinnedFeed(v: string) { 549 - return this.setSavedFeeds(this.savedFeeds, [...this.pinnedFeeds, v]) 418 + return this._optimisticUpdateSavedFeeds( 419 + this.savedFeeds, 420 + [...this.pinnedFeeds, v], 421 + () => this.rootStore.agent.addPinnedFeed(v), 422 + ) 550 423 } 551 424 552 425 async removePinnedFeed(v: string) { 553 - return this.setSavedFeeds( 426 + return this._optimisticUpdateSavedFeeds( 554 427 this.savedFeeds, 555 428 this.pinnedFeeds.filter(uri => uri !== v), 429 + () => this.rootStore.agent.removePinnedFeed(v), 556 430 ) 557 431 } 558 432
+12 -10
src/view/com/modals/ContentFilteringSettings.tsx
··· 48 48 <ScrollView style={styles.scrollContainer}> 49 49 <View style={s.mb10}> 50 50 {isIOS ? ( 51 - <Text type="md" style={pal.textLight}> 52 - Adult content can only be enabled via the Web at{' '} 53 - <TextLink 54 - style={pal.link} 55 - href="https://bsky.app" 56 - text="bsky.app" 57 - /> 58 - . 59 - </Text> 51 + store.preferences.adultContentEnabled ? null : ( 52 + <Text type="md" style={pal.textLight}> 53 + Adult content can only be enabled via the Web at{' '} 54 + <TextLink 55 + style={pal.link} 56 + href="https://bsky.app" 57 + text="bsky.app" 58 + /> 59 + . 60 + </Text> 61 + ) 60 62 ) : ( 61 63 <ToggleButton 62 64 type="default-light" ··· 188 190 /> 189 191 <SelectableBtn 190 192 current={current} 191 - value="show" 193 + value="ignore" 192 194 label="Show" 193 195 right 194 196 onChange={onChange}
+12 -5
yarn.lock
··· 45 45 tlds "^1.234.0" 46 46 typed-emitter "^2.1.0" 47 47 48 - "@atproto/api@^0.6.6": 49 - version "0.6.6" 50 - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.6.6.tgz#c1bfdb6bc7dee9cdba1901cde0081c2d422d7c29" 51 - integrity sha512-j+yNTjllVxuTc4bAegghTopju7MdhczLXWvWIli40uXwCzQ3JjS1mFr/47eETtysib2phWYQvfhtCrqQq6AAig== 48 + "@atproto/api@^0.6.8": 49 + version "0.6.8" 50 + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.6.8.tgz#fe77f3ab2e7a815edca1357b0a89a7496be8f764" 51 + integrity sha512-WmXpIbO79f85UA8AzvvSqKibojBXN1HT3KEHhUOqXJRW8X1trYijgWIXwhnxhoBQXgiQfzKG7HdORvRjmRSLoQ== 52 52 dependencies: 53 53 "@atproto/common-web" "*" 54 - "@atproto/uri" "*" 54 + "@atproto/syntax" "*" 55 55 "@atproto/xrpc" "*" 56 56 tlds "^1.234.0" 57 57 typed-emitter "^2.1.0" ··· 316 316 multiformats "^9.6.4" 317 317 uint8arrays "3.0.0" 318 318 zod "^3.21.4" 319 + 320 + "@atproto/syntax@*": 321 + version "0.1.0" 322 + resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.1.0.tgz#f13b9dad8d13342cc54295ecd702ea13c82c9bf0" 323 + integrity sha512-Ktui0qvIXt1o1Px1KKC0eqn69MfRHQ9ok5EwjcxIEPbJ8OY5XqkeyJneFDIWRJZiR6vqPHfjFYRUpTB+jNPfRQ== 324 + dependencies: 325 + "@atproto/common-web" "*" 319 326 320 327 "@atproto/uri@*": 321 328 version "0.1.0"