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 consistencyzio/stable
parent
3a90b479fd
commit
a29f10aefe
|
@ -24,7 +24,7 @@
|
||||||
"e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all"
|
"e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/api": "^0.6.6",
|
"@atproto/api": "^0.6.8",
|
||||||
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
||||||
"@braintree/sanitize-url": "^6.0.2",
|
"@braintree/sanitize-url": "^6.0.2",
|
||||||
"@emoji-mart/react": "^1.1.1",
|
"@emoji-mart/react": "^1.1.1",
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {ComAtprotoLabelDefs, AppBskyGraphDefs} from '@atproto/api'
|
import {ComAtprotoLabelDefs} from '@atproto/api'
|
||||||
import {LabelPreferencesModel} from 'state/models/ui/preferences'
|
import {LabelPreferencesModel} from 'state/models/ui/preferences'
|
||||||
|
|
||||||
export type Label = ComAtprotoLabelDefs.Label
|
export type Label = ComAtprotoLabelDefs.Label
|
||||||
|
@ -16,54 +16,3 @@ export interface LabelValGroup {
|
||||||
warning: string
|
warning: string
|
||||||
values: 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
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,21 +4,9 @@ import AwaitLock from 'await-lock'
|
||||||
import isEqual from 'lodash.isequal'
|
import isEqual from 'lodash.isequal'
|
||||||
import {isObj, hasProp} from 'lib/type-guards'
|
import {isObj, hasProp} from 'lib/type-guards'
|
||||||
import {RootStoreModel} from '../root-store'
|
import {RootStoreModel} from '../root-store'
|
||||||
import {
|
import {ModerationOpts} from '@atproto/api'
|
||||||
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 {DEFAULT_FEEDS} from 'lib/constants'
|
import {DEFAULT_FEEDS} from 'lib/constants'
|
||||||
import {isIOS, deviceLocales} from 'platform/detection'
|
import {deviceLocales} from 'platform/detection'
|
||||||
import {LANGUAGES} from '../../../locale/languages'
|
import {LANGUAGES} from '../../../locale/languages'
|
||||||
|
|
||||||
// TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf
|
// TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf
|
||||||
|
@ -32,7 +20,7 @@ const LABEL_GROUPS = [
|
||||||
'spam',
|
'spam',
|
||||||
'impersonation',
|
'impersonation',
|
||||||
]
|
]
|
||||||
const VISIBILITY_VALUES = ['show', 'warn', 'hide']
|
const VISIBILITY_VALUES = ['ignore', 'warn', 'hide']
|
||||||
const DEFAULT_LANG_CODES = (deviceLocales || [])
|
const DEFAULT_LANG_CODES = (deviceLocales || [])
|
||||||
.concat(['en', 'ja', 'pt', 'de'])
|
.concat(['en', 'ja', 'pt', 'de'])
|
||||||
.slice(0, 6)
|
.slice(0, 6)
|
||||||
|
@ -52,7 +40,7 @@ export class LabelPreferencesModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PreferencesModel {
|
export class PreferencesModel {
|
||||||
adultContentEnabled = !isIOS
|
adultContentEnabled = false
|
||||||
contentLanguages: string[] = deviceLocales || []
|
contentLanguages: string[] = deviceLocales || []
|
||||||
postLanguage: string = deviceLocales[0] || 'en'
|
postLanguage: string = deviceLocales[0] || 'en'
|
||||||
postLanguageHistory: string[] = DEFAULT_LANG_CODES
|
postLanguageHistory: string[] = DEFAULT_LANG_CODES
|
||||||
|
@ -189,43 +177,32 @@ export class PreferencesModel {
|
||||||
await this.lock.acquireAsync()
|
await this.lock.acquireAsync()
|
||||||
try {
|
try {
|
||||||
// fetch preferences
|
// fetch preferences
|
||||||
let hasSavedFeedsPref = false
|
const prefs = await this.rootStore.agent.getPreferences()
|
||||||
const res = await this.rootStore.agent.app.bsky.actor.getPreferences({})
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
for (const pref of res.data.preferences) {
|
this.adultContentEnabled = prefs.adultContentEnabled
|
||||||
|
for (const label in prefs.contentLabels) {
|
||||||
if (
|
if (
|
||||||
AppBskyActorDefs.isAdultContentPref(pref) &&
|
LABEL_GROUPS.includes(label) &&
|
||||||
AppBskyActorDefs.validateAdultContentPref(pref).success
|
VISIBILITY_VALUES.includes(prefs.contentLabels[label])
|
||||||
) {
|
) {
|
||||||
this.adultContentEnabled = pref.enabled
|
this.contentLabels[label as keyof LabelPreferencesModel] =
|
||||||
} else if (
|
prefs.contentLabels[label]
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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
|
// set defaults on missing items
|
||||||
if (!hasSavedFeedsPref) {
|
if (typeof prefs.feeds.saved === 'undefined') {
|
||||||
const {saved, pinned} = await DEFAULT_FEEDS(
|
const {saved, pinned} = await DEFAULT_FEEDS(
|
||||||
this.rootStore.agent.service.toString(),
|
this.rootStore.agent.service.toString(),
|
||||||
(handle: string) =>
|
(handle: string) =>
|
||||||
|
@ -237,14 +214,7 @@ export class PreferencesModel {
|
||||||
this.savedFeeds = saved
|
this.savedFeeds = saved
|
||||||
this.pinnedFeeds = pinned
|
this.pinnedFeeds = pinned
|
||||||
})
|
})
|
||||||
res.data.preferences.push({
|
await this.rootStore.agent.setSavedFeeds(saved, pinned)
|
||||||
$type: 'app.bsky.actor.defs#savedFeedsPref',
|
|
||||||
saved,
|
|
||||||
pinned,
|
|
||||||
})
|
|
||||||
await this.rootStore.agent.app.bsky.actor.putPreferences({
|
|
||||||
preferences: res.data.preferences,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.lock.release()
|
this.lock.release()
|
||||||
|
@ -253,35 +223,6 @@ export class PreferencesModel {
|
||||||
await this.rootStore.me.savedFeeds.updateCache(clearCache)
|
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.
|
* This function resets the preferences to an empty array of no preferences.
|
||||||
*/
|
*/
|
||||||
|
@ -381,84 +322,12 @@ export class PreferencesModel {
|
||||||
value: LabelPreference,
|
value: LabelPreference,
|
||||||
) {
|
) {
|
||||||
this.contentLabels[key] = value
|
this.contentLabels[key] = value
|
||||||
|
await this.rootStore.agent.setContentLabelPref(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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async setAdultContentEnabled(v: boolean) {
|
async setAdultContentEnabled(v: boolean) {
|
||||||
this.adultContentEnabled = v
|
this.adultContentEnabled = v
|
||||||
await this.update((prefs: AppBskyActorDefs.Preferences) => {
|
await this.rootStore.agent.setAdultContentEnabled(v)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get moderationOpts(): ModerationOpts {
|
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 oldSaved = this.savedFeeds
|
||||||
const oldPinned = this.pinnedFeeds
|
const oldPinned = this.pinnedFeeds
|
||||||
this.savedFeeds = saved
|
this.savedFeeds = saved
|
||||||
this.pinnedFeeds = pinned
|
this.pinnedFeeds = pinned
|
||||||
try {
|
try {
|
||||||
await this.update((prefs: AppBskyActorDefs.Preferences) => {
|
const res = await cb()
|
||||||
let feedsPref = prefs.find(
|
runInAction(() => {
|
||||||
pref =>
|
this.savedFeeds = res.saved
|
||||||
AppBskyActorDefs.isSavedFeedsPref(pref) &&
|
this.pinnedFeeds = res.pinned
|
||||||
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])
|
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
runInAction(() => {
|
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) {
|
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) {
|
async removeSavedFeed(v: string) {
|
||||||
return this.setSavedFeeds(
|
return this._optimisticUpdateSavedFeeds(
|
||||||
this.savedFeeds.filter(uri => uri !== v),
|
this.savedFeeds.filter(uri => uri !== v),
|
||||||
this.pinnedFeeds.filter(uri => uri !== v),
|
this.pinnedFeeds.filter(uri => uri !== v),
|
||||||
|
() => this.rootStore.agent.removeSavedFeed(v),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async addPinnedFeed(v: string) {
|
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) {
|
async removePinnedFeed(v: string) {
|
||||||
return this.setSavedFeeds(
|
return this._optimisticUpdateSavedFeeds(
|
||||||
this.savedFeeds,
|
this.savedFeeds,
|
||||||
this.pinnedFeeds.filter(uri => uri !== v),
|
this.pinnedFeeds.filter(uri => uri !== v),
|
||||||
|
() => this.rootStore.agent.removePinnedFeed(v),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,15 +48,17 @@ export const Component = observer(({}: {}) => {
|
||||||
<ScrollView style={styles.scrollContainer}>
|
<ScrollView style={styles.scrollContainer}>
|
||||||
<View style={s.mb10}>
|
<View style={s.mb10}>
|
||||||
{isIOS ? (
|
{isIOS ? (
|
||||||
<Text type="md" style={pal.textLight}>
|
store.preferences.adultContentEnabled ? null : (
|
||||||
Adult content can only be enabled via the Web at{' '}
|
<Text type="md" style={pal.textLight}>
|
||||||
<TextLink
|
Adult content can only be enabled via the Web at{' '}
|
||||||
style={pal.link}
|
<TextLink
|
||||||
href="https://bsky.app"
|
style={pal.link}
|
||||||
text="bsky.app"
|
href="https://bsky.app"
|
||||||
/>
|
text="bsky.app"
|
||||||
.
|
/>
|
||||||
</Text>
|
.
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
type="default-light"
|
type="default-light"
|
||||||
|
@ -188,7 +190,7 @@ function SelectGroup({current, onChange, group}: SelectGroupProps) {
|
||||||
/>
|
/>
|
||||||
<SelectableBtn
|
<SelectableBtn
|
||||||
current={current}
|
current={current}
|
||||||
value="show"
|
value="ignore"
|
||||||
label="Show"
|
label="Show"
|
||||||
right
|
right
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
|
17
yarn.lock
17
yarn.lock
|
@ -45,13 +45,13 @@
|
||||||
tlds "^1.234.0"
|
tlds "^1.234.0"
|
||||||
typed-emitter "^2.1.0"
|
typed-emitter "^2.1.0"
|
||||||
|
|
||||||
"@atproto/api@^0.6.6":
|
"@atproto/api@^0.6.8":
|
||||||
version "0.6.6"
|
version "0.6.8"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.6.6.tgz#c1bfdb6bc7dee9cdba1901cde0081c2d422d7c29"
|
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.6.8.tgz#fe77f3ab2e7a815edca1357b0a89a7496be8f764"
|
||||||
integrity sha512-j+yNTjllVxuTc4bAegghTopju7MdhczLXWvWIli40uXwCzQ3JjS1mFr/47eETtysib2phWYQvfhtCrqQq6AAig==
|
integrity sha512-WmXpIbO79f85UA8AzvvSqKibojBXN1HT3KEHhUOqXJRW8X1trYijgWIXwhnxhoBQXgiQfzKG7HdORvRjmRSLoQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@atproto/common-web" "*"
|
"@atproto/common-web" "*"
|
||||||
"@atproto/uri" "*"
|
"@atproto/syntax" "*"
|
||||||
"@atproto/xrpc" "*"
|
"@atproto/xrpc" "*"
|
||||||
tlds "^1.234.0"
|
tlds "^1.234.0"
|
||||||
typed-emitter "^2.1.0"
|
typed-emitter "^2.1.0"
|
||||||
|
@ -317,6 +317,13 @@
|
||||||
uint8arrays "3.0.0"
|
uint8arrays "3.0.0"
|
||||||
zod "^3.21.4"
|
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@*":
|
"@atproto/uri@*":
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/uri/-/uri-0.1.0.tgz#1cb4695d2f1766ec8d542af6a495a416f6f6c214"
|
resolved "https://registry.yarnpkg.com/@atproto/uri/-/uri-0.1.0.tgz#1cb4695d2f1766ec8d542af6a495a416f6f6c214"
|
||||||
|
|
Loading…
Reference in New Issue