@atproto/bsky 0.0.237 → 0.0.239

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/api/app/bsky/actor/searchActors.d.ts.map +1 -1
  3. package/dist/api/app/bsky/actor/searchActors.js +26 -1
  4. package/dist/api/app/bsky/actor/searchActors.js.map +1 -1
  5. package/dist/api/app/bsky/actor/searchActorsTypeahead.d.ts.map +1 -1
  6. package/dist/api/app/bsky/actor/searchActorsTypeahead.js +26 -2
  7. package/dist/api/app/bsky/actor/searchActorsTypeahead.js.map +1 -1
  8. package/dist/api/app/bsky/feed/getFeed.d.ts.map +1 -1
  9. package/dist/api/app/bsky/feed/getFeed.js +1 -0
  10. package/dist/api/app/bsky/feed/getFeed.js.map +1 -1
  11. package/dist/api/app/bsky/feed/searchPosts.d.ts.map +1 -1
  12. package/dist/api/app/bsky/feed/searchPosts.js +56 -1
  13. package/dist/api/app/bsky/feed/searchPosts.js.map +1 -1
  14. package/dist/api/app/bsky/graph/searchStarterPacks.d.ts.map +1 -1
  15. package/dist/api/app/bsky/graph/searchStarterPacks.js +26 -1
  16. package/dist/api/app/bsky/graph/searchStarterPacks.js.map +1 -1
  17. package/dist/api/app/bsky/unspecced/getPopularFeedGenerators.d.ts.map +1 -1
  18. package/dist/api/app/bsky/unspecced/getPopularFeedGenerators.js +27 -6
  19. package/dist/api/app/bsky/unspecced/getPopularFeedGenerators.js.map +1 -1
  20. package/dist/api/com/atproto/repo/getRecord.js +2 -2
  21. package/dist/api/com/atproto/repo/getRecord.js.map +1 -1
  22. package/dist/data-plane/server/routes/feed-gens.d.ts.map +1 -1
  23. package/dist/data-plane/server/routes/feed-gens.js +21 -12
  24. package/dist/data-plane/server/routes/feed-gens.js.map +1 -1
  25. package/dist/data-plane/server/routes/search.d.ts.map +1 -1
  26. package/dist/data-plane/server/routes/search.js +62 -12
  27. package/dist/data-plane/server/routes/search.js.map +1 -1
  28. package/dist/feature-gates/gates.d.ts +1 -0
  29. package/dist/feature-gates/gates.d.ts.map +1 -1
  30. package/dist/feature-gates/gates.js +1 -0
  31. package/dist/feature-gates/gates.js.map +1 -1
  32. package/dist/hydration/hydrator.d.ts.map +1 -1
  33. package/dist/hydration/hydrator.js +3 -2
  34. package/dist/hydration/hydrator.js.map +1 -1
  35. package/dist/hydration/label.d.ts +1 -1
  36. package/dist/hydration/label.d.ts.map +1 -1
  37. package/dist/hydration/label.js +4 -4
  38. package/dist/hydration/label.js.map +1 -1
  39. package/dist/lexicons/app/bsky/actor/profile.defs.d.ts.map +1 -1
  40. package/dist/lexicons/app/bsky/actor/status.defs.d.ts.map +1 -1
  41. package/dist/lexicons/app/bsky/draft/defs.defs.d.ts +22 -0
  42. package/dist/lexicons/app/bsky/draft/defs.defs.d.ts.map +1 -1
  43. package/dist/lexicons/app/bsky/draft/defs.defs.js +11 -0
  44. package/dist/lexicons/app/bsky/draft/defs.defs.js.map +1 -1
  45. package/dist/lexicons/app/bsky/embed/gallery.d.ts +3 -0
  46. package/dist/lexicons/app/bsky/embed/gallery.d.ts.map +1 -0
  47. package/dist/lexicons/app/bsky/embed/gallery.defs.d.ts +130 -0
  48. package/dist/lexicons/app/bsky/embed/gallery.defs.d.ts.map +1 -0
  49. package/dist/lexicons/app/bsky/embed/gallery.defs.js +47 -0
  50. package/dist/lexicons/app/bsky/embed/gallery.defs.js.map +1 -0
  51. package/dist/lexicons/app/bsky/embed/gallery.js +6 -0
  52. package/dist/lexicons/app/bsky/embed/gallery.js.map +1 -0
  53. package/dist/lexicons/app/bsky/embed/record.defs.d.ts +2 -1
  54. package/dist/lexicons/app/bsky/embed/record.defs.d.ts.map +1 -1
  55. package/dist/lexicons/app/bsky/embed/record.defs.js +2 -0
  56. package/dist/lexicons/app/bsky/embed/record.defs.js.map +1 -1
  57. package/dist/lexicons/app/bsky/embed/recordWithMedia.defs.d.ts +13 -12
  58. package/dist/lexicons/app/bsky/embed/recordWithMedia.defs.d.ts.map +1 -1
  59. package/dist/lexicons/app/bsky/embed/recordWithMedia.defs.js +3 -0
  60. package/dist/lexicons/app/bsky/embed/recordWithMedia.defs.js.map +1 -1
  61. package/dist/lexicons/app/bsky/embed.d.ts +1 -0
  62. package/dist/lexicons/app/bsky/embed.d.ts.map +1 -1
  63. package/dist/lexicons/app/bsky/embed.js +1 -0
  64. package/dist/lexicons/app/bsky/embed.js.map +1 -1
  65. package/dist/lexicons/app/bsky/feed/defs.defs.d.ts +2 -1
  66. package/dist/lexicons/app/bsky/feed/defs.defs.d.ts.map +1 -1
  67. package/dist/lexicons/app/bsky/feed/defs.defs.js +2 -0
  68. package/dist/lexicons/app/bsky/feed/defs.defs.js.map +1 -1
  69. package/dist/lexicons/app/bsky/feed/generator.defs.d.ts.map +1 -1
  70. package/dist/lexicons/app/bsky/feed/like.defs.d.ts.map +1 -1
  71. package/dist/lexicons/app/bsky/feed/post.defs.d.ts +12 -11
  72. package/dist/lexicons/app/bsky/feed/post.defs.d.ts.map +1 -1
  73. package/dist/lexicons/app/bsky/feed/post.defs.js +2 -0
  74. package/dist/lexicons/app/bsky/feed/post.defs.js.map +1 -1
  75. package/dist/lexicons/app/bsky/feed/postgate.defs.d.ts.map +1 -1
  76. package/dist/lexicons/app/bsky/feed/repost.defs.d.ts.map +1 -1
  77. package/dist/lexicons/app/bsky/feed/threadgate.defs.d.ts.map +1 -1
  78. package/dist/lexicons/app/bsky/graph/block.defs.d.ts.map +1 -1
  79. package/dist/lexicons/app/bsky/graph/follow.defs.d.ts.map +1 -1
  80. package/dist/lexicons/app/bsky/graph/list.defs.d.ts.map +1 -1
  81. package/dist/lexicons/app/bsky/graph/listblock.defs.d.ts.map +1 -1
  82. package/dist/lexicons/app/bsky/graph/listitem.defs.d.ts.map +1 -1
  83. package/dist/lexicons/app/bsky/graph/starterpack.defs.d.ts.map +1 -1
  84. package/dist/lexicons/app/bsky/graph/verification.defs.d.ts.map +1 -1
  85. package/dist/lexicons/app/bsky/labeler/service.defs.d.ts.map +1 -1
  86. package/dist/lexicons/app/bsky/notification/declaration.defs.d.ts.map +1 -1
  87. package/dist/lexicons/chat/bsky/actor/declaration.defs.d.ts.map +1 -1
  88. package/dist/lexicons/chat/bsky/group/createGroup.defs.d.ts +4 -4
  89. package/dist/lexicons/chat/bsky/group/createGroup.defs.js +2 -2
  90. package/dist/lexicons/chat/bsky/group/createGroup.defs.js.map +1 -1
  91. package/dist/lexicons/chat/bsky/group/defs.defs.d.ts +8 -1
  92. package/dist/lexicons/chat/bsky/group/defs.defs.d.ts.map +1 -1
  93. package/dist/lexicons/chat/bsky/group/defs.defs.js +5 -1
  94. package/dist/lexicons/chat/bsky/group/defs.defs.js.map +1 -1
  95. package/dist/lexicons/com/atproto/lexicon/schema.defs.d.ts.map +1 -1
  96. package/dist/lexicons/com/germnetwork/declaration.defs.d.ts.map +1 -1
  97. package/dist/lexicons/site/standard/document.defs.d.ts.map +1 -1
  98. package/dist/lexicons/site/standard/graph/recommend.defs.d.ts.map +1 -1
  99. package/dist/lexicons/site/standard/graph/subscription.defs.d.ts.map +1 -1
  100. package/dist/lexicons/site/standard/publication.defs.d.ts.map +1 -1
  101. package/dist/lexicons/site/standard/theme/basic.defs.d.ts.map +1 -1
  102. package/dist/proto/bsky_connect.d.ts +49 -2
  103. package/dist/proto/bsky_connect.d.ts.map +1 -1
  104. package/dist/proto/bsky_connect.js +49 -2
  105. package/dist/proto/bsky_connect.js.map +1 -1
  106. package/dist/proto/bsky_pb.d.ts +482 -0
  107. package/dist/proto/bsky_pb.d.ts.map +1 -1
  108. package/dist/proto/bsky_pb.js +608 -0
  109. package/dist/proto/bsky_pb.js.map +1 -1
  110. package/dist/views/index.d.ts +4 -1
  111. package/dist/views/index.d.ts.map +1 -1
  112. package/dist/views/index.js +39 -5
  113. package/dist/views/index.js.map +1 -1
  114. package/dist/views/types.d.ts +8 -2
  115. package/dist/views/types.d.ts.map +1 -1
  116. package/dist/views/types.js +2 -0
  117. package/dist/views/types.js.map +1 -1
  118. package/package.json +7 -7
  119. package/proto/bsky.proto +166 -2
  120. package/src/api/app/bsky/actor/searchActors.ts +35 -1
  121. package/src/api/app/bsky/actor/searchActorsTypeahead.ts +35 -2
  122. package/src/api/app/bsky/feed/getFeed.ts +1 -0
  123. package/src/api/app/bsky/feed/searchPosts.ts +60 -1
  124. package/src/api/app/bsky/graph/searchStarterPacks.ts +35 -1
  125. package/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +28 -6
  126. package/src/api/com/atproto/repo/getRecord.ts +2 -2
  127. package/src/data-plane/server/routes/feed-gens.ts +33 -14
  128. package/src/data-plane/server/routes/search.ts +81 -13
  129. package/src/feature-gates/gates.ts +1 -0
  130. package/src/hydration/hydrator.ts +3 -6
  131. package/src/hydration/label.ts +3 -3
  132. package/src/views/index.ts +61 -11
  133. package/src/views/types.ts +9 -0
  134. package/tests/data-plane/handle-invalidation.test.ts +2 -1
  135. package/tests/views/__snapshots__/posts.test.ts.snap +251 -0
  136. package/tests/views/drafts.test.ts +105 -0
  137. package/tests/views/posts.test.ts +134 -0
  138. package/tsconfig.build.json +2 -2
  139. package/tsconfig.build.tsbuildinfo +1 -1
  140. package/tsconfig.json +2 -2
  141. package/tsconfig.tests.json +2 -2
@@ -1,4 +1,4 @@
1
- import { AtUri } from '@atproto/syntax'
1
+ import { atUri } from '@atproto/lex'
2
2
  import { InvalidRequestError, Server } from '@atproto/xrpc-server'
3
3
  import { AppContext } from '../../../../context.js'
4
4
  import { com } from '../../../../lexicons/index.js'
@@ -21,7 +21,7 @@ export default function (server: Server, ctx: AppContext) {
21
21
  throw new InvalidRequestError(`Could not find repo: ${repo}`)
22
22
  }
23
23
 
24
- const uri = AtUri.make(did, collection, rkey).toString()
24
+ const uri = atUri(did, collection, rkey)
25
25
  const result = await ctx.hydrator.getRecord(uri, includeTakedowns)
26
26
 
27
27
  if (!result || (cid && result.cid !== cid)) {
@@ -45,22 +45,18 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
45
45
  },
46
46
 
47
47
  async searchFeedGenerators(req) {
48
- const { ref } = db.db.dynamic
49
- const limit = req.limit
50
- const query = req.query.trim()
51
- let builder = db.db
52
- .selectFrom('feed_generator')
53
- .if(!!query, (q) => q.where('displayName', 'ilike', `%${query}%`))
54
- .selectAll()
55
- const keyset = new TimeCidKeyset(
56
- ref('feed_generator.createdAt'),
57
- ref('feed_generator.cid'),
48
+ return searchFeedGeneratorsImpl(db, req.query, req.limit)
49
+ },
50
+
51
+ async searchFeedGeneratorsV2(req) {
52
+ const { uris, cursor } = await searchFeedGeneratorsImpl(
53
+ db,
54
+ req.params?.query ?? '',
55
+ req.params?.limit ?? 25,
58
56
  )
59
- builder = paginate(builder, { limit, keyset })
60
- const feeds = await builder.execute()
61
57
  return {
62
- uris: feeds.map((f) => f.uri),
63
- cursor: keyset.packFromResult(feeds),
58
+ feedGenerators: uris.map((uri) => ({ uri, score: 0 })),
59
+ pageInfo: { cursor: cursor ?? '', hitsTotal: 0n },
64
60
  }
65
61
  },
66
62
 
@@ -68,3 +64,26 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
68
64
  throw new Error('unimplemented')
69
65
  },
70
66
  })
67
+
68
+ const searchFeedGeneratorsImpl = async (
69
+ db: Database,
70
+ query: string,
71
+ limit: number,
72
+ ) => {
73
+ const { ref } = db.db.dynamic
74
+ const trimmed = query.trim()
75
+ let builder = db.db
76
+ .selectFrom('feed_generator')
77
+ .if(!!trimmed, (q) => q.where('displayName', 'ilike', `%${trimmed}%`))
78
+ .selectAll()
79
+ const keyset = new TimeCidKeyset(
80
+ ref('feed_generator.createdAt'),
81
+ ref('feed_generator.cid'),
82
+ )
83
+ builder = paginate(builder, { limit, keyset })
84
+ const feeds = await builder.execute()
85
+ return {
86
+ uris: feeds.map((f) => f.uri),
87
+ cursor: keyset.packFromResult(feeds),
88
+ }
89
+ }
@@ -8,9 +8,12 @@ import {
8
8
  } from '../db/pagination.js'
9
9
  import { parsePostSearchQuery } from '../util.js'
10
10
 
11
- export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
12
- // @TODO actor search endpoints still fall back to search service
13
- async searchActors(req) {
11
+ export default (db: Database): Partial<ServiceImpl<typeof Service>> => {
12
+ const searchActorsImpl = async (req: {
13
+ term: string
14
+ limit: number
15
+ cursor?: string
16
+ }) => {
14
17
  const { term, limit, cursor } = req
15
18
  const { ref } = db.db.dynamic
16
19
  let builder = db.db
@@ -35,10 +38,13 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
35
38
  dids: res.map((row) => row.did),
36
39
  cursor: keyset.packFromResult(res),
37
40
  }
38
- },
41
+ }
39
42
 
40
- // @TODO post search endpoint still falls back to search service
41
- async searchPosts(req) {
43
+ const searchPostsImpl = async (req: {
44
+ term: string
45
+ limit: number
46
+ cursor?: string
47
+ }) => {
42
48
  const { term, limit, cursor } = req
43
49
  const { q, author } = parsePostSearchQuery(term)
44
50
 
@@ -75,9 +81,13 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
75
81
  uris: res.map((row) => row.uri),
76
82
  cursor: keyset.packFromResult(res),
77
83
  }
78
- },
84
+ }
79
85
 
80
- async searchStarterPacks(req) {
86
+ const searchStarterPacksImpl = async (req: {
87
+ term: string
88
+ limit: number
89
+ cursor?: string
90
+ }) => {
81
91
  const { term, limit, cursor } = req
82
92
  const { ref } = db.db.dynamic
83
93
  let builder = db.db
@@ -99,14 +109,72 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
99
109
 
100
110
  const res = await builder.execute()
101
111
 
102
- const cur = keyset.packFromResult(res)
103
-
104
112
  return {
105
113
  uris: res.map((row) => row.uri),
106
- cursor: cur,
114
+ cursor: keyset.packFromResult(res),
107
115
  }
108
- },
109
- })
116
+ }
117
+
118
+ return {
119
+ // @TODO actor search endpoints still fall back to search service
120
+ searchActors: searchActorsImpl,
121
+
122
+ // @TODO post search endpoint still falls back to search service
123
+ searchPosts: searchPostsImpl,
124
+
125
+ searchStarterPacks: searchStarterPacksImpl,
126
+
127
+ // V2 endpoints reuse the V1 SQL for dev env and reshape the response.
128
+ async searchActorsV2(req) {
129
+ const { dids, cursor } = await searchActorsImpl({
130
+ term: req.params?.query ?? '',
131
+ limit: req.params?.limit ?? 25,
132
+ cursor: req.params?.cursor,
133
+ })
134
+ return {
135
+ actors: dids.map((did) => ({ did, score: 0 })),
136
+ pageInfo: { cursor: cursor ?? '', hitsTotal: 0n },
137
+ }
138
+ },
139
+
140
+ async searchActorsTypeahead(req) {
141
+ const { dids } = await searchActorsImpl({
142
+ term: req.query,
143
+ limit: req.limit || 10,
144
+ })
145
+ return {
146
+ actors: dids.map((did) => ({ did, score: 0 })),
147
+ }
148
+ },
149
+
150
+ async searchPostsV2(req) {
151
+ const author = req.filters?.authors?.[0]
152
+ const baseQuery = req.params?.query ?? ''
153
+ const term = author ? `${baseQuery} from:${author}` : baseQuery
154
+ const { uris, cursor } = await searchPostsImpl({
155
+ term,
156
+ limit: req.params?.limit ?? 25,
157
+ cursor: req.params?.cursor,
158
+ })
159
+ return {
160
+ posts: uris.map((uri) => ({ uri, score: 0 })),
161
+ pageInfo: { cursor: cursor ?? '', hitsTotal: 0n },
162
+ }
163
+ },
164
+
165
+ async searchStarterPacksV2(req) {
166
+ const { uris, cursor } = await searchStarterPacksImpl({
167
+ term: req.params?.query ?? '',
168
+ limit: req.params?.limit ?? 25,
169
+ cursor: req.params?.cursor,
170
+ })
171
+ return {
172
+ starterPacks: uris.map((uri) => ({ uri, score: 0 })),
173
+ pageInfo: { cursor: cursor ?? '', hitsTotal: 0n },
174
+ }
175
+ },
176
+ }
177
+ }
110
178
 
111
179
  // Remove leading @ in case a handle is input that way
112
180
  const cleanQuery = (query: string) => query.trim().replace(/^@/g, '')
@@ -11,6 +11,7 @@ export enum Gate {
11
11
  SuggestedUsersForExploreEnable = 'suggested_users:for_explore:enable',
12
12
  SuggestedUsersForDiscoverEnable = 'suggested_users:for_discover:enable',
13
13
  SuggestedUsersForSeeMoreEnable = 'suggested_users:for_see_more:enable',
14
+ SearchV2Enable = 'search:v2:enable',
14
15
 
15
16
  // temp
16
17
  AATest = 'aa-test-appview',
@@ -1,5 +1,6 @@
1
1
  import assert from 'node:assert'
2
2
  import { dedupeStrs, mapDefined } from '@atproto/common'
3
+ import { atUri } from '@atproto/lex'
3
4
  import { AtUri, AtUriString, DidString, UriString } from '@atproto/syntax'
4
5
  import { DataPlaneClient } from '../data-plane/client/index.js'
5
6
  import {
@@ -1575,12 +1576,8 @@ const isModList = (
1575
1576
  const labelSubjectsForDid = (dids: DidString[]) => {
1576
1577
  return [
1577
1578
  ...dids,
1578
- ...dids.map((did) =>
1579
- AtUri.make(did, app.bsky.actor.profile.$type, 'self').toString(),
1580
- ),
1581
- ...dids.map((did) =>
1582
- AtUri.make(did, app.bsky.actor.status.$type, 'self').toString(),
1583
- ),
1579
+ ...dids.map((did) => atUri(did, app.bsky.actor.profile)),
1580
+ ...dids.map((did) => atUri(did, app.bsky.actor.status)),
1584
1581
  ]
1585
1582
  }
1586
1583
 
@@ -1,4 +1,4 @@
1
- import { AtUri, AtUriString, DidString, UriString } from '@atproto/syntax'
1
+ import { AtUriString, DidString, UriString, atUri } from '@atproto/lex'
2
2
  import { DataPlaneClient } from '../data-plane/client/index.js'
3
3
  import { app, com } from '../lexicons/index.js'
4
4
  import { ParsedLabelers } from '../util.js'
@@ -198,8 +198,8 @@ export class LabelHydrator {
198
198
  }
199
199
  }
200
200
 
201
- const labelerDidToUri = (did: DidString): AtUriString => {
202
- return AtUri.make(did, app.bsky.labeler.service.$type, 'self').toString()
201
+ function labelerDidToUri<T extends DidString>(did: T) {
202
+ return atUri(did, app.bsky.labeler.service)
203
203
  }
204
204
 
205
205
  const IMPERSONATION_LABEL = 'impersonation'
@@ -4,6 +4,7 @@ import {
4
4
  Un$Typed,
5
5
  Unknown$TypedObject,
6
6
  UriString,
7
+ atUri,
7
8
  getBlobCidString,
8
9
  } from '@atproto/lex'
9
10
  import {
@@ -66,6 +67,10 @@ import {
66
67
  ExternalEmbedView,
67
68
  FeedViewPost,
68
69
  FollowRecord,
70
+ GalleryEmbed,
71
+ GalleryEmbedView,
72
+ GalleryImageEmbed,
73
+ GalleryImageEmbedView,
69
74
  GeneratorView,
70
75
  GetPostThreadV2QueryParams,
71
76
  ImagesEmbed,
@@ -115,6 +120,8 @@ import {
115
120
  VideoEmbed,
116
121
  VideoEmbedView,
117
122
  isExternalEmbedType,
123
+ isGalleryEmbedType,
124
+ isGalleryImageEmbedType,
118
125
  isImagesEmbedType,
119
126
  isLabelerRecordType,
120
127
  isListRuleType,
@@ -135,6 +142,11 @@ const notificationDeletedRecord =
135
142
  const notificationDeletedRecordCid =
136
143
  'bafyreidad6nyekfa4a67yfb573ptxiv6s7kyxyg2ra6qbbemcruadvtuim'
137
144
 
145
+ // Soft-limit for `app.bsky.embed.gallery#main.items`. The lexicon's
146
+ // schema-level cap is 20, but clients are expected to enforce a soft limit
147
+ // of 10 today. The AppView trims defensively at the view boundary.
148
+ const GALLERY_SOFT_LIMIT = 10
149
+
138
150
  export class Views {
139
151
  public imgUriBuilder: ImageUriBuilder = this.opts.imgUriBuilder
140
152
  public videoUriBuilder: VideoUriBuilder = this.opts.videoUriBuilder
@@ -348,11 +360,7 @@ export class Views {
348
360
  ): Un$Typed<ProfileViewBasic> | undefined {
349
361
  const actor = state.actors?.get(did)
350
362
  if (!actor) return
351
- const profileUri = AtUri.make(
352
- did,
353
- app.bsky.actor.profile.$nsid,
354
- 'self',
355
- ).toString()
363
+ const profileUri = atUri(did, app.bsky.actor.profile)
356
364
  const labels = [
357
365
  ...(state.labels?.getBySubject(did) ?? []),
358
366
  ...(state.labels?.getBySubject(profileUri) ?? []),
@@ -603,7 +611,7 @@ export class Views {
603
611
  return undefined
604
612
  }
605
613
 
606
- const uri = AtUri.make(did, app.bsky.actor.status.$nsid, 'self').toString()
614
+ const uri = atUri(did, app.bsky.actor.status)
607
615
  const labels = state.labels?.getBySubject(uri)
608
616
 
609
617
  const minDuration = 5 * MINUTE
@@ -838,11 +846,7 @@ export class Views {
838
846
  const viewer = state.labelerViewers?.get(did)
839
847
  const aggs = state.labelerAggs?.get(did)
840
848
 
841
- const uri = AtUri.make(
842
- did,
843
- app.bsky.labeler.service.$type,
844
- 'self',
845
- ).toString()
849
+ const uri = atUri(did, app.bsky.labeler.service)
846
850
  const labels = [
847
851
  ...(state.labels?.getBySubject(uri) ?? []),
848
852
  ...this.selfLabels({
@@ -2090,6 +2094,8 @@ export class Views {
2090
2094
  return this.imagesEmbed(creatorFromUri(postUri), embed)
2091
2095
  } else if (isVideoEmbedType(embed)) {
2092
2096
  return this.videoEmbed(creatorFromUri(postUri), embed)
2097
+ } else if (isGalleryEmbedType(embed)) {
2098
+ return this.galleryEmbed(creatorFromUri(postUri), embed)
2093
2099
  } else if (isExternalEmbedType(embed)) {
2094
2100
  return this.externalEmbed(creatorFromUri(postUri), embed, state)
2095
2101
  } else if (isRecordEmbedType(embed)) {
@@ -2133,6 +2139,47 @@ export class Views {
2133
2139
  })
2134
2140
  }
2135
2141
 
2142
+ galleryEmbed(did: DidString, embed: GalleryEmbed): $Typed<GalleryEmbedView> {
2143
+ // The lexicon's schema-level cap is 20, but clients are expected to
2144
+ // enforce a soft limit of 10. Trim defensively at the view boundary so
2145
+ // viewers see at most 10 items regardless of what was authored.
2146
+ const items = embed.items.slice(0, GALLERY_SOFT_LIMIT).flatMap((item) => {
2147
+ const view = this.galleryItemView(did, item)
2148
+ return view ? [view] : []
2149
+ })
2150
+ return app.bsky.embed.gallery.view.$build({ items })
2151
+ }
2152
+
2153
+ private galleryItemView(
2154
+ did: DidString,
2155
+ item: GalleryEmbed['items'][number],
2156
+ ): $Typed<GalleryImageEmbedView> | undefined {
2157
+ if (isGalleryImageEmbedType(item)) {
2158
+ return this.galleryImageView(did, item)
2159
+ }
2160
+ return undefined
2161
+ }
2162
+
2163
+ private galleryImageView(
2164
+ did: DidString,
2165
+ item: GalleryImageEmbed,
2166
+ ): $Typed<GalleryImageEmbedView> {
2167
+ return app.bsky.embed.gallery.viewImage.$build({
2168
+ thumbnail: this.imgUriBuilder.getPresetUri(
2169
+ 'feed_thumbnail',
2170
+ did,
2171
+ getBlobCidString(item.image),
2172
+ ),
2173
+ fullsize: this.imgUriBuilder.getPresetUri(
2174
+ 'feed_fullsize',
2175
+ did,
2176
+ getBlobCidString(item.image),
2177
+ ),
2178
+ alt: item.alt,
2179
+ aspectRatio: item.aspectRatio,
2180
+ })
2181
+ }
2182
+
2136
2183
  externalEmbed(
2137
2184
  did: DidString,
2138
2185
  embed: ExternalEmbed,
@@ -2528,11 +2575,14 @@ export class Views {
2528
2575
  let mediaEmbed:
2529
2576
  | $Typed<ImagesEmbedView>
2530
2577
  | $Typed<VideoEmbedView>
2578
+ | $Typed<GalleryEmbedView>
2531
2579
  | $Typed<ExternalEmbedView>
2532
2580
  if (isImagesEmbedType(embed.media)) {
2533
2581
  mediaEmbed = this.imagesEmbed(creator, embed.media)
2534
2582
  } else if (isVideoEmbedType(embed.media)) {
2535
2583
  mediaEmbed = this.videoEmbed(creator, embed.media)
2584
+ } else if (isGalleryEmbedType(embed.media)) {
2585
+ mediaEmbed = this.galleryEmbed(creator, embed.media)
2536
2586
  } else if (isExternalEmbedType(embed.media)) {
2537
2587
  mediaEmbed = this.externalEmbed(creator, embed.media, state)
2538
2588
  } else {
@@ -33,6 +33,13 @@ export const isVideoEmbedType = app.bsky.embed.video.$isTypeOf
33
33
  export type VideoEmbed = app.bsky.embed.video.Main
34
34
  export type VideoEmbedView = app.bsky.embed.video.View
35
35
 
36
+ export const isGalleryEmbedType = app.bsky.embed.gallery.$isTypeOf
37
+ export type GalleryEmbed = app.bsky.embed.gallery.Main
38
+ export type GalleryEmbedView = app.bsky.embed.gallery.View
39
+ export const isGalleryImageEmbedType = app.bsky.embed.gallery.image.$isTypeOf
40
+ export type GalleryImageEmbed = app.bsky.embed.gallery.Image
41
+ export type GalleryImageEmbedView = app.bsky.embed.gallery.ViewImage
42
+
36
43
  export const isExternalEmbedType = app.bsky.embed.external.$isTypeOf
37
44
  export type ExternalEmbed = app.bsky.embed.external.Main
38
45
  export type ExternalEmbedView = app.bsky.embed.external.View
@@ -57,6 +64,7 @@ export type RecordWithMediaEmbedView = app.bsky.embed.recordWithMedia.View
57
64
  export type Embed =
58
65
  | ImagesEmbed
59
66
  | VideoEmbed
67
+ | GalleryEmbed
60
68
  | ExternalEmbed
61
69
  | RecordEmbed
62
70
  | RecordWithMedia
@@ -64,6 +72,7 @@ export type Embed =
64
72
  export type EmbedView =
65
73
  | ImagesEmbedView
66
74
  | VideoEmbedView
75
+ | GalleryEmbedView
67
76
  | ExternalEmbedView
68
77
  | RecordEmbedView
69
78
  | RecordWithMediaView
@@ -116,7 +116,8 @@ describe('handle invalidation', () => {
116
116
 
117
117
  it('deals with handle contention', async () => {
118
118
  await backdateIndexedAt(bob)
119
- // update alices handle so that the pds will let bob take her old handle
119
+ // make alice lose her handle so that the pds will let bob take her old handle
120
+ mockHandles['not-alice.test'] = null
120
121
  await network.pds.ctx.accountManager.updateHandle(alice, 'not-alice.test')
121
122
 
122
123
  await pdsAgent.api.com.atproto.identity.updateHandle(
@@ -1,5 +1,256 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
+ exports[`pds posts views > embeds gallery with record. 1`] = `
4
+ {
5
+ "author": {
6
+ "associated": {
7
+ "activitySubscription": {
8
+ "allowSubscriptions": "followers",
9
+ },
10
+ },
11
+ "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)",
12
+ "createdAt": "1970-01-01T00:00:00.000Z",
13
+ "did": "user(0)",
14
+ "displayName": "ali",
15
+ "handle": "alice.test",
16
+ "labels": [
17
+ {
18
+ "cid": "cids(2)",
19
+ "cts": "1970-01-01T00:00:00.000Z",
20
+ "src": "user(0)",
21
+ "uri": "record(1)",
22
+ "val": "self-label-a",
23
+ },
24
+ {
25
+ "cid": "cids(2)",
26
+ "cts": "1970-01-01T00:00:00.000Z",
27
+ "src": "user(0)",
28
+ "uri": "record(1)",
29
+ "val": "self-label-b",
30
+ },
31
+ ],
32
+ },
33
+ "bookmarkCount": 0,
34
+ "cid": "cids(0)",
35
+ "embed": {
36
+ "$type": "app.bsky.embed.recordWithMedia#view",
37
+ "media": {
38
+ "$type": "app.bsky.embed.gallery#view",
39
+ "items": [
40
+ {
41
+ "$type": "app.bsky.embed.gallery#viewImage",
42
+ "alt": "landscape",
43
+ "aspectRatio": {
44
+ "height": 3,
45
+ "width": 4,
46
+ },
47
+ "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(1)/cids(3)",
48
+ "thumbnail": "https://bsky.public.url/img/feed_thumbnail/plain/user(1)/cids(3)",
49
+ },
50
+ ],
51
+ },
52
+ "record": {
53
+ "record": {
54
+ "$type": "app.bsky.embed.record#viewRecord",
55
+ "author": {
56
+ "associated": {
57
+ "activitySubscription": {
58
+ "allowSubscriptions": "followers",
59
+ },
60
+ },
61
+ "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)",
62
+ "createdAt": "1970-01-01T00:00:00.000Z",
63
+ "did": "user(0)",
64
+ "displayName": "ali",
65
+ "handle": "alice.test",
66
+ "labels": [
67
+ {
68
+ "cid": "cids(2)",
69
+ "cts": "1970-01-01T00:00:00.000Z",
70
+ "src": "user(0)",
71
+ "uri": "record(1)",
72
+ "val": "self-label-a",
73
+ },
74
+ {
75
+ "cid": "cids(2)",
76
+ "cts": "1970-01-01T00:00:00.000Z",
77
+ "src": "user(0)",
78
+ "uri": "record(1)",
79
+ "val": "self-label-b",
80
+ },
81
+ ],
82
+ },
83
+ "cid": "cids(4)",
84
+ "embeds": [],
85
+ "indexedAt": "1970-01-01T00:00:00.000Z",
86
+ "labels": [],
87
+ "likeCount": 0,
88
+ "quoteCount": 1,
89
+ "replyCount": 0,
90
+ "repostCount": 0,
91
+ "uri": "record(2)",
92
+ "value": {
93
+ "$type": "app.bsky.feed.post",
94
+ "createdAt": "1970-01-01T00:00:00.000Z",
95
+ "text": "embedded",
96
+ },
97
+ },
98
+ },
99
+ },
100
+ "indexedAt": "1970-01-01T00:00:00.000Z",
101
+ "labels": [],
102
+ "likeCount": 0,
103
+ "quoteCount": 0,
104
+ "record": {
105
+ "$type": "app.bsky.feed.post",
106
+ "createdAt": "1970-01-01T00:00:00.000Z",
107
+ "embed": {
108
+ "$type": "app.bsky.embed.recordWithMedia",
109
+ "media": {
110
+ "$type": "app.bsky.embed.gallery",
111
+ "items": [
112
+ {
113
+ "$type": "app.bsky.embed.gallery#image",
114
+ "alt": "landscape",
115
+ "aspectRatio": {
116
+ "height": 3,
117
+ "width": 4,
118
+ },
119
+ "image": {
120
+ "$type": "blob",
121
+ "mimeType": "image/jpeg",
122
+ "ref": {
123
+ "$link": "cids(3)",
124
+ },
125
+ "size": 4114,
126
+ },
127
+ },
128
+ ],
129
+ },
130
+ "record": {
131
+ "record": {
132
+ "cid": "cids(4)",
133
+ "uri": "record(2)",
134
+ },
135
+ },
136
+ },
137
+ "text": "gallery + record",
138
+ },
139
+ "replyCount": 0,
140
+ "repostCount": 0,
141
+ "uri": "record(0)",
142
+ }
143
+ `;
144
+
145
+ exports[`pds posts views > embeds gallery. 1`] = `
146
+ {
147
+ "author": {
148
+ "associated": {
149
+ "activitySubscription": {
150
+ "allowSubscriptions": "followers",
151
+ },
152
+ },
153
+ "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)",
154
+ "createdAt": "1970-01-01T00:00:00.000Z",
155
+ "did": "user(0)",
156
+ "displayName": "ali",
157
+ "handle": "alice.test",
158
+ "labels": [
159
+ {
160
+ "cid": "cids(2)",
161
+ "cts": "1970-01-01T00:00:00.000Z",
162
+ "src": "user(0)",
163
+ "uri": "record(1)",
164
+ "val": "self-label-a",
165
+ },
166
+ {
167
+ "cid": "cids(2)",
168
+ "cts": "1970-01-01T00:00:00.000Z",
169
+ "src": "user(0)",
170
+ "uri": "record(1)",
171
+ "val": "self-label-b",
172
+ },
173
+ ],
174
+ },
175
+ "bookmarkCount": 0,
176
+ "cid": "cids(0)",
177
+ "embed": {
178
+ "$type": "app.bsky.embed.gallery#view",
179
+ "items": [
180
+ {
181
+ "$type": "app.bsky.embed.gallery#viewImage",
182
+ "alt": "landscape",
183
+ "aspectRatio": {
184
+ "height": 3,
185
+ "width": 4,
186
+ },
187
+ "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(1)/cids(3)",
188
+ "thumbnail": "https://bsky.public.url/img/feed_thumbnail/plain/user(1)/cids(3)",
189
+ },
190
+ {
191
+ "$type": "app.bsky.embed.gallery#viewImage",
192
+ "alt": "portrait",
193
+ "aspectRatio": {
194
+ "height": 4,
195
+ "width": 3,
196
+ },
197
+ "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(1)/cids(1)",
198
+ "thumbnail": "https://bsky.public.url/img/feed_thumbnail/plain/user(1)/cids(1)",
199
+ },
200
+ ],
201
+ },
202
+ "indexedAt": "1970-01-01T00:00:00.000Z",
203
+ "labels": [],
204
+ "likeCount": 0,
205
+ "quoteCount": 0,
206
+ "record": {
207
+ "$type": "app.bsky.feed.post",
208
+ "createdAt": "1970-01-01T00:00:00.000Z",
209
+ "embed": {
210
+ "$type": "app.bsky.embed.gallery",
211
+ "items": [
212
+ {
213
+ "$type": "app.bsky.embed.gallery#image",
214
+ "alt": "landscape",
215
+ "aspectRatio": {
216
+ "height": 3,
217
+ "width": 4,
218
+ },
219
+ "image": {
220
+ "$type": "blob",
221
+ "mimeType": "image/jpeg",
222
+ "ref": {
223
+ "$link": "cids(3)",
224
+ },
225
+ "size": 4114,
226
+ },
227
+ },
228
+ {
229
+ "$type": "app.bsky.embed.gallery#image",
230
+ "alt": "portrait",
231
+ "aspectRatio": {
232
+ "height": 4,
233
+ "width": 3,
234
+ },
235
+ "image": {
236
+ "$type": "blob",
237
+ "mimeType": "image/jpeg",
238
+ "ref": {
239
+ "$link": "cids(1)",
240
+ },
241
+ "size": 3976,
242
+ },
243
+ },
244
+ ],
245
+ },
246
+ "text": "gallery",
247
+ },
248
+ "replyCount": 0,
249
+ "repostCount": 0,
250
+ "uri": "record(0)",
251
+ }
252
+ `;
253
+
3
254
  exports[`pds posts views > embeds video with record. 1`] = `
4
255
  {
5
256
  "author": {