From 33aa0c722226a18215af0ae1833c7c552fc7aaa7 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 11 Sep 2024 18:25:05 -0500 Subject: [PATCH] NUX API (#2810) * Codegen * Explicitly add Zod (already a peer dep) and validation to api * Add Nux methods * Match naming convention * Remove id, it won't be used * Add tests * Use id instead of name, little clearer * Update API contracts * Update tests * Changeset * Don't mutate --- .changeset/selfish-ads-itch.md | 7 ++ lexicons/app/bsky/actor/defs.json | 35 +++++++ packages/api/package.json | 3 +- packages/api/src/agent.ts | 81 ++++++++++++++++- packages/api/src/client/lexicons.ts | 37 ++++++++ .../src/client/types/app/bsky/actor/defs.ts | 23 +++++ packages/api/src/types.ts | 1 + packages/api/src/util.ts | 15 +++ packages/api/tests/bsky-agent.test.ts | 91 +++++++++++++++++++ packages/api/tests/moderation-prefs.test.ts | 4 + packages/bsky/src/lexicon/lexicons.ts | 37 ++++++++ .../src/lexicon/types/app/bsky/actor/defs.ts | 23 +++++ packages/ozone/src/lexicon/lexicons.ts | 37 ++++++++ .../src/lexicon/types/app/bsky/actor/defs.ts | 23 +++++ packages/pds/src/lexicon/lexicons.ts | 37 ++++++++ .../src/lexicon/types/app/bsky/actor/defs.ts | 23 +++++ pnpm-lock.yaml | 3 + 17 files changed, 478 insertions(+), 2 deletions(-) create mode 100644 .changeset/selfish-ads-itch.md diff --git a/.changeset/selfish-ads-itch.md b/.changeset/selfish-ads-itch.md new file mode 100644 index 00000000000..23fa5c09b81 --- /dev/null +++ b/.changeset/selfish-ads-itch.md @@ -0,0 +1,7 @@ +--- +"@atproto/bsky": patch +"@atproto/api": patch +"@atproto/pds": patch +--- + +Add NUX API diff --git a/lexicons/app/bsky/actor/defs.json b/lexicons/app/bsky/actor/defs.json index 6ba7aaa734a..cf2452e2bf6 100644 --- a/lexicons/app/bsky/actor/defs.json +++ b/lexicons/app/bsky/actor/defs.json @@ -419,6 +419,15 @@ "type": "array", "maxLength": 1000, "items": { "type": "string", "maxLength": 100 } + }, + "nuxs": { + "description": "Storage for NUXs the user has encountered.", + "type": "array", + "maxLength": 100, + "items": { + "type": "ref", + "ref": "app.bsky.actor.defs#nux" + } } } }, @@ -429,6 +438,32 @@ "properties": { "guide": { "type": "string", "maxLength": 100 } } + }, + "nux": { + "type": "object", + "description": "A new user experiences (NUX) storage object", + "required": ["id", "completed"], + "properties": { + "id": { + "type": "string", + "maxLength": 100 + }, + "completed": { + "type": "boolean", + "default": false + }, + "data": { + "description": "Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters.", + "type": "string", + "maxLength": 3000, + "maxGraphemes": 300 + }, + "expiresAt": { + "type": "string", + "format": "datetime", + "description": "The date and time at which the NUX will expire and should be considered completed." + } + } } } } diff --git a/packages/api/package.json b/packages/api/package.json index 326f187f17c..3a335acee67 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -28,7 +28,8 @@ "@atproto/xrpc": "workspace:^", "await-lock": "^2.2.2", "multiformats": "^9.9.0", - "tlds": "^1.234.0" + "tlds": "^1.234.0", + "zod": "^3.23.8" }, "devDependencies": { "@atproto/lex-cli": "workspace:^", diff --git a/packages/api/src/agent.ts b/packages/api/src/agent.ts index 7c54bb755f3..4f2ce6da015 100644 --- a/packages/api/src/agent.ts +++ b/packages/api/src/agent.ts @@ -14,7 +14,7 @@ import { ToolsNS, } from './client/index' import { schemas } from './client/lexicons' -import { MutedWord } from './client/types/app/bsky/actor/defs' +import { MutedWord, Nux } from './client/types/app/bsky/actor/defs' import { BSKY_LABELER_DID } from './const' import { interpretLabelValueDefinitions } from './moderation' import { DEFAULT_LABEL_SETTINGS } from './moderation/const/labels' @@ -40,6 +40,7 @@ import { sanitizeMutedWordValue, savedFeedsToUriArrays, validateSavedFeed, + validateNux, } from './util' const FEED_VIEW_PREF_DEFAULTS = { @@ -570,6 +571,7 @@ export class Agent extends XrpcClient { bskyAppState: { queuedNudges: [], activeProgressGuide: undefined, + nuxs: [], }, } const res = await this.app.bsky.actor.getPreferences({}) @@ -674,6 +676,7 @@ export class Agent extends XrpcClient { const { $type, ...v } = pref prefs.bskyAppState.queuedNudges = v.queuedNudges || [] prefs.bskyAppState.activeProgressGuide = v.activeProgressGuide + prefs.bskyAppState.nuxs = v.nuxs || [] } } @@ -1374,6 +1377,82 @@ export class Agent extends XrpcClient { }) } + /** + * Insert or update a NUX in user prefs + */ + async bskyAppUpsertNux(nux: Nux) { + validateNux(nux) + + await this.updatePreferences((prefs: AppBskyActorDefs.Preferences) => { + let bskyAppStatePref: AppBskyActorDefs.BskyAppStatePref = prefs.findLast( + (pref) => + AppBskyActorDefs.isBskyAppStatePref(pref) && + AppBskyActorDefs.validateBskyAppStatePref(pref).success, + ) + + bskyAppStatePref = bskyAppStatePref || {} + bskyAppStatePref.nuxs = bskyAppStatePref.nuxs || [] + + const existing = bskyAppStatePref.nuxs?.find((n) => { + return n.id === nux.id + }) + + let next: AppBskyActorDefs.Nux + + if (existing) { + next = { + id: existing.id, + completed: nux.completed, + data: nux.data, + expiresAt: nux.expiresAt, + } + } else { + next = nux + } + + // remove duplicates and append + bskyAppStatePref.nuxs = bskyAppStatePref.nuxs + .filter((n) => n.id !== nux.id) + .concat(next) + + return prefs + .filter((p) => !AppBskyActorDefs.isBskyAppStatePref(p)) + .concat([ + { + ...bskyAppStatePref, + $type: 'app.bsky.actor.defs#bskyAppStatePref', + }, + ]) + }) + } + + /** + * Removes NUXs from user preferences. + */ + async bskyAppRemoveNuxs(ids: string[]) { + await this.updatePreferences((prefs: AppBskyActorDefs.Preferences) => { + let bskyAppStatePref: AppBskyActorDefs.BskyAppStatePref = prefs.findLast( + (pref) => + AppBskyActorDefs.isBskyAppStatePref(pref) && + AppBskyActorDefs.validateBskyAppStatePref(pref).success, + ) + + bskyAppStatePref = bskyAppStatePref || {} + bskyAppStatePref.nuxs = (bskyAppStatePref.nuxs || []).filter((nux) => { + return !ids.includes(nux.id) + }) + + return prefs + .filter((p) => !AppBskyActorDefs.isBskyAppStatePref(p)) + .concat([ + { + ...bskyAppStatePref, + $type: 'app.bsky.actor.defs#bskyAppStatePref', + }, + ]) + }) + } + //- Private methods #prefsLock = new AwaitLock() diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 162a97ec751..51d39bcb6f1 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -4572,6 +4572,15 @@ export const schemaDict = { maxLength: 100, }, }, + nuxs: { + description: 'Storage for NUXs the user has encountered.', + type: 'array', + maxLength: 100, + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#nux', + }, + }, }, }, bskyAppProgressGuide: { @@ -4586,6 +4595,34 @@ export const schemaDict = { }, }, }, + nux: { + type: 'object', + description: 'A new user experiences (NUX) storage object', + required: ['id', 'completed'], + properties: { + id: { + type: 'string', + maxLength: 100, + }, + completed: { + type: 'boolean', + default: false, + }, + data: { + description: + 'Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters.', + type: 'string', + maxLength: 3000, + maxGraphemes: 300, + }, + expiresAt: { + type: 'string', + format: 'datetime', + description: + 'The date and time at which the NUX will expire and should be considered completed.', + }, + }, + }, }, }, AppBskyActorGetPreferences: { diff --git a/packages/api/src/client/types/app/bsky/actor/defs.ts b/packages/api/src/client/types/app/bsky/actor/defs.ts index d6c0de137b0..9f6deedf815 100644 --- a/packages/api/src/client/types/app/bsky/actor/defs.ts +++ b/packages/api/src/client/types/app/bsky/actor/defs.ts @@ -469,6 +469,8 @@ export interface BskyAppStatePref { activeProgressGuide?: BskyAppProgressGuide /** An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user. */ queuedNudges?: string[] + /** Storage for NUXs the user has encountered. */ + nuxs?: Nux[] [k: string]: unknown } @@ -501,3 +503,24 @@ export function isBskyAppProgressGuide(v: unknown): v is BskyAppProgressGuide { export function validateBskyAppProgressGuide(v: unknown): ValidationResult { return lexicons.validate('app.bsky.actor.defs#bskyAppProgressGuide', v) } + +/** A new user experiences (NUX) storage object */ +export interface Nux { + id: string + completed: boolean + /** Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters. */ + data?: string + /** The date and time at which the NUX will expire and should be considered completed. */ + expiresAt?: string + [k: string]: unknown +} + +export function isNux(v: unknown): v is Nux { + return ( + isObj(v) && hasProp(v, '$type') && v.$type === 'app.bsky.actor.defs#nux' + ) +} + +export function validateNux(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#nux', v) +} diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index f8878ec1581..582f9a70c15 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -106,5 +106,6 @@ export interface BskyPreferences { bskyAppState: { queuedNudges: string[] activeProgressGuide: AppBskyActorDefs.BskyAppProgressGuide | undefined + nuxs: AppBskyActorDefs.Nux[] } } diff --git a/packages/api/src/util.ts b/packages/api/src/util.ts index 196952ff557..145ffba7fbe 100644 --- a/packages/api/src/util.ts +++ b/packages/api/src/util.ts @@ -1,6 +1,8 @@ import { AtUri } from '@atproto/syntax' import { TID } from '@atproto/common-web' +import zod from 'zod' +import { Nux } from './client/types/app/bsky/actor/defs' import { AppBskyActorDefs } from './client' export function sanitizeMutedWordValue(value: string) { @@ -94,3 +96,16 @@ export const asDid = (value: string): Did => { if (isDid(value)) return value throw new TypeError(`Invalid DID: ${value}`) } + +export const nuxSchema = zod + .object({ + id: zod.string().max(64), + completed: zod.boolean(), + data: zod.string().max(300).optional(), + expiresAt: zod.string().datetime().optional(), + }) + .strict() + +export function validateNux(nux: Nux) { + nuxSchema.parse(nux) +} diff --git a/packages/api/tests/bsky-agent.test.ts b/packages/api/tests/bsky-agent.test.ts index 491ade3545f..81f7186aad7 100644 --- a/packages/api/tests/bsky-agent.test.ts +++ b/packages/api/tests/bsky-agent.test.ts @@ -276,6 +276,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -317,6 +318,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -358,6 +360,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -399,6 +402,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -444,6 +448,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -492,6 +497,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -540,6 +546,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -588,6 +595,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -636,6 +644,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -684,6 +693,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -738,6 +748,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -786,6 +797,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -834,6 +846,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -882,6 +895,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -930,6 +944,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -985,6 +1000,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -1040,6 +1056,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -1095,6 +1112,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -1150,6 +1168,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) }) @@ -1332,6 +1351,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: ['two'], + nuxs: [], }, }) @@ -1389,6 +1409,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: ['two'], + nuxs: [], }, }) @@ -1447,6 +1468,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: ['two'], + nuxs: [], }, }) @@ -1501,6 +1523,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: ['two'], + nuxs: [], }, }) @@ -1555,6 +1578,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: ['two'], + nuxs: [], }, }) @@ -1609,6 +1633,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: ['two'], + nuxs: [], }, }) @@ -1675,6 +1700,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: ['two', 'three'], + nuxs: [], }, }) @@ -3246,6 +3272,71 @@ describe('agent', () => { }) }) + describe('nuxs', () => { + let agent: BskyAgent + + const nux = { + id: 'a', + completed: false, + data: '{}', + expiresAt: new Date(Date.now() + 6e3).toISOString(), + } + + beforeAll(async () => { + agent = new BskyAgent({ service: network.pds.url }) + + await agent.createAccount({ + handle: 'nuxs.test', + email: 'nuxs@test.com', + password: 'password', + }) + }) + + it('bskyAppUpsertNux', async () => { + // never duplicates + await agent.bskyAppUpsertNux(nux) + await agent.bskyAppUpsertNux(nux) + await agent.bskyAppUpsertNux(nux) + + const prefs = await agent.getPreferences() + const nuxs = prefs.bskyAppState.nuxs + + expect(nuxs.length).toEqual(1) + expect(nuxs.find((n) => n.id === nux.id)).toEqual(nux) + }) + + it('bskyAppUpsertNux completed', async () => { + // never duplicates + await agent.bskyAppUpsertNux({ + ...nux, + completed: true, + }) + + const prefs = await agent.getPreferences() + const nuxs = prefs.bskyAppState.nuxs + + expect(nuxs.length).toEqual(1) + expect(nuxs.find((n) => n.id === nux.id)?.completed).toEqual(true) + }) + + it('bskyAppRemoveNuxs', async () => { + await agent.bskyAppRemoveNuxs([nux.id]) + + const prefs = await agent.getPreferences() + const nuxs = prefs.bskyAppState.nuxs + + expect(nuxs.length).toEqual(0) + }) + + it('bskyAppUpsertNux validates nux', async () => { + // @ts-expect-error + expect(() => agent.bskyAppUpsertNux({ name: 'a' })).rejects.toThrow() + expect(() => + agent.bskyAppUpsertNux({ id: 'a', completed: false, foo: 'bar' }), + ).rejects.toThrow() + }) + }) + // end }) }) diff --git a/packages/api/tests/moderation-prefs.test.ts b/packages/api/tests/moderation-prefs.test.ts index a166df30db6..9ce6b58c6f7 100644 --- a/packages/api/tests/moderation-prefs.test.ts +++ b/packages/api/tests/moderation-prefs.test.ts @@ -84,6 +84,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) }) @@ -133,6 +134,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) expect(agent.labelers).toStrictEqual(['did:plc:other']) @@ -167,6 +169,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) expect(agent.labelers).toStrictEqual([]) @@ -223,6 +226,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) }) diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 295d5beccdf..30e66d5af35 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -4572,6 +4572,15 @@ export const schemaDict = { maxLength: 100, }, }, + nuxs: { + description: 'Storage for NUXs the user has encountered.', + type: 'array', + maxLength: 100, + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#nux', + }, + }, }, }, bskyAppProgressGuide: { @@ -4586,6 +4595,34 @@ export const schemaDict = { }, }, }, + nux: { + type: 'object', + description: 'A new user experiences (NUX) storage object', + required: ['id', 'completed'], + properties: { + id: { + type: 'string', + maxLength: 100, + }, + completed: { + type: 'boolean', + default: false, + }, + data: { + description: + 'Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters.', + type: 'string', + maxLength: 3000, + maxGraphemes: 300, + }, + expiresAt: { + type: 'string', + format: 'datetime', + description: + 'The date and time at which the NUX will expire and should be considered completed.', + }, + }, + }, }, }, AppBskyActorGetPreferences: { diff --git a/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts b/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts index c7eadff70d7..926b468cb6d 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts @@ -469,6 +469,8 @@ export interface BskyAppStatePref { activeProgressGuide?: BskyAppProgressGuide /** An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user. */ queuedNudges?: string[] + /** Storage for NUXs the user has encountered. */ + nuxs?: Nux[] [k: string]: unknown } @@ -501,3 +503,24 @@ export function isBskyAppProgressGuide(v: unknown): v is BskyAppProgressGuide { export function validateBskyAppProgressGuide(v: unknown): ValidationResult { return lexicons.validate('app.bsky.actor.defs#bskyAppProgressGuide', v) } + +/** A new user experiences (NUX) storage object */ +export interface Nux { + id: string + completed: boolean + /** Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters. */ + data?: string + /** The date and time at which the NUX will expire and should be considered completed. */ + expiresAt?: string + [k: string]: unknown +} + +export function isNux(v: unknown): v is Nux { + return ( + isObj(v) && hasProp(v, '$type') && v.$type === 'app.bsky.actor.defs#nux' + ) +} + +export function validateNux(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#nux', v) +} diff --git a/packages/ozone/src/lexicon/lexicons.ts b/packages/ozone/src/lexicon/lexicons.ts index 162a97ec751..51d39bcb6f1 100644 --- a/packages/ozone/src/lexicon/lexicons.ts +++ b/packages/ozone/src/lexicon/lexicons.ts @@ -4572,6 +4572,15 @@ export const schemaDict = { maxLength: 100, }, }, + nuxs: { + description: 'Storage for NUXs the user has encountered.', + type: 'array', + maxLength: 100, + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#nux', + }, + }, }, }, bskyAppProgressGuide: { @@ -4586,6 +4595,34 @@ export const schemaDict = { }, }, }, + nux: { + type: 'object', + description: 'A new user experiences (NUX) storage object', + required: ['id', 'completed'], + properties: { + id: { + type: 'string', + maxLength: 100, + }, + completed: { + type: 'boolean', + default: false, + }, + data: { + description: + 'Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters.', + type: 'string', + maxLength: 3000, + maxGraphemes: 300, + }, + expiresAt: { + type: 'string', + format: 'datetime', + description: + 'The date and time at which the NUX will expire and should be considered completed.', + }, + }, + }, }, }, AppBskyActorGetPreferences: { diff --git a/packages/ozone/src/lexicon/types/app/bsky/actor/defs.ts b/packages/ozone/src/lexicon/types/app/bsky/actor/defs.ts index c7eadff70d7..926b468cb6d 100644 --- a/packages/ozone/src/lexicon/types/app/bsky/actor/defs.ts +++ b/packages/ozone/src/lexicon/types/app/bsky/actor/defs.ts @@ -469,6 +469,8 @@ export interface BskyAppStatePref { activeProgressGuide?: BskyAppProgressGuide /** An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user. */ queuedNudges?: string[] + /** Storage for NUXs the user has encountered. */ + nuxs?: Nux[] [k: string]: unknown } @@ -501,3 +503,24 @@ export function isBskyAppProgressGuide(v: unknown): v is BskyAppProgressGuide { export function validateBskyAppProgressGuide(v: unknown): ValidationResult { return lexicons.validate('app.bsky.actor.defs#bskyAppProgressGuide', v) } + +/** A new user experiences (NUX) storage object */ +export interface Nux { + id: string + completed: boolean + /** Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters. */ + data?: string + /** The date and time at which the NUX will expire and should be considered completed. */ + expiresAt?: string + [k: string]: unknown +} + +export function isNux(v: unknown): v is Nux { + return ( + isObj(v) && hasProp(v, '$type') && v.$type === 'app.bsky.actor.defs#nux' + ) +} + +export function validateNux(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#nux', v) +} diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 162a97ec751..51d39bcb6f1 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -4572,6 +4572,15 @@ export const schemaDict = { maxLength: 100, }, }, + nuxs: { + description: 'Storage for NUXs the user has encountered.', + type: 'array', + maxLength: 100, + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#nux', + }, + }, }, }, bskyAppProgressGuide: { @@ -4586,6 +4595,34 @@ export const schemaDict = { }, }, }, + nux: { + type: 'object', + description: 'A new user experiences (NUX) storage object', + required: ['id', 'completed'], + properties: { + id: { + type: 'string', + maxLength: 100, + }, + completed: { + type: 'boolean', + default: false, + }, + data: { + description: + 'Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters.', + type: 'string', + maxLength: 3000, + maxGraphemes: 300, + }, + expiresAt: { + type: 'string', + format: 'datetime', + description: + 'The date and time at which the NUX will expire and should be considered completed.', + }, + }, + }, }, }, AppBskyActorGetPreferences: { diff --git a/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts b/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts index c7eadff70d7..926b468cb6d 100644 --- a/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts +++ b/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts @@ -469,6 +469,8 @@ export interface BskyAppStatePref { activeProgressGuide?: BskyAppProgressGuide /** An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user. */ queuedNudges?: string[] + /** Storage for NUXs the user has encountered. */ + nuxs?: Nux[] [k: string]: unknown } @@ -501,3 +503,24 @@ export function isBskyAppProgressGuide(v: unknown): v is BskyAppProgressGuide { export function validateBskyAppProgressGuide(v: unknown): ValidationResult { return lexicons.validate('app.bsky.actor.defs#bskyAppProgressGuide', v) } + +/** A new user experiences (NUX) storage object */ +export interface Nux { + id: string + completed: boolean + /** Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters. */ + data?: string + /** The date and time at which the NUX will expire and should be considered completed. */ + expiresAt?: string + [k: string]: unknown +} + +export function isNux(v: unknown): v is Nux { + return ( + isObj(v) && hasProp(v, '$type') && v.$type === 'app.bsky.actor.defs#nux' + ) +} + +export function validateNux(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#nux', v) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cbbb37dae73..e328796b1d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,6 +89,9 @@ importers: tlds: specifier: ^1.234.0 version: 1.234.0 + zod: + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@atproto/lex-cli': specifier: workspace:^