Skip to content

Commit

Permalink
Add feed-vew and thread-view preferences (#1638)
Browse files Browse the repository at this point in the history
* Add feed and thread preference lexicons

* Add feed-view and thread-view preference APIs
  • Loading branch information
pfrazee authored Sep 20, 2023
1 parent 828dfa8 commit 061865c
Show file tree
Hide file tree
Showing 10 changed files with 908 additions and 18 deletions.
48 changes: 47 additions & 1 deletion lexicons/app/bsky/actor/defs.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@
"#adultContentPref",
"#contentLabelPref",
"#savedFeedsPref",
"#personalDetailsPref"
"#personalDetailsPref",
"#feedViewPref",
"#threadViewPref"
]
}
},
Expand Down Expand Up @@ -149,6 +151,50 @@
"description": "The birth date of the owner of the account."
}
}
},
"feedViewPref": {
"type": "object",
"required": ["feed"],
"properties": {
"feed": {
"type": "string",
"description": "The URI of the feed, or an identifier which describes the feed."
},
"hideReplies": {
"type": "boolean",
"description": "Hide replies in the feed."
},
"hideRepliesByUnfollowed": {
"type": "boolean",
"description": "Hide replies in the feed if they are not by followed users."
},
"hideRepliesByLikeCount": {
"type": "integer",
"description": "Hide replies in the feed if they do not have this number of likes."
},
"hideReposts": {
"type": "boolean",
"description": "Hide reposts in the feed."
},
"hideQuotePosts": {
"type": "boolean",
"description": "Hide quote posts in the feed."
}
}
},
"threadViewPref": {
"type": "object",
"properties": {
"sort": {
"type": "string",
"description": "Sorting mode.",
"knownValues": ["oldest", "newest", "most-likes", "random"]
},
"prioritizeFollowedUsers": {
"type": "boolean",
"description": "Show followed users at the top of all replies."
}
}
}
}
}
70 changes: 70 additions & 0 deletions packages/api/src/bsky-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ import {
} from './client'
import { BskyPreferences, BskyLabelPreference } from './types'

const FEED_VIEW_PREF_DEFAULTS = {
hideReplies: false,
hideRepliesByUnfollowed: false,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
}
const THREAD_VIEW_PREF_DEFAULTS = {
sort: 'oldest',
prioritizeFollowedUsers: true,
}

declare global {
interface Array<T> {
findLast(
Expand Down Expand Up @@ -254,6 +266,12 @@ export class BskyAgent extends AtpAgent {
saved: undefined,
pinned: undefined,
},
feedViewPrefs: {
home: {
...FEED_VIEW_PREF_DEFAULTS,
},
},
threadViewPrefs: { ...THREAD_VIEW_PREF_DEFAULTS },
adultContentEnabled: false,
contentLabels: {},
birthDate: undefined,
Expand Down Expand Up @@ -289,6 +307,18 @@ export class BskyAgent extends AtpAgent {
if (pref.birthDate) {
prefs.birthDate = new Date(pref.birthDate)
}
} else if (
AppBskyActorDefs.isFeedViewPref(pref) &&
AppBskyActorDefs.validateFeedViewPref(pref).success
) {
const { $type, feed, ...v } = pref
prefs.feedViewPrefs[pref.feed] = { ...FEED_VIEW_PREF_DEFAULTS, ...v }
} else if (
AppBskyActorDefs.isThreadViewPref(pref) &&
AppBskyActorDefs.validateThreadViewPref(pref).success
) {
const { $type, ...v } = pref
prefs.threadViewPrefs = { ...prefs.threadViewPrefs, ...v }
}
}
return prefs
Expand Down Expand Up @@ -406,6 +436,46 @@ export class BskyAgent extends AtpAgent {
.concat([personalDetailsPref])
})
}

async setFeedViewPrefs(
feed: string,
pref: Omit<AppBskyActorDefs.FeedViewPref, '$type' | 'feed'>,
) {
await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => {
const existing = prefs.findLast(
(pref) =>
AppBskyActorDefs.isFeedViewPref(pref) &&
AppBskyActorDefs.validateFeedViewPref(pref).success &&
pref.feed === feed,
)
if (existing) {
pref = { ...existing, ...pref }
}
return prefs
.filter(
(p) => !AppBskyActorDefs.isFeedViewPref(pref) || p.feed !== feed,
)
.concat([{ ...pref, $type: 'app.bsky.actor.defs#feedViewPref', feed }])
})
}

async setThreadViewPrefs(
pref: Omit<AppBskyActorDefs.ThreadViewPref, '$type'>,
) {
await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => {
const existing = prefs.findLast(
(pref) =>
AppBskyActorDefs.isThreadViewPref(pref) &&
AppBskyActorDefs.validateThreadViewPref(pref).success,
)
if (existing) {
pref = { ...existing, ...pref }
}
return prefs
.filter((p) => !AppBskyActorDefs.isThreadViewPref(p))
.concat([{ ...pref, $type: 'app.bsky.actor.defs#threadViewPref' }])
})
}
}

/**
Expand Down
49 changes: 49 additions & 0 deletions packages/api/src/client/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3687,6 +3687,8 @@ export const schemaDict = {
'lex:app.bsky.actor.defs#contentLabelPref',
'lex:app.bsky.actor.defs#savedFeedsPref',
'lex:app.bsky.actor.defs#personalDetailsPref',
'lex:app.bsky.actor.defs#feedViewPref',
'lex:app.bsky.actor.defs#threadViewPref',
],
},
},
Expand Down Expand Up @@ -3743,6 +3745,53 @@ export const schemaDict = {
},
},
},
feedViewPref: {
type: 'object',
required: ['feed'],
properties: {
feed: {
type: 'string',
description:
'The URI of the feed, or an identifier which describes the feed.',
},
hideReplies: {
type: 'boolean',
description: 'Hide replies in the feed.',
},
hideRepliesByUnfollowed: {
type: 'boolean',
description:
'Hide replies in the feed if they are not by followed users.',
},
hideRepliesByLikeCount: {
type: 'integer',
description:
'Hide replies in the feed if they do not have this number of likes.',
},
hideReposts: {
type: 'boolean',
description: 'Hide reposts in the feed.',
},
hideQuotePosts: {
type: 'boolean',
description: 'Hide quote posts in the feed.',
},
},
},
threadViewPref: {
type: 'object',
properties: {
sort: {
type: 'string',
description: 'Sorting mode.',
knownValues: ['oldest', 'newest', 'most-likes', 'random'],
},
prioritizeFollowedUsers: {
type: 'boolean',
description: 'Show followed users at the top of all replies.',
},
},
},
},
},
AppBskyActorGetPreferences: {
Expand Down
50 changes: 50 additions & 0 deletions packages/api/src/client/types/app/bsky/actor/defs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ export type Preferences = (
| ContentLabelPref
| SavedFeedsPref
| PersonalDetailsPref
| FeedViewPref
| ThreadViewPref
| { $type: string; [k: string]: unknown }
)[]

Expand Down Expand Up @@ -182,3 +184,51 @@ export function isPersonalDetailsPref(v: unknown): v is PersonalDetailsPref {
export function validatePersonalDetailsPref(v: unknown): ValidationResult {
return lexicons.validate('app.bsky.actor.defs#personalDetailsPref', v)
}

export interface FeedViewPref {
/** The URI of the feed, or an identifier which describes the feed. */
feed: string
/** Hide replies in the feed. */
hideReplies?: boolean
/** Hide replies in the feed if they are not by followed users. */
hideRepliesByUnfollowed?: boolean
/** Hide replies in the feed if they do not have this number of likes. */
hideRepliesByLikeCount?: number
/** Hide reposts in the feed. */
hideReposts?: boolean
/** Hide quote posts in the feed. */
hideQuotePosts?: boolean
[k: string]: unknown
}

export function isFeedViewPref(v: unknown): v is FeedViewPref {
return (
isObj(v) &&
hasProp(v, '$type') &&
v.$type === 'app.bsky.actor.defs#feedViewPref'
)
}

export function validateFeedViewPref(v: unknown): ValidationResult {
return lexicons.validate('app.bsky.actor.defs#feedViewPref', v)
}

export interface ThreadViewPref {
/** Sorting mode. */
sort?: 'oldest' | 'newest' | 'most-likes' | 'random' | (string & {})
/** Show followed users at the top of all replies. */
prioritizeFollowedUsers?: boolean
[k: string]: unknown
}

export function isThreadViewPref(v: unknown): v is ThreadViewPref {
return (
isObj(v) &&
hasProp(v, '$type') &&
v.$type === 'app.bsky.actor.defs#threadViewPref'
)
}

export function validateThreadViewPref(v: unknown): ValidationResult {
return lexicons.validate('app.bsky.actor.defs#threadViewPref', v)
}
3 changes: 3 additions & 0 deletions packages/api/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { LabelPreference } from './moderation/types'
import { AppBskyActorDefs } from './client'

/**
* Used by the PersistSessionHandler to indicate what change occurred
Expand Down Expand Up @@ -87,6 +88,8 @@ export interface BskyPreferences {
saved?: string[]
pinned?: string[]
}
feedViewPrefs: Record<string, Omit<AppBskyActorDefs.FeedViewPref, '$type'>>
threadViewPrefs: Omit<AppBskyActorDefs.ThreadViewPref, '$type'>
adultContentEnabled: boolean
contentLabels: Record<string, BskyLabelPreference>
birthDate: Date | undefined
Expand Down
Loading

0 comments on commit 061865c

Please sign in to comment.