Skip to content

Commit

Permalink
Priority notification setting (#2648)
Browse files Browse the repository at this point in the history
* priority notif settings in bsync

* lint

* priority notifications lexicon update

* codegen

* putNotificationPreferences -> putPreferences

* bsync: reorg around notif "priority", fix build, add validation & tests

* bsync: notif channel fix, tests fix

* bsky: update protos for priority notifs

* api prerelease

* add priority notif to actor state table

* dataplane impl

* appview: wire-up notif priority params

* appview: notif priority tests

* dataplane impl

* fix up tests

* tidy

* add changeset

---------

Co-authored-by: Samuel Newman <[email protected]>
Co-authored-by: Devin Ivy <[email protected]>
  • Loading branch information
3 people authored Jul 23, 2024
1 parent 12dcdb6 commit 76c91f8
Show file tree
Hide file tree
Showing 64 changed files with 2,247 additions and 89 deletions.
7 changes: 7 additions & 0 deletions .changeset/wise-ghosts-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@atproto/bsync": patch
"@atproto/bsky": patch
"@atproto/api": patch
---

Support for priority notifications
1 change: 1 addition & 0 deletions lexicons/app/bsky/notification/getUnreadCount.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"parameters": {
"type": "params",
"properties": {
"priority": { "type": "boolean" },
"seenAt": { "type": "string", "format": "datetime" }
}
},
Expand Down
2 changes: 2 additions & 0 deletions lexicons/app/bsky/notification/listNotifications.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"maximum": 100,
"default": 50
},
"priority": { "type": "boolean" },
"cursor": { "type": "string" },
"seenAt": { "type": "string", "format": "datetime" }
}
Expand All @@ -29,6 +30,7 @@
"type": "array",
"items": { "type": "ref", "ref": "#notification" }
},
"priority": { "type": "boolean" },
"seenAt": { "type": "string", "format": "datetime" }
}
}
Expand Down
20 changes: 20 additions & 0 deletions lexicons/app/bsky/notification/putPreferences.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"lexicon": 1,
"id": "app.bsky.notification.putPreferences",
"defs": {
"main": {
"type": "procedure",
"description": "Set notification-related preferences for an account. Requires auth.",
"input": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["priority"],
"properties": {
"priority": { "type": "boolean" }
}
}
}
}
}
}
2 changes: 1 addition & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@atproto/api",
"version": "0.12.24",
"version": "0.12.25-next.0",
"license": "MIT",
"description": "Client library for atproto and Bluesky",
"keywords": [
Expand Down
13 changes: 13 additions & 0 deletions packages/api/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ import * as AppBskyLabelerGetServices from './types/app/bsky/labeler/getServices
import * as AppBskyLabelerService from './types/app/bsky/labeler/service'
import * as AppBskyNotificationGetUnreadCount from './types/app/bsky/notification/getUnreadCount'
import * as AppBskyNotificationListNotifications from './types/app/bsky/notification/listNotifications'
import * as AppBskyNotificationPutPreferences from './types/app/bsky/notification/putPreferences'
import * as AppBskyNotificationRegisterPush from './types/app/bsky/notification/registerPush'
import * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen'
import * as AppBskyRichtextFacet from './types/app/bsky/richtext/facet'
Expand Down Expand Up @@ -350,6 +351,7 @@ export * as AppBskyLabelerGetServices from './types/app/bsky/labeler/getServices
export * as AppBskyLabelerService from './types/app/bsky/labeler/service'
export * as AppBskyNotificationGetUnreadCount from './types/app/bsky/notification/getUnreadCount'
export * as AppBskyNotificationListNotifications from './types/app/bsky/notification/listNotifications'
export * as AppBskyNotificationPutPreferences from './types/app/bsky/notification/putPreferences'
export * as AppBskyNotificationRegisterPush from './types/app/bsky/notification/registerPush'
export * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen'
export * as AppBskyRichtextFacet from './types/app/bsky/richtext/facet'
Expand Down Expand Up @@ -2816,6 +2818,17 @@ export class AppBskyNotificationNS {
})
}

putPreferences(
data?: AppBskyNotificationPutPreferences.InputSchema,
opts?: AppBskyNotificationPutPreferences.CallOptions,
): Promise<AppBskyNotificationPutPreferences.Response> {
return this._service.xrpc
.call('app.bsky.notification.putPreferences', opts?.qp, data, opts)
.catch((e) => {
throw AppBskyNotificationPutPreferences.toKnownErr(e)
})
}

registerPush(
data?: AppBskyNotificationRegisterPush.InputSchema,
opts?: AppBskyNotificationRegisterPush.CallOptions,
Expand Down
33 changes: 33 additions & 0 deletions packages/api/src/client/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8341,6 +8341,9 @@ export const schemaDict = {
parameters: {
type: 'params',
properties: {
priority: {
type: 'boolean',
},
seenAt: {
type: 'string',
format: 'datetime',
Expand Down Expand Up @@ -8379,6 +8382,9 @@ export const schemaDict = {
maximum: 100,
default: 50,
},
priority: {
type: 'boolean',
},
cursor: {
type: 'string',
},
Expand All @@ -8404,6 +8410,9 @@ export const schemaDict = {
ref: 'lex:app.bsky.notification.listNotifications#notification',
},
},
priority: {
type: 'boolean',
},
seenAt: {
type: 'string',
format: 'datetime',
Expand Down Expand Up @@ -8475,6 +8484,29 @@ export const schemaDict = {
},
},
},
AppBskyNotificationPutPreferences: {
lexicon: 1,
id: 'app.bsky.notification.putPreferences',
defs: {
main: {
type: 'procedure',
description:
'Set notification-related preferences for an account. Requires auth.',
input: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['priority'],
properties: {
priority: {
type: 'boolean',
},
},
},
},
},
},
},
AppBskyNotificationRegisterPush: {
lexicon: 1,
id: 'app.bsky.notification.registerPush',
Expand Down Expand Up @@ -11769,6 +11801,7 @@ export const ids = {
AppBskyNotificationGetUnreadCount: 'app.bsky.notification.getUnreadCount',
AppBskyNotificationListNotifications:
'app.bsky.notification.listNotifications',
AppBskyNotificationPutPreferences: 'app.bsky.notification.putPreferences',
AppBskyNotificationRegisterPush: 'app.bsky.notification.registerPush',
AppBskyNotificationUpdateSeen: 'app.bsky.notification.updateSeen',
AppBskyRichtextFacet: 'app.bsky.richtext.facet',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { lexicons } from '../../../../lexicons'
import { CID } from 'multiformats/cid'

export interface QueryParams {
priority?: boolean
seenAt?: string
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs'

export interface QueryParams {
limit?: number
priority?: boolean
cursor?: string
seenAt?: string
}
Expand All @@ -20,6 +21,7 @@ export type InputSchema = undefined
export interface OutputSchema {
cursor?: string
notifications: Notification[]
priority?: boolean
seenAt?: string
[k: string]: unknown
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers, XRPCError } from '@atproto/xrpc'
import { ValidationResult, BlobRef } from '@atproto/lexicon'
import { isObj, hasProp } from '../../../../util'
import { lexicons } from '../../../../lexicons'
import { CID } from 'multiformats/cid'

export interface QueryParams {}

export interface InputSchema {
priority: boolean
[k: string]: unknown
}

export interface CallOptions {
headers?: Headers
qp?: QueryParams
encoding: 'application/json'
}

export interface Response {
success: boolean
headers: Headers
}

export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}
8 changes: 7 additions & 1 deletion packages/bsky/proto/bsky.proto
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ message ActorInfo {
string allow_incoming_chats_from = 8;
string upstream_status = 9;
google.protobuf.Timestamp created_at = 10;
bool priority_notifications = 11;
}

message GetActorsResponse {
Expand Down Expand Up @@ -648,6 +649,7 @@ message GetNotificationsRequest {
string actor_did = 1;
int32 limit = 2;
string cursor = 3;
bool priority = 4;
}

message Notification {
Expand All @@ -656,6 +658,7 @@ message Notification {
string reason = 3;
string reason_subject = 4;
google.protobuf.Timestamp timestamp = 5;
bool priority = 6;
}

message GetNotificationsResponse {
Expand All @@ -668,6 +671,7 @@ message GetNotificationsResponse {
message UpdateNotificationSeenRequest {
string actor_did = 1;
google.protobuf.Timestamp timestamp = 2;
bool priority = 3;
}

message UpdateNotificationSeenResponse {}
Expand All @@ -676,6 +680,7 @@ message UpdateNotificationSeenResponse {}
// - hydrating read state onto notifications
message GetNotificationSeenRequest {
string actor_did = 1;
bool priority = 2;
}

message GetNotificationSeenResponse {
Expand All @@ -686,6 +691,7 @@ message GetNotificationSeenResponse {
// - `getUnreadCount`
message GetUnreadNotificationCountRequest {
string actor_did = 1;
bool priority = 2;
}

message GetUnreadNotificationCountResponse {
Expand Down Expand Up @@ -1203,7 +1209,7 @@ service Service {
rpc CreateActorMutelistSubscription(CreateActorMutelistSubscriptionRequest) returns (CreateActorMutelistSubscriptionResponse);
rpc DeleteActorMutelistSubscription(DeleteActorMutelistSubscriptionRequest) returns (DeleteActorMutelistSubscriptionResponse);
rpc ClearActorMutelistSubscriptions(ClearActorMutelistSubscriptionsRequest) returns (ClearActorMutelistSubscriptionsResponse);

rpc CreateThreadMute(CreateThreadMuteRequest) returns (CreateThreadMuteResponse);
rpc DeleteThreadMute(DeleteThreadMuteRequest) returns (DeleteThreadMuteResponse);
rpc ClearThreadMutes(ClearThreadMutesRequest) returns (ClearThreadMutesResponse);
Expand Down
7 changes: 7 additions & 0 deletions packages/bsky/src/api/app/bsky/notification/getUnreadCount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ const skeleton = async (
if (params.seenAt) {
throw new InvalidRequestError('The seenAt parameter is unsupported')
}
const priority = params.priority ?? (await getPriority(ctx, params.viewer))
const res = await ctx.hydrator.dataplane.getUnreadNotificationCount({
actorDid: params.viewer,
priority,
})
return {
count: res.count,
Expand Down Expand Up @@ -72,3 +74,8 @@ type Params = QueryParams & {
type SkeletonState = {
count: number
}

const getPriority = async (ctx: Context, did: string) => {
const actors = await ctx.hydrator.actor.getActors([did])
return !!actors.get(did)?.priorityNotifications
}
19 changes: 17 additions & 2 deletions packages/bsky/src/api/app/bsky/notification/listNotifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,20 @@ const skeleton = async (
throw new InvalidRequestError('The seenAt parameter is unsupported')
}
const viewer = params.hydrateCtx.viewer
const priority = params.priority ?? (await getPriority(ctx, viewer))
if (clearlyBadCursor(params.cursor)) {
return { notifs: [] }
return { notifs: [], priority }
}
const [res, lastSeenRes] = await Promise.all([
ctx.hydrator.dataplane.getNotifications({
actorDid: viewer,
priority,
cursor: params.cursor,
limit: params.limit,
}),
ctx.hydrator.dataplane.getNotificationSeen({
actorDid: viewer,
priority,
}),
])
// @NOTE for the first page of results if there's no last-seen time, consider top notification unread
Expand All @@ -72,6 +75,7 @@ const skeleton = async (
return {
notifs: res.notifications,
cursor: res.cursor || undefined,
priority,
lastSeenNotifs: lastSeenDate?.toISOString(),
}
}
Expand Down Expand Up @@ -105,7 +109,12 @@ const presentation = (
const notifications = mapDefined(notifs, (notif) =>
ctx.views.notification(notif, lastSeenNotifs, hydration),
)
return { notifications, cursor, seenAt: skeleton.lastSeenNotifs }
return {
notifications,
cursor,
priority: skeleton.priority,
seenAt: skeleton.lastSeenNotifs,
}
}

type Context = {
Expand All @@ -119,6 +128,12 @@ type Params = QueryParams & {

type SkeletonState = {
notifs: Notification[]
priority: boolean
lastSeenNotifs?: string
cursor?: string
}

const getPriority = async (ctx: Context, did: string) => {
const actors = await ctx.hydrator.actor.getActors([did])
return !!actors.get(did)?.priorityNotifications
}
16 changes: 16 additions & 0 deletions packages/bsky/src/api/app/bsky/notification/putPreferences.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Server } from '../../../../lexicon'
import AppContext from '../../../../context'

export default function (server: Server, ctx: AppContext) {
server.app.bsky.notification.putPreferences({
auth: ctx.authVerifier.standard,
handler: async ({ input, auth }) => {
const { priority } = input.body
const viewer = auth.credentials.iss
await ctx.bsyncClient.addNotifOperation({
actorDid: viewer,
priority,
})
},
})
}
19 changes: 14 additions & 5 deletions packages/bsky/src/api/app/bsky/notification/updateSeen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,21 @@ export default function (server: Server, ctx: AppContext) {
server.app.bsky.notification.updateSeen({
auth: ctx.authVerifier.standard,
handler: async ({ input, auth }) => {
const { seenAt } = input.body
const viewer = auth.credentials.iss
await ctx.dataplane.updateNotificationSeen({
actorDid: viewer,
timestamp: Timestamp.fromDate(new Date(seenAt)),
})
const seenAt = new Date(input.body.seenAt)
// For now we keep separate seen times behind the scenes for priority, but treat them as a single seen time.
await Promise.all([
ctx.dataplane.updateNotificationSeen({
actorDid: viewer,
timestamp: Timestamp.fromDate(seenAt),
priority: false,
}),
ctx.dataplane.updateNotificationSeen({
actorDid: viewer,
timestamp: Timestamp.fromDate(seenAt),
priority: true,
}),
])
},
})
}
Loading

0 comments on commit 76c91f8

Please sign in to comment.