@atproto/bsky 0.0.251 → 0.0.254
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.
- package/CHANGELOG.md +50 -0
- package/dist/api/age-assurance/const.js +19 -19
- package/dist/api/age-assurance/const.js.map +1 -1
- package/dist/kws.d.ts +8 -0
- package/dist/kws.d.ts.map +1 -1
- package/dist/kws.js +53 -4
- package/dist/kws.js.map +1 -1
- package/dist/lexicons/app/bsky/actor/defs.defs.d.ts +6 -0
- package/dist/lexicons/app/bsky/actor/defs.defs.d.ts.map +1 -1
- package/dist/lexicons/app/bsky/actor/defs.defs.js +1 -0
- package/dist/lexicons/app/bsky/actor/defs.defs.js.map +1 -1
- package/package.json +28 -23
- package/bin/migration-create.ts +0 -46
- package/buf.gen.yaml +0 -12
- package/proto/bsky.proto +0 -1907
- package/proto/courier.proto +0 -84
- package/proto/rolodex.proto +0 -116
- package/src/api/age-assurance/const.ts +0 -203
- package/src/api/age-assurance/index.ts +0 -34
- package/src/api/age-assurance/kws/age-verified.test.ts +0 -98
- package/src/api/age-assurance/kws/age-verified.ts +0 -81
- package/src/api/age-assurance/kws/const.ts +0 -33
- package/src/api/age-assurance/kws/external-payload.test.ts +0 -72
- package/src/api/age-assurance/kws/external-payload.ts +0 -149
- package/src/api/age-assurance/redirects/kws-age-verified.ts +0 -107
- package/src/api/age-assurance/stash.ts +0 -23
- package/src/api/age-assurance/types.ts +0 -10
- package/src/api/age-assurance/util.ts +0 -68
- package/src/api/age-assurance/webhooks/kws-age-verified.ts +0 -75
- package/src/api/app/bsky/actor/getProfile.ts +0 -106
- package/src/api/app/bsky/actor/getProfiles.ts +0 -99
- package/src/api/app/bsky/actor/getSuggestions.ts +0 -163
- package/src/api/app/bsky/actor/searchActors.ts +0 -171
- package/src/api/app/bsky/actor/searchActorsTypeahead.ts +0 -164
- package/src/api/app/bsky/ageassurance/begin.ts +0 -175
- package/src/api/app/bsky/ageassurance/getConfig.ts +0 -16
- package/src/api/app/bsky/ageassurance/getState.ts +0 -58
- package/src/api/app/bsky/bookmark/createBookmark.ts +0 -39
- package/src/api/app/bsky/bookmark/deleteBookmark.ts +0 -32
- package/src/api/app/bsky/bookmark/getBookmarks.ts +0 -97
- package/src/api/app/bsky/bookmark/util.ts +0 -13
- package/src/api/app/bsky/contact/dismissMatch.ts +0 -26
- package/src/api/app/bsky/contact/getMatches.ts +0 -111
- package/src/api/app/bsky/contact/getSyncStatus.ts +0 -38
- package/src/api/app/bsky/contact/importContacts.ts +0 -119
- package/src/api/app/bsky/contact/removeData.ts +0 -25
- package/src/api/app/bsky/contact/sendNotification.ts +0 -33
- package/src/api/app/bsky/contact/startPhoneVerification.ts +0 -26
- package/src/api/app/bsky/contact/util.ts +0 -92
- package/src/api/app/bsky/contact/verifyPhone.ts +0 -29
- package/src/api/app/bsky/draft/createDraft.ts +0 -45
- package/src/api/app/bsky/draft/deleteDraft.ts +0 -20
- package/src/api/app/bsky/draft/getDrafts.ts +0 -44
- package/src/api/app/bsky/draft/updateDraft.ts +0 -26
- package/src/api/app/bsky/embed/getEmbedExternalView.ts +0 -154
- package/src/api/app/bsky/feed/getActorFeeds.ts +0 -101
- package/src/api/app/bsky/feed/getActorLikes.ts +0 -128
- package/src/api/app/bsky/feed/getAuthorFeed.ts +0 -279
- package/src/api/app/bsky/feed/getFeed.ts +0 -282
- package/src/api/app/bsky/feed/getFeedGenerator.ts +0 -68
- package/src/api/app/bsky/feed/getFeedGenerators.ts +0 -86
- package/src/api/app/bsky/feed/getLikes.ts +0 -151
- package/src/api/app/bsky/feed/getListFeed.ts +0 -147
- package/src/api/app/bsky/feed/getPostThread.ts +0 -156
- package/src/api/app/bsky/feed/getPosts.ts +0 -100
- package/src/api/app/bsky/feed/getQuotes.ts +0 -122
- package/src/api/app/bsky/feed/getRepostedBy.ts +0 -121
- package/src/api/app/bsky/feed/getSuggestedFeeds.ts +0 -39
- package/src/api/app/bsky/feed/getTimeline.ts +0 -127
- package/src/api/app/bsky/feed/searchPosts.ts +0 -274
- package/src/api/app/bsky/feed/searchPostsV2.ts +0 -243
- package/src/api/app/bsky/graph/getActorStarterPacks.ts +0 -103
- package/src/api/app/bsky/graph/getBlocks.ts +0 -94
- package/src/api/app/bsky/graph/getFollowers.ts +0 -153
- package/src/api/app/bsky/graph/getFollows.ts +0 -151
- package/src/api/app/bsky/graph/getKnownFollowers.ts +0 -123
- package/src/api/app/bsky/graph/getList.ts +0 -152
- package/src/api/app/bsky/graph/getListBlocks.ts +0 -87
- package/src/api/app/bsky/graph/getListMutes.ts +0 -87
- package/src/api/app/bsky/graph/getLists.ts +0 -125
- package/src/api/app/bsky/graph/getListsWithMembership.ts +0 -146
- package/src/api/app/bsky/graph/getMutes.ts +0 -87
- package/src/api/app/bsky/graph/getRelationships.ts +0 -58
- package/src/api/app/bsky/graph/getStarterPack.ts +0 -82
- package/src/api/app/bsky/graph/getStarterPacks.ts +0 -86
- package/src/api/app/bsky/graph/getStarterPacksWithMembership.ts +0 -136
- package/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts +0 -171
- package/src/api/app/bsky/graph/muteActor.ts +0 -24
- package/src/api/app/bsky/graph/muteActorList.ts +0 -19
- package/src/api/app/bsky/graph/muteThread.ts +0 -19
- package/src/api/app/bsky/graph/searchStarterPacks.ts +0 -169
- package/src/api/app/bsky/graph/unmuteActor.ts +0 -21
- package/src/api/app/bsky/graph/unmuteActorList.ts +0 -19
- package/src/api/app/bsky/graph/unmuteThread.ts +0 -19
- package/src/api/app/bsky/labeler/getServices.ts +0 -41
- package/src/api/app/bsky/notification/getPreferences.ts +0 -49
- package/src/api/app/bsky/notification/getUnreadCount.ts +0 -83
- package/src/api/app/bsky/notification/listActivitySubscriptions.ts +0 -116
- package/src/api/app/bsky/notification/listNotifications.ts +0 -272
- package/src/api/app/bsky/notification/putActivitySubscription.ts +0 -69
- package/src/api/app/bsky/notification/putPreferences.ts +0 -17
- package/src/api/app/bsky/notification/putPreferencesV2.ts +0 -64
- package/src/api/app/bsky/notification/registerPush.ts +0 -40
- package/src/api/app/bsky/notification/unregisterPush.ts +0 -39
- package/src/api/app/bsky/notification/updateSeen.ts +0 -52
- package/src/api/app/bsky/notification/util.ts +0 -134
- package/src/api/app/bsky/unspecced/getAgeAssuranceState.ts +0 -48
- package/src/api/app/bsky/unspecced/getConfig.ts +0 -20
- package/src/api/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.ts +0 -162
- package/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +0 -91
- package/src/api/app/bsky/unspecced/getPostThreadOtherV2.ts +0 -126
- package/src/api/app/bsky/unspecced/getPostThreadV2.ts +0 -148
- package/src/api/app/bsky/unspecced/getSuggestedFeeds.ts +0 -100
- package/src/api/app/bsky/unspecced/getSuggestedOnboardingUsers.ts +0 -143
- package/src/api/app/bsky/unspecced/getSuggestedStarterPacks.ts +0 -165
- package/src/api/app/bsky/unspecced/getSuggestedUsers.ts +0 -183
- package/src/api/app/bsky/unspecced/getSuggestedUsersForDiscover.ts +0 -175
- package/src/api/app/bsky/unspecced/getSuggestedUsersForExplore.ts +0 -178
- package/src/api/app/bsky/unspecced/getSuggestedUsersForSeeMore.ts +0 -178
- package/src/api/app/bsky/unspecced/getTaggedSuggestions.ts +0 -27
- package/src/api/app/bsky/unspecced/getTrendingTopics.ts +0 -96
- package/src/api/app/bsky/unspecced/getTrends.ts +0 -162
- package/src/api/app/bsky/unspecced/initAgeAssurance.ts +0 -150
- package/src/api/blob-dispatcher.ts +0 -41
- package/src/api/blob-resolver.ts +0 -401
- package/src/api/com/atproto/admin/getAccountInfos.ts +0 -49
- package/src/api/com/atproto/admin/getSubjectStatus.ts +0 -79
- package/src/api/com/atproto/admin/updateSubjectStatus.ts +0 -80
- package/src/api/com/atproto/identity/resolveHandle.ts +0 -19
- package/src/api/com/atproto/label/queryLabels.ts +0 -42
- package/src/api/com/atproto/repo/getRecord.ts +0 -44
- package/src/api/com/atproto/temp/fetchLabels.ts +0 -9
- package/src/api/external.ts +0 -15
- package/src/api/health.ts +0 -47
- package/src/api/index.ts +0 -216
- package/src/api/internal/bsky/actor/getProfiles.ts +0 -87
- package/src/api/kws/api.ts +0 -113
- package/src/api/kws/index.ts +0 -29
- package/src/api/kws/types.ts +0 -78
- package/src/api/kws/util.test.ts +0 -50
- package/src/api/kws/util.ts +0 -109
- package/src/api/kws/webhook.ts +0 -134
- package/src/api/sitemap.ts +0 -76
- package/src/api/util.ts +0 -45
- package/src/api/well-known.ts +0 -44
- package/src/auth-verifier.ts +0 -522
- package/src/bsync.ts +0 -41
- package/src/cache/read-through.ts +0 -157
- package/src/config.ts +0 -719
- package/src/context.ts +0 -143
- package/src/courier.ts +0 -41
- package/src/data-plane/client/hosts.ts +0 -103
- package/src/data-plane/client/index.ts +0 -125
- package/src/data-plane/client/util.test.ts +0 -29
- package/src/data-plane/client/util.ts +0 -74
- package/src/data-plane/index.ts +0 -2
- package/src/data-plane/server/background.ts +0 -66
- package/src/data-plane/server/bsync-subscription.ts +0 -590
- package/src/data-plane/server/db/database-schema.ts +0 -90
- package/src/data-plane/server/db/db.ts +0 -204
- package/src/data-plane/server/db/index.ts +0 -2
- package/src/data-plane/server/db/migrations/20230309T045948368Z-init.ts +0 -305
- package/src/data-plane/server/db/migrations/20230408T152211201Z-notification-init.ts +0 -25
- package/src/data-plane/server/db/migrations/20230417T210628672Z-moderation-init.ts +0 -127
- package/src/data-plane/server/db/migrations/20230420T211446071Z-did-cache.ts +0 -14
- package/src/data-plane/server/db/migrations/20230427T194702079Z-notif-record-index.ts +0 -14
- package/src/data-plane/server/db/migrations/20230605T144730094Z-post-profile-aggs.ts +0 -23
- package/src/data-plane/server/db/migrations/20230607T211442112Z-feed-generator-init.ts +0 -34
- package/src/data-plane/server/db/migrations/20230608T155101190Z-algo-whats-hot-view.ts +0 -85
- package/src/data-plane/server/db/migrations/20230608T201813132Z-mute-lists.ts +0 -78
- package/src/data-plane/server/db/migrations/20230608T205147239Z-mutes.ts +0 -15
- package/src/data-plane/server/db/migrations/20230609T153623961Z-blocks.ts +0 -33
- package/src/data-plane/server/db/migrations/20230609T232122649Z-actor-deletion-indexes.ts +0 -27
- package/src/data-plane/server/db/migrations/20230610T203555962Z-suggested-follows.ts +0 -13
- package/src/data-plane/server/db/migrations/20230611T215300060Z-actor-state.ts +0 -13
- package/src/data-plane/server/db/migrations/20230620T161134972Z-post-langs.ts +0 -9
- package/src/data-plane/server/db/migrations/20230627T212437895Z-optional-handle.ts +0 -15
- package/src/data-plane/server/db/migrations/20230629T220835893Z-remove-post-hierarchy.ts +0 -32
- package/src/data-plane/server/db/migrations/20230703T045536691Z-feed-and-label-indices.ts +0 -31
- package/src/data-plane/server/db/migrations/20230720T164800037Z-posts-cursor-idx.ts +0 -19
- package/src/data-plane/server/db/migrations/20230807T035309811Z-feed-item-delete-invite-for-user-idx.ts +0 -14
- package/src/data-plane/server/db/migrations/20230808T172902639Z-repo-rev.ts +0 -12
- package/src/data-plane/server/db/migrations/20230810T203349843Z-action-duration.ts +0 -23
- package/src/data-plane/server/db/migrations/20230817T195936007Z-native-notifications.ts +0 -16
- package/src/data-plane/server/db/migrations/20230830T205507322Z-suggested-feeds.ts +0 -13
- package/src/data-plane/server/db/migrations/20230904T211011773Z-block-lists.ts +0 -24
- package/src/data-plane/server/db/migrations/20230906T222220386Z-thread-gating.ts +0 -27
- package/src/data-plane/server/db/migrations/20230920T213858047Z-add-tags-to-post.ts +0 -9
- package/src/data-plane/server/db/migrations/20230929T192920807Z-record-cursor-indexes.ts +0 -40
- package/src/data-plane/server/db/migrations/20231003T202833377Z-create-moderation-subject-status.ts +0 -123
- package/src/data-plane/server/db/migrations/20231220T225126090Z-blob-takedowns.ts +0 -66
- package/src/data-plane/server/db/migrations/20240124T023719200Z-tagged-suggestions.ts +0 -15
- package/src/data-plane/server/db/migrations/20240226T225725627Z-labelers.ts +0 -27
- package/src/data-plane/server/db/migrations/20240530T170337073Z-account-deactivation.ts +0 -12
- package/src/data-plane/server/db/migrations/20240606T171229898Z-thread-mutes.ts +0 -15
- package/src/data-plane/server/db/migrations/20240606T222548219Z-starter-packs.ts +0 -45
- package/src/data-plane/server/db/migrations/20240719T203853939Z-priority-notifs.ts +0 -25
- package/src/data-plane/server/db/migrations/20240723T220700077Z-quotes-post-aggs.ts +0 -12
- package/src/data-plane/server/db/migrations/20240723T220703655Z-quotes.ts +0 -28
- package/src/data-plane/server/db/migrations/20240801T193939827Z-post-gate.ts +0 -17
- package/src/data-plane/server/db/migrations/20240808T224251220Z-post-gate-flags.ts +0 -25
- package/src/data-plane/server/db/migrations/20240829T211238293Z-simplify-actor-sync.ts +0 -23
- package/src/data-plane/server/db/migrations/20240831T134810923Z-pinned-posts.ts +0 -17
- package/src/data-plane/server/db/migrations/20241114T153108102Z-add-starter-packs-name.ts +0 -19
- package/src/data-plane/server/db/migrations/20250116T222618297Z-post-embed-video.ts +0 -17
- package/src/data-plane/server/db/migrations/20250207T174822012Z-add-label-exp.ts +0 -9
- package/src/data-plane/server/db/migrations/20250404T163421487Z-verifications.ts +0 -39
- package/src/data-plane/server/db/migrations/20250526T023712742Z-like-repost-via.ts +0 -17
- package/src/data-plane/server/db/migrations/20250528T221913281Z-add-record-tags.ts +0 -9
- package/src/data-plane/server/db/migrations/20250602T190357447Z-add-private-data.ts +0 -22
- package/src/data-plane/server/db/migrations/20250611T140649895Z-add-activity-subscription.ts +0 -22
- package/src/data-plane/server/db/migrations/20250627T025331240Z-add-actor-age-assurance-columns.ts +0 -22
- package/src/data-plane/server/db/migrations/20250812T183735692Z-add-bookmarks.ts +0 -23
- package/src/data-plane/server/db/migrations/20250813T174955711Z-add-post-agg-bookmarks.ts +0 -12
- package/src/data-plane/server/db/migrations/20251120T004738098Z-update-actor-age-assurance-v2.ts +0 -28
- package/src/data-plane/server/db/migrations/20260112T133951271Z-add-drafts.ts +0 -24
- package/src/data-plane/server/db/migrations/20260604T224952774Z-post-embed-gallery-image.ts +0 -19
- package/src/data-plane/server/db/migrations/index.ts +0 -60
- package/src/data-plane/server/db/migrations/provider.ts +0 -26
- package/src/data-plane/server/db/pagination.ts +0 -341
- package/src/data-plane/server/db/tables/activity-subscription.ts +0 -12
- package/src/data-plane/server/db/tables/actor-block.ts +0 -14
- package/src/data-plane/server/db/tables/actor-state.ts +0 -10
- package/src/data-plane/server/db/tables/actor-sync.ts +0 -9
- package/src/data-plane/server/db/tables/actor.ts +0 -19
- package/src/data-plane/server/db/tables/algo.ts +0 -11
- package/src/data-plane/server/db/tables/blob-takedown.ts +0 -9
- package/src/data-plane/server/db/tables/bookmark.ts +0 -11
- package/src/data-plane/server/db/tables/did-cache.ts +0 -13
- package/src/data-plane/server/db/tables/draft.ts +0 -11
- package/src/data-plane/server/db/tables/duplicate-record.ts +0 -12
- package/src/data-plane/server/db/tables/feed-generator.ts +0 -21
- package/src/data-plane/server/db/tables/feed-item.ts +0 -12
- package/src/data-plane/server/db/tables/follow.ts +0 -15
- package/src/data-plane/server/db/tables/label.ts +0 -13
- package/src/data-plane/server/db/tables/labeler.ts +0 -16
- package/src/data-plane/server/db/tables/like.ts +0 -18
- package/src/data-plane/server/db/tables/list-block.ts +0 -15
- package/src/data-plane/server/db/tables/list-item.ts +0 -16
- package/src/data-plane/server/db/tables/list-mute.ts +0 -9
- package/src/data-plane/server/db/tables/list.ts +0 -19
- package/src/data-plane/server/db/tables/mute.ts +0 -9
- package/src/data-plane/server/db/tables/notification-push-token.ts +0 -10
- package/src/data-plane/server/db/tables/notification.ts +0 -16
- package/src/data-plane/server/db/tables/post-agg.ts +0 -16
- package/src/data-plane/server/db/tables/post-embed.ts +0 -47
- package/src/data-plane/server/db/tables/post-gate.ts +0 -12
- package/src/data-plane/server/db/tables/post.ts +0 -28
- package/src/data-plane/server/db/tables/private-data.ts +0 -13
- package/src/data-plane/server/db/tables/profile-agg.ts +0 -14
- package/src/data-plane/server/db/tables/profile.ts +0 -17
- package/src/data-plane/server/db/tables/quote.ts +0 -15
- package/src/data-plane/server/db/tables/record.ts +0 -15
- package/src/data-plane/server/db/tables/repost.ts +0 -18
- package/src/data-plane/server/db/tables/starter-pack.ts +0 -17
- package/src/data-plane/server/db/tables/subscription.ts +0 -9
- package/src/data-plane/server/db/tables/suggested-feed.ts +0 -10
- package/src/data-plane/server/db/tables/suggested-follow.ts +0 -10
- package/src/data-plane/server/db/tables/tagged-suggestion.ts +0 -11
- package/src/data-plane/server/db/tables/thread-gate.ts +0 -12
- package/src/data-plane/server/db/tables/thread-mute.ts +0 -9
- package/src/data-plane/server/db/tables/verification.ts +0 -20
- package/src/data-plane/server/db/tables/view-param.ts +0 -12
- package/src/data-plane/server/db/types.ts +0 -12
- package/src/data-plane/server/db/util.ts +0 -67
- package/src/data-plane/server/index.ts +0 -44
- package/src/data-plane/server/indexing/index.ts +0 -458
- package/src/data-plane/server/indexing/plugins/block.ts +0 -84
- package/src/data-plane/server/indexing/plugins/chat-declaration.ts +0 -60
- package/src/data-plane/server/indexing/plugins/feed-generator.ts +0 -80
- package/src/data-plane/server/indexing/plugins/follow.ts +0 -132
- package/src/data-plane/server/indexing/plugins/germ-declaration.ts +0 -57
- package/src/data-plane/server/indexing/plugins/labeler.ts +0 -74
- package/src/data-plane/server/indexing/plugins/like.ts +0 -151
- package/src/data-plane/server/indexing/plugins/list-block.ts +0 -84
- package/src/data-plane/server/indexing/plugins/list-item.ts +0 -92
- package/src/data-plane/server/indexing/plugins/list.ts +0 -80
- package/src/data-plane/server/indexing/plugins/notif-declaration.ts +0 -57
- package/src/data-plane/server/indexing/plugins/post-gate.ts +0 -101
- package/src/data-plane/server/indexing/plugins/post.ts +0 -675
- package/src/data-plane/server/indexing/plugins/profile.ts +0 -90
- package/src/data-plane/server/indexing/plugins/repost.ts +0 -176
- package/src/data-plane/server/indexing/plugins/starter-pack.ts +0 -74
- package/src/data-plane/server/indexing/plugins/status.ts +0 -57
- package/src/data-plane/server/indexing/plugins/thread-gate.ts +0 -101
- package/src/data-plane/server/indexing/plugins/verification.ts +0 -113
- package/src/data-plane/server/indexing/processor.ts +0 -292
- package/src/data-plane/server/routes/activity-subscription.ts +0 -84
- package/src/data-plane/server/routes/blocks.ts +0 -127
- package/src/data-plane/server/routes/bookmarks.ts +0 -83
- package/src/data-plane/server/routes/drafts.ts +0 -37
- package/src/data-plane/server/routes/feed-gens.ts +0 -89
- package/src/data-plane/server/routes/feeds.ts +0 -176
- package/src/data-plane/server/routes/follows.ts +0 -141
- package/src/data-plane/server/routes/identity.ts +0 -59
- package/src/data-plane/server/routes/index.ts +0 -67
- package/src/data-plane/server/routes/interactions.ts +0 -120
- package/src/data-plane/server/routes/labels.ts +0 -55
- package/src/data-plane/server/routes/likes.ts +0 -92
- package/src/data-plane/server/routes/lists.ts +0 -88
- package/src/data-plane/server/routes/moderation.ts +0 -102
- package/src/data-plane/server/routes/mutes.ts +0 -195
- package/src/data-plane/server/routes/notifs.ts +0 -239
- package/src/data-plane/server/routes/profile.ts +0 -220
- package/src/data-plane/server/routes/quotes.ts +0 -32
- package/src/data-plane/server/routes/records.ts +0 -119
- package/src/data-plane/server/routes/relationships.ts +0 -174
- package/src/data-plane/server/routes/reposts.ts +0 -76
- package/src/data-plane/server/routes/search.ts +0 -181
- package/src/data-plane/server/routes/site-standard.ts +0 -12
- package/src/data-plane/server/routes/sitemap.ts +0 -43
- package/src/data-plane/server/routes/starter-packs.ts +0 -32
- package/src/data-plane/server/routes/suggestions.ts +0 -175
- package/src/data-plane/server/routes/sync.ts +0 -16
- package/src/data-plane/server/routes/threads.ts +0 -33
- package/src/data-plane/server/subscription.ts +0 -152
- package/src/data-plane/server/util.ts +0 -202
- package/src/error.ts +0 -12
- package/src/etcd.ts +0 -90
- package/src/feature-gates/README.md +0 -83
- package/src/feature-gates/gates.ts +0 -25
- package/src/feature-gates/index.ts +0 -235
- package/src/feature-gates/metrics.test.ts +0 -196
- package/src/feature-gates/metrics.ts +0 -107
- package/src/feature-gates/types.ts +0 -62
- package/src/feature-gates/utils.test.ts +0 -66
- package/src/feature-gates/utils.ts +0 -135
- package/src/hydration/actor.ts +0 -578
- package/src/hydration/external.test.ts +0 -281
- package/src/hydration/external.ts +0 -268
- package/src/hydration/feed.ts +0 -455
- package/src/hydration/graph.ts +0 -354
- package/src/hydration/hydrator.ts +0 -1835
- package/src/hydration/label.ts +0 -207
- package/src/hydration/util.ts +0 -199
- package/src/image/index.ts +0 -2
- package/src/image/invalidator.ts +0 -33
- package/src/image/logger.ts +0 -6
- package/src/image/server.ts +0 -228
- package/src/image/sharp.ts +0 -89
- package/src/image/uri.ts +0 -114
- package/src/image/util.ts +0 -41
- package/src/index.ts +0 -312
- package/src/kws.ts +0 -197
- package/src/logger.ts +0 -40
- package/src/pipeline.ts +0 -60
- package/src/redis.ts +0 -205
- package/src/rolodex.ts +0 -42
- package/src/stash.ts +0 -76
- package/src/util/debug.ts +0 -10
- package/src/util/http.ts +0 -42
- package/src/util/retry.ts +0 -10
- package/src/util/standard-site.test.ts +0 -423
- package/src/util/standard-site.ts +0 -196
- package/src/util/uris.ts +0 -38
- package/src/util.ts +0 -48
- package/src/views/index.ts +0 -2783
- package/src/views/threads-v2.ts +0 -510
- package/src/views/types.ts +0 -193
- package/src/views/util.ts +0 -114
- package/test.env +0 -2
- package/tests/__snapshots__/feed-generation.test.ts.snap +0 -2510
- package/tests/_util.ts +0 -292
- package/tests/admin/admin-auth.test.ts +0 -190
- package/tests/admin/moderation.test.ts +0 -222
- package/tests/auth.test.ts +0 -90
- package/tests/blob-resolver.test.ts +0 -125
- package/tests/data-plane/__snapshots__/indexing.test.ts.snap +0 -887
- package/tests/data-plane/db.test.ts +0 -189
- package/tests/data-plane/duplicate-records.test.ts +0 -156
- package/tests/data-plane/handle-invalidation.test.ts +0 -153
- package/tests/data-plane/indexing.test.ts +0 -827
- package/tests/data-plane/subscription.test.ts +0 -128
- package/tests/data-plane/thread-mutes.test.ts +0 -175
- package/tests/entryway-auth.test.ts +0 -177
- package/tests/etcd.test.ts +0 -302
- package/tests/feed-generation.test.ts +0 -965
- package/tests/hydration/util.test.ts +0 -119
- package/tests/image/server.test.ts +0 -97
- package/tests/image/sharp.test.ts +0 -199
- package/tests/image/uri.test.ts +0 -106
- package/tests/label-hydration.test.ts +0 -245
- package/tests/postgates.test.ts +0 -182
- package/tests/query-labels.test.ts +0 -85
- package/tests/redis-cache.test.ts +0 -230
- package/tests/seed/feed-hidden-replies.ts +0 -63
- package/tests/seed/get-suggested-starter-packs.ts +0 -64
- package/tests/seed/get-trends.ts +0 -71
- package/tests/seed/known-followers.ts +0 -111
- package/tests/seed/postgates.ts +0 -57
- package/tests/server.test.ts +0 -115
- package/tests/sitemap.test.ts +0 -75
- package/tests/stash.test.ts +0 -180
- package/tests/utils.test.ts +0 -36
- package/tests/views/__snapshots__/actor-search.test.ts.snap +0 -155
- package/tests/views/__snapshots__/author-feed.test.ts.snap +0 -6301
- package/tests/views/__snapshots__/block-lists.test.ts.snap +0 -690
- package/tests/views/__snapshots__/blocks.test.ts.snap +0 -462
- package/tests/views/__snapshots__/bookmarks.test.ts.snap +0 -379
- package/tests/views/__snapshots__/follows.test.ts.snap +0 -798
- package/tests/views/__snapshots__/labeler-service.test.ts.snap +0 -233
- package/tests/views/__snapshots__/likes.test.ts.snap +0 -124
- package/tests/views/__snapshots__/list-feed.test.ts.snap +0 -883
- package/tests/views/__snapshots__/lists.test.ts.snap +0 -650
- package/tests/views/__snapshots__/mute-lists.test.ts.snap +0 -705
- package/tests/views/__snapshots__/mutes.test.ts.snap +0 -353
- package/tests/views/__snapshots__/notifications.test.ts.snap +0 -3074
- package/tests/views/__snapshots__/posts.test.ts.snap +0 -1019
- package/tests/views/__snapshots__/profile.test.ts.snap +0 -811
- package/tests/views/__snapshots__/quotes.test.ts.snap +0 -445
- package/tests/views/__snapshots__/reposts.test.ts.snap +0 -112
- package/tests/views/__snapshots__/starter-packs.test.ts.snap +0 -836
- package/tests/views/__snapshots__/thread-v2.test.ts.snap +0 -1298
- package/tests/views/__snapshots__/thread.test.ts.snap +0 -2051
- package/tests/views/__snapshots__/threadgating.test.ts.snap +0 -189
- package/tests/views/__snapshots__/timeline.test.ts.snap +0 -7252
- package/tests/views/account-deactivation.test.ts +0 -81
- package/tests/views/actor-likes.test.ts +0 -138
- package/tests/views/actor-search.test.ts +0 -261
- package/tests/views/age-assurance-v2.test.ts +0 -823
- package/tests/views/age-assurance.test.ts +0 -479
- package/tests/views/author-feed.test.ts +0 -694
- package/tests/views/block-lists.test.ts +0 -661
- package/tests/views/blocks.test.ts +0 -889
- package/tests/views/bookmarks.test.ts +0 -345
- package/tests/views/drafts.test.ts +0 -430
- package/tests/views/feed-hidden-replies.test.ts +0 -243
- package/tests/views/feed-view-post.test.ts +0 -501
- package/tests/views/follows.test.ts +0 -372
- package/tests/views/get-config.test.ts +0 -64
- package/tests/views/get-suggested-onboarding-users.test.ts +0 -193
- package/tests/views/get-suggested-starter-packs.test.ts +0 -137
- package/tests/views/get-trends.test.ts +0 -141
- package/tests/views/internal-actor.test.ts +0 -127
- package/tests/views/known-followers.test.ts +0 -193
- package/tests/views/labeler-service.test.ts +0 -242
- package/tests/views/labels-needs-review.test.ts +0 -316
- package/tests/views/labels-takedown.test.ts +0 -247
- package/tests/views/likes.test.ts +0 -192
- package/tests/views/list-feed.test.ts +0 -195
- package/tests/views/lists.test.ts +0 -412
- package/tests/views/mute-lists.test.ts +0 -509
- package/tests/views/mutes.test.ts +0 -308
- package/tests/views/notifications.test.ts +0 -1581
- package/tests/views/post-search.test.ts +0 -281
- package/tests/views/posts-debug.test.ts +0 -79
- package/tests/views/posts.test.ts +0 -427
- package/tests/views/profile-debug.test.ts +0 -75
- package/tests/views/profile.test.ts +0 -795
- package/tests/views/quotes.test.ts +0 -133
- package/tests/views/reposts.test.ts +0 -112
- package/tests/views/starter-packs.test.ts +0 -395
- package/tests/views/suggestions.test.ts +0 -145
- package/tests/views/thread-v2.test.ts +0 -2005
- package/tests/views/thread.test.ts +0 -637
- package/tests/views/threadgating.test.ts +0 -843
- package/tests/views/timeline.test.ts +0 -328
- package/tests/views/verification.test.ts +0 -296
- package/tsconfig.build.json +0 -10
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -7
- package/tsconfig.tests.json +0 -8
- package/vitest.config.ts +0 -10
package/src/views/index.ts
DELETED
|
@@ -1,2783 +0,0 @@
|
|
|
1
|
-
import { HOUR, MINUTE, dedupeStrs, mapDefined } from '@atproto/common'
|
|
2
|
-
import {
|
|
3
|
-
$Typed,
|
|
4
|
-
Un$Typed,
|
|
5
|
-
Unknown$TypedObject,
|
|
6
|
-
UriString,
|
|
7
|
-
atUri,
|
|
8
|
-
getBlobCidString,
|
|
9
|
-
} from '@atproto/lex'
|
|
10
|
-
import {
|
|
11
|
-
AtUri,
|
|
12
|
-
AtUriString,
|
|
13
|
-
DatetimeString,
|
|
14
|
-
DidString,
|
|
15
|
-
INVALID_HANDLE,
|
|
16
|
-
normalizeDatetimeAlways,
|
|
17
|
-
} from '@atproto/syntax'
|
|
18
|
-
import { Actor, ProfileViewerState } from '../hydration/actor.js'
|
|
19
|
-
import {
|
|
20
|
-
AssociatedSiteStandardRecord,
|
|
21
|
-
SiteStandardDocument,
|
|
22
|
-
SiteStandardPublication,
|
|
23
|
-
getSiteStandardRecordsFromHydrationMapsByRefs,
|
|
24
|
-
} from '../hydration/external.js'
|
|
25
|
-
import { FeedItem, Like, Post, Repost } from '../hydration/feed.js'
|
|
26
|
-
import { Follow, Verification } from '../hydration/graph.js'
|
|
27
|
-
import { HydrationState } from '../hydration/hydrator.js'
|
|
28
|
-
import { Label } from '../hydration/label.js'
|
|
29
|
-
import { RecordInfo, parseString } from '../hydration/util.js'
|
|
30
|
-
import { ImageUriBuilder } from '../image/uri.js'
|
|
31
|
-
import { app, site } from '../lexicons/index.js'
|
|
32
|
-
import { viewsLogger } from '../logger.js'
|
|
33
|
-
import { Notification } from '../proto/bsky_pb.js'
|
|
34
|
-
import {
|
|
35
|
-
estimateReadingTimeMinutes,
|
|
36
|
-
validateStandardSiteForUrl,
|
|
37
|
-
} from '../util/standard-site.js'
|
|
38
|
-
import {
|
|
39
|
-
postUriToPostgateUri,
|
|
40
|
-
postUriToThreadgateUri,
|
|
41
|
-
safePinnedPost,
|
|
42
|
-
uriToDid,
|
|
43
|
-
uriToDid as creatorFromUri,
|
|
44
|
-
} from '../util/uris.js'
|
|
45
|
-
import {
|
|
46
|
-
ThreadItemValueBlocked,
|
|
47
|
-
ThreadItemValueNoUnauthenticated,
|
|
48
|
-
ThreadItemValueNotFound,
|
|
49
|
-
ThreadItemValuePost,
|
|
50
|
-
ThreadOtherAnchorPostNode,
|
|
51
|
-
ThreadOtherItemValuePost,
|
|
52
|
-
ThreadOtherPostNode,
|
|
53
|
-
ThreadTree,
|
|
54
|
-
ThreadTreeVisible,
|
|
55
|
-
sortTrimFlattenThreadTree,
|
|
56
|
-
} from './threads-v2.js'
|
|
57
|
-
import {
|
|
58
|
-
ActivitySubscription,
|
|
59
|
-
BlockedPost,
|
|
60
|
-
BookmarkView,
|
|
61
|
-
Embed,
|
|
62
|
-
EmbedView,
|
|
63
|
-
ExternalEmbed,
|
|
64
|
-
ExternalEmbedColorRgb,
|
|
65
|
-
ExternalEmbedSourceThemeView,
|
|
66
|
-
ExternalEmbedSourceView,
|
|
67
|
-
ExternalEmbedView,
|
|
68
|
-
FeedViewPost,
|
|
69
|
-
FollowRecord,
|
|
70
|
-
GalleryEmbed,
|
|
71
|
-
GalleryEmbedView,
|
|
72
|
-
GalleryImageEmbed,
|
|
73
|
-
GalleryImageEmbedView,
|
|
74
|
-
GeneratorView,
|
|
75
|
-
GetPostThreadV2QueryParams,
|
|
76
|
-
ImagesEmbed,
|
|
77
|
-
ImagesEmbedView,
|
|
78
|
-
KnownFollowers,
|
|
79
|
-
LabelerRecord,
|
|
80
|
-
LabelerView,
|
|
81
|
-
LabelerViewDetailed,
|
|
82
|
-
LikeRecord,
|
|
83
|
-
ListItemView,
|
|
84
|
-
ListView,
|
|
85
|
-
ListViewBasic,
|
|
86
|
-
MaybePostView,
|
|
87
|
-
NotFoundPost,
|
|
88
|
-
NotificationRecordDeleted,
|
|
89
|
-
NotificationView,
|
|
90
|
-
PostEmbedView,
|
|
91
|
-
PostRecord,
|
|
92
|
-
PostView,
|
|
93
|
-
ProfileAssociatedActivitySubscription,
|
|
94
|
-
ProfileAssociatedChat,
|
|
95
|
-
ProfileRecord,
|
|
96
|
-
ProfileView,
|
|
97
|
-
ProfileViewBasic,
|
|
98
|
-
ProfileViewDetailed,
|
|
99
|
-
ProfileViewer,
|
|
100
|
-
ReasonPin,
|
|
101
|
-
ReasonRepost,
|
|
102
|
-
RecordEmbed,
|
|
103
|
-
RecordEmbedView,
|
|
104
|
-
RecordEmbedViewInternal,
|
|
105
|
-
RecordWithMedia,
|
|
106
|
-
RecordWithMediaView,
|
|
107
|
-
ReplyRef,
|
|
108
|
-
RepostRecord,
|
|
109
|
-
SiteStandardPublicationRecord,
|
|
110
|
-
StarterPackView,
|
|
111
|
-
StarterPackViewBasic,
|
|
112
|
-
StatusView,
|
|
113
|
-
ThreadItem,
|
|
114
|
-
ThreadOtherItem,
|
|
115
|
-
ThreadViewPost,
|
|
116
|
-
ThreadgateView,
|
|
117
|
-
VerificationRecord,
|
|
118
|
-
VerificationState,
|
|
119
|
-
VerificationView,
|
|
120
|
-
VideoEmbed,
|
|
121
|
-
VideoEmbedView,
|
|
122
|
-
isExternalEmbedType,
|
|
123
|
-
isGalleryEmbedType,
|
|
124
|
-
isGalleryImageEmbedType,
|
|
125
|
-
isImagesEmbedType,
|
|
126
|
-
isLabelerRecordType,
|
|
127
|
-
isListRuleType,
|
|
128
|
-
isPostRecordType,
|
|
129
|
-
isPostViewType,
|
|
130
|
-
isProfileRecordType,
|
|
131
|
-
isRecordEmbedType,
|
|
132
|
-
isRecordWithMediaType,
|
|
133
|
-
isSelfLabelsType,
|
|
134
|
-
isVideoEmbedType,
|
|
135
|
-
} from './types.js'
|
|
136
|
-
import { VideoUriBuilder, parsePostgate, parseThreadGate } from './util.js'
|
|
137
|
-
|
|
138
|
-
const notificationDeletedRecord =
|
|
139
|
-
app.bsky.notification.defs.recordDeleted.$build({})
|
|
140
|
-
|
|
141
|
-
// Pre-computed CID for the `notificationDeletedRecord`.
|
|
142
|
-
const notificationDeletedRecordCid =
|
|
143
|
-
'bafyreidad6nyekfa4a67yfb573ptxiv6s7kyxyg2ra6qbbemcruadvtuim'
|
|
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
|
-
|
|
150
|
-
export class Views {
|
|
151
|
-
public imgUriBuilder: ImageUriBuilder = this.opts.imgUriBuilder
|
|
152
|
-
public videoUriBuilder: VideoUriBuilder = this.opts.videoUriBuilder
|
|
153
|
-
public indexedAtEpoch: Date | undefined = this.opts.indexedAtEpoch
|
|
154
|
-
private threadTagsBumpDown: readonly string[] = this.opts.threadTagsBumpDown
|
|
155
|
-
private threadTagsHide: readonly string[] = this.opts.threadTagsHide
|
|
156
|
-
private visibilityTagHide: string = this.opts.visibilityTagHide
|
|
157
|
-
private visibilityTagRankPrefix: string = this.opts.visibilityTagRankPrefix
|
|
158
|
-
constructor(
|
|
159
|
-
private opts: {
|
|
160
|
-
imgUriBuilder: ImageUriBuilder
|
|
161
|
-
videoUriBuilder: VideoUriBuilder
|
|
162
|
-
indexedAtEpoch: Date | undefined
|
|
163
|
-
threadTagsBumpDown: readonly string[]
|
|
164
|
-
threadTagsHide: readonly string[]
|
|
165
|
-
visibilityTagHide: string
|
|
166
|
-
visibilityTagRankPrefix: string
|
|
167
|
-
},
|
|
168
|
-
) {}
|
|
169
|
-
|
|
170
|
-
// Actor
|
|
171
|
-
// ------------
|
|
172
|
-
|
|
173
|
-
actorIsNoHosted(did: DidString, state: HydrationState): boolean {
|
|
174
|
-
return (
|
|
175
|
-
this.actorIsDeactivated(did, state) || this.actorIsTakendown(did, state)
|
|
176
|
-
)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
actorIsDeactivated(did: DidString, state: HydrationState): boolean {
|
|
180
|
-
if (state.actors?.get(did)?.upstreamStatus === 'deactivated') return true
|
|
181
|
-
return false
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
actorIsTakendown(did: DidString, state: HydrationState): boolean {
|
|
185
|
-
const actor = state.actors?.get(did)
|
|
186
|
-
if (actor?.takedownRef) return true
|
|
187
|
-
if (actor?.upstreamStatus === 'takendown') return true
|
|
188
|
-
if (actor?.upstreamStatus === 'suspended') return true
|
|
189
|
-
if (state.labels?.get(did)?.isTakendown) return true
|
|
190
|
-
return false
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
noUnauthenticatedPost(state: HydrationState, post: PostView): boolean {
|
|
194
|
-
const isNoUnauthenticated = post.author.labels?.some(
|
|
195
|
-
(l) => l.val === '!no-unauthenticated',
|
|
196
|
-
)
|
|
197
|
-
return !state.ctx?.viewer && !!isNoUnauthenticated
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
viewerBlockExists(did: DidString, state: HydrationState): boolean {
|
|
201
|
-
if (state.ctx?.skipViewerBlocks) return false
|
|
202
|
-
const viewer = state.profileViewers?.get(did)
|
|
203
|
-
if (!viewer) return false
|
|
204
|
-
return !!(
|
|
205
|
-
viewer.blockedBy ||
|
|
206
|
-
viewer.blocking ||
|
|
207
|
-
this.blockedByList(viewer, state) ||
|
|
208
|
-
this.blockingByList(viewer, state)
|
|
209
|
-
)
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
viewerMuteExists(did: DidString, state: HydrationState): boolean {
|
|
213
|
-
const viewer = state.profileViewers?.get(did)
|
|
214
|
-
if (!viewer) return false
|
|
215
|
-
return !!(viewer.muted || this.mutedByList(viewer, state))
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
blockingByList(
|
|
219
|
-
viewer: ProfileViewerState,
|
|
220
|
-
state: HydrationState,
|
|
221
|
-
): undefined | AtUriString {
|
|
222
|
-
return (
|
|
223
|
-
viewer.blockingByList && this.recordActive(viewer.blockingByList, state)
|
|
224
|
-
)
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
blockedByList(
|
|
228
|
-
viewer: ProfileViewerState,
|
|
229
|
-
state: HydrationState,
|
|
230
|
-
): undefined | AtUriString {
|
|
231
|
-
return (
|
|
232
|
-
viewer.blockedByList && this.recordActive(viewer.blockedByList, state)
|
|
233
|
-
)
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
mutedByList(
|
|
237
|
-
viewer: ProfileViewerState,
|
|
238
|
-
state: HydrationState,
|
|
239
|
-
): undefined | AtUriString {
|
|
240
|
-
return viewer.mutedByList && this.recordActive(viewer.mutedByList, state)
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
recordActive(
|
|
244
|
-
uri: AtUriString,
|
|
245
|
-
state: HydrationState,
|
|
246
|
-
): AtUriString | undefined {
|
|
247
|
-
const did = uriToDid(uri)
|
|
248
|
-
const actor = state.actors?.get(did)
|
|
249
|
-
if (!actor || this.actorIsTakendown(did, state)) {
|
|
250
|
-
// actor may not be present when takedowns are eagerly applied during hydration.
|
|
251
|
-
// so it's important to _try_ to hydrate the actor for records checked this way.
|
|
252
|
-
return
|
|
253
|
-
}
|
|
254
|
-
return uri
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
viewerSeesNeedsReview(
|
|
258
|
-
{ did, uri }: { did?: DidString; uri?: AtUriString },
|
|
259
|
-
state: HydrationState,
|
|
260
|
-
): boolean {
|
|
261
|
-
const { labels, profileViewers, ctx } = state
|
|
262
|
-
did = did || (uri && uriToDid(uri))
|
|
263
|
-
if (!did) {
|
|
264
|
-
return true
|
|
265
|
-
}
|
|
266
|
-
if (
|
|
267
|
-
labels?.get(did)?.needsReview ||
|
|
268
|
-
(uri && labels?.get(uri)?.needsReview)
|
|
269
|
-
) {
|
|
270
|
-
// content marked as needs review
|
|
271
|
-
return ctx?.viewer === did || !!profileViewers?.get(did)?.following
|
|
272
|
-
}
|
|
273
|
-
return true
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
replyIsHiddenByThreadgate(
|
|
277
|
-
replyUri: AtUriString,
|
|
278
|
-
rootPostUri: AtUriString,
|
|
279
|
-
state: HydrationState,
|
|
280
|
-
) {
|
|
281
|
-
const threadgateUri = postUriToThreadgateUri(rootPostUri)
|
|
282
|
-
const threadgate = state.threadgates?.get(threadgateUri)
|
|
283
|
-
return !!threadgate?.record?.hiddenReplies?.includes(replyUri)
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
profileDetailed(
|
|
287
|
-
did: DidString,
|
|
288
|
-
state: HydrationState,
|
|
289
|
-
): Un$Typed<ProfileViewDetailed> | undefined {
|
|
290
|
-
const actor = state.actors?.get(did)
|
|
291
|
-
if (!actor) return
|
|
292
|
-
const baseView = this.profile(did, state)
|
|
293
|
-
if (!baseView) return
|
|
294
|
-
const knownFollowers = this.knownFollowers(did, state)
|
|
295
|
-
const profileAggs = state.profileAggs?.get(did)
|
|
296
|
-
|
|
297
|
-
return {
|
|
298
|
-
...baseView,
|
|
299
|
-
website: this.profileWebsite(did, state),
|
|
300
|
-
viewer: baseView.viewer
|
|
301
|
-
? {
|
|
302
|
-
...baseView.viewer,
|
|
303
|
-
knownFollowers,
|
|
304
|
-
}
|
|
305
|
-
: undefined,
|
|
306
|
-
banner: actor.profile?.banner
|
|
307
|
-
? this.imgUriBuilder.getPresetUri(
|
|
308
|
-
'banner',
|
|
309
|
-
did,
|
|
310
|
-
getBlobCidString(actor.profile.banner),
|
|
311
|
-
)
|
|
312
|
-
: undefined,
|
|
313
|
-
followersCount: profileAggs?.followers ?? 0,
|
|
314
|
-
followsCount: profileAggs?.follows ?? 0,
|
|
315
|
-
postsCount: profileAggs?.posts ?? 0,
|
|
316
|
-
associated: {
|
|
317
|
-
lists: profileAggs?.lists,
|
|
318
|
-
feedgens: profileAggs?.feeds,
|
|
319
|
-
starterPacks: profileAggs?.starterPacks,
|
|
320
|
-
labeler: actor.isLabeler,
|
|
321
|
-
chat: this.profileAssociatedChat(actor),
|
|
322
|
-
activitySubscription: this.profileAssociatedActivitySubscription(actor),
|
|
323
|
-
germ: actor.germ?.record.messageMe
|
|
324
|
-
? {
|
|
325
|
-
showButtonTo: actor.germ.record.messageMe.showButtonTo,
|
|
326
|
-
messageMeUrl: actor.germ.record.messageMe.messageMeUrl,
|
|
327
|
-
}
|
|
328
|
-
: undefined,
|
|
329
|
-
},
|
|
330
|
-
joinedViaStarterPack: actor.profile?.joinedViaStarterPack
|
|
331
|
-
? this.starterPackBasic(actor.profile.joinedViaStarterPack.uri, state)
|
|
332
|
-
: undefined,
|
|
333
|
-
pinnedPost: safePinnedPost(actor.profile?.pinnedPost),
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
profile(
|
|
337
|
-
did: DidString,
|
|
338
|
-
state: HydrationState,
|
|
339
|
-
): Un$Typed<ProfileView> | undefined {
|
|
340
|
-
const actor = state.actors?.get(did)
|
|
341
|
-
if (!actor) return
|
|
342
|
-
const basicView = this.profileBasic(did, state)
|
|
343
|
-
if (!basicView) return
|
|
344
|
-
return {
|
|
345
|
-
...basicView,
|
|
346
|
-
description: actor.profile?.description || undefined,
|
|
347
|
-
indexedAt:
|
|
348
|
-
actor.indexedAt && actor.sortedAt
|
|
349
|
-
? (this.indexedAt({
|
|
350
|
-
sortedAt: actor.sortedAt,
|
|
351
|
-
indexedAt: actor.indexedAt,
|
|
352
|
-
}).toISOString() as DatetimeString)
|
|
353
|
-
: undefined,
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
profileBasic(
|
|
358
|
-
did: DidString,
|
|
359
|
-
state: HydrationState,
|
|
360
|
-
): Un$Typed<ProfileViewBasic> | undefined {
|
|
361
|
-
const actor = state.actors?.get(did)
|
|
362
|
-
if (!actor) return
|
|
363
|
-
const profileUri = atUri(did, app.bsky.actor.profile)
|
|
364
|
-
const labels = [
|
|
365
|
-
...(state.labels?.getBySubject(did) ?? []),
|
|
366
|
-
...(state.labels?.getBySubject(profileUri) ?? []),
|
|
367
|
-
...this.selfLabels({
|
|
368
|
-
uri: profileUri,
|
|
369
|
-
cid: actor.profileCid?.toString(),
|
|
370
|
-
record: actor.profile,
|
|
371
|
-
}),
|
|
372
|
-
]
|
|
373
|
-
return {
|
|
374
|
-
did,
|
|
375
|
-
handle: actor.handle ?? INVALID_HANDLE,
|
|
376
|
-
displayName: actor.profile?.displayName,
|
|
377
|
-
pronouns: actor.profile?.pronouns,
|
|
378
|
-
avatar: actor.profile?.avatar
|
|
379
|
-
? this.imgUriBuilder.getPresetUri(
|
|
380
|
-
'avatar',
|
|
381
|
-
did,
|
|
382
|
-
getBlobCidString(actor.profile.avatar),
|
|
383
|
-
)
|
|
384
|
-
: undefined,
|
|
385
|
-
// associated.feedgens and associated.lists info not necessarily included
|
|
386
|
-
// on profile and profile-basic views, but should be on profile-detailed.
|
|
387
|
-
associated: {
|
|
388
|
-
labeler: actor.isLabeler ? true : undefined,
|
|
389
|
-
chat: this.profileAssociatedChat(actor),
|
|
390
|
-
activitySubscription: this.profileAssociatedActivitySubscription(actor),
|
|
391
|
-
germ: actor.germ?.record.messageMe
|
|
392
|
-
? {
|
|
393
|
-
showButtonTo: actor.germ.record.messageMe.showButtonTo,
|
|
394
|
-
messageMeUrl: actor.germ.record.messageMe.messageMeUrl,
|
|
395
|
-
}
|
|
396
|
-
: undefined,
|
|
397
|
-
},
|
|
398
|
-
viewer: this.profileViewer(did, state),
|
|
399
|
-
labels,
|
|
400
|
-
createdAt: actor.createdAt
|
|
401
|
-
? (actor.createdAt.toISOString() as DatetimeString)
|
|
402
|
-
: undefined,
|
|
403
|
-
verification: this.verification(did, state),
|
|
404
|
-
status: this.status(did, state),
|
|
405
|
-
debug: state.ctx?.includeDebugField ? actor.debug : undefined,
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
profileAssociatedChat(actor: Actor): ProfileAssociatedChat | undefined {
|
|
410
|
-
if (!actor.allowIncomingChatsFrom) return undefined
|
|
411
|
-
return {
|
|
412
|
-
allowIncoming: actor.allowIncomingChatsFrom,
|
|
413
|
-
allowGroupInvites: actor.allowGroupChatInvitesFrom,
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
profileAssociatedActivitySubscription(
|
|
418
|
-
actor: Actor,
|
|
419
|
-
): ProfileAssociatedActivitySubscription {
|
|
420
|
-
return { allowSubscriptions: actor.allowActivitySubscriptionsFrom }
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
profileKnownFollowers(
|
|
424
|
-
did: DidString,
|
|
425
|
-
state: HydrationState,
|
|
426
|
-
): ProfileView | undefined {
|
|
427
|
-
const actor = state.actors?.get(did)
|
|
428
|
-
if (!actor) return
|
|
429
|
-
const baseView = this.profile(did, state)
|
|
430
|
-
if (!baseView) return
|
|
431
|
-
const knownFollowers = this.knownFollowers(did, state)
|
|
432
|
-
return {
|
|
433
|
-
...baseView,
|
|
434
|
-
viewer: baseView.viewer
|
|
435
|
-
? {
|
|
436
|
-
...baseView.viewer,
|
|
437
|
-
knownFollowers,
|
|
438
|
-
}
|
|
439
|
-
: undefined,
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
profileViewer(
|
|
444
|
-
did: DidString,
|
|
445
|
-
state: HydrationState,
|
|
446
|
-
): ProfileViewer | undefined {
|
|
447
|
-
const viewer = state.profileViewers?.get(did)
|
|
448
|
-
if (!viewer) return
|
|
449
|
-
const blockedByList = this.blockedByList(viewer, state)
|
|
450
|
-
const blockedByUri = viewer.blockedBy || blockedByList
|
|
451
|
-
const blockingByList = this.blockingByList(viewer, state)
|
|
452
|
-
const blockingUri = viewer.blocking || blockingByList
|
|
453
|
-
const block = !!blockedByUri || !!blockingUri
|
|
454
|
-
const mutedByList = this.mutedByList(viewer, state)
|
|
455
|
-
return {
|
|
456
|
-
muted: !!(viewer.muted || mutedByList),
|
|
457
|
-
mutedByList: mutedByList ? this.listBasic(mutedByList, state) : undefined,
|
|
458
|
-
blockedBy: !!blockedByUri,
|
|
459
|
-
blocking: blockingUri,
|
|
460
|
-
blockingByList: blockingByList
|
|
461
|
-
? this.listBasic(blockingByList, state)
|
|
462
|
-
: undefined,
|
|
463
|
-
following: viewer.following && !block ? viewer.following : undefined,
|
|
464
|
-
followedBy: viewer.followedBy && !block ? viewer.followedBy : undefined,
|
|
465
|
-
activitySubscription: this.profileViewerActivitySubscription(
|
|
466
|
-
viewer,
|
|
467
|
-
did,
|
|
468
|
-
state,
|
|
469
|
-
),
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
profileViewerActivitySubscription(
|
|
474
|
-
profileViewer: ProfileViewerState,
|
|
475
|
-
did: DidString,
|
|
476
|
-
state: HydrationState,
|
|
477
|
-
): ActivitySubscription | undefined {
|
|
478
|
-
const actor = state.actors?.get(did)
|
|
479
|
-
if (!actor) return undefined
|
|
480
|
-
|
|
481
|
-
const activitySubscription = state.activitySubscriptions?.get(did)
|
|
482
|
-
if (!activitySubscription) return undefined
|
|
483
|
-
|
|
484
|
-
const allowFrom = actor.allowActivitySubscriptionsFrom
|
|
485
|
-
const actorFollowsViewer = !!profileViewer.followedBy
|
|
486
|
-
const viewerFollowsActor = !!profileViewer.following
|
|
487
|
-
if (
|
|
488
|
-
(allowFrom === 'followers' && viewerFollowsActor) ||
|
|
489
|
-
(allowFrom === 'mutuals' && actorFollowsViewer && viewerFollowsActor)
|
|
490
|
-
) {
|
|
491
|
-
return activitySubscription
|
|
492
|
-
}
|
|
493
|
-
return undefined
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
profileWebsite(did: DidString, state: HydrationState): UriString | undefined {
|
|
497
|
-
const actor = state.actors?.get(did)
|
|
498
|
-
if (!actor?.profile?.website) return
|
|
499
|
-
const { website } = actor.profile
|
|
500
|
-
|
|
501
|
-
// The record property accepts any URI, but we don't want
|
|
502
|
-
// to pass the client any schemes other than HTTPS.
|
|
503
|
-
return website.startsWith('https://') ? website : undefined
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
knownFollowers(
|
|
507
|
-
did: DidString,
|
|
508
|
-
state: HydrationState,
|
|
509
|
-
): KnownFollowers | undefined {
|
|
510
|
-
const knownFollowers = state.knownFollowers?.get(did)
|
|
511
|
-
if (!knownFollowers) return
|
|
512
|
-
const blocks = state.bidirectionalBlocks?.get(did)
|
|
513
|
-
const followers = mapDefined(knownFollowers.followers, (followerDid) => {
|
|
514
|
-
if (this.viewerBlockExists(followerDid, state)) {
|
|
515
|
-
return undefined
|
|
516
|
-
}
|
|
517
|
-
if (blocks?.get(followerDid)) {
|
|
518
|
-
return undefined
|
|
519
|
-
}
|
|
520
|
-
if (this.actorIsNoHosted(followerDid, state)) {
|
|
521
|
-
// @TODO only needed right now to work around getProfile's { includeTakedowns: true }
|
|
522
|
-
return undefined
|
|
523
|
-
}
|
|
524
|
-
return this.profileBasic(followerDid, state)
|
|
525
|
-
})
|
|
526
|
-
return { count: knownFollowers.count, followers }
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
verification(
|
|
530
|
-
did: DidString,
|
|
531
|
-
state: HydrationState,
|
|
532
|
-
): VerificationState | undefined {
|
|
533
|
-
const actor = state.actors?.get(did)
|
|
534
|
-
if (!actor) return
|
|
535
|
-
|
|
536
|
-
// Currently, the handle comes as "handle.invalid" from the production dataplane.
|
|
537
|
-
// But the contract allows for empty handle, so we cover both cases.
|
|
538
|
-
if (!actor.handle || actor.handle === INVALID_HANDLE) return
|
|
539
|
-
|
|
540
|
-
const isImpersonation = state.labels?.get(did)?.isImpersonation
|
|
541
|
-
|
|
542
|
-
const verifications = actor.verifications.map(
|
|
543
|
-
({ issuer, uri, displayName, handle, createdAt }): VerificationView => {
|
|
544
|
-
// @NOTE: We don't factor-in impersonation when evaluating the validity of each verification,
|
|
545
|
-
// only in the overall profile verification validity.
|
|
546
|
-
// The verification record's `displayName`/`handle` are the *subject's* snapshot at
|
|
547
|
-
// verification time; we compare them to the subject's current values to determine validity.
|
|
548
|
-
const isValid =
|
|
549
|
-
!!displayName &&
|
|
550
|
-
displayName === actor.profile?.displayName &&
|
|
551
|
-
!!handle &&
|
|
552
|
-
handle === actor.handle
|
|
553
|
-
|
|
554
|
-
// Expose the *issuer's* current handle/displayName, sourced from the
|
|
555
|
-
// issuer's hydrated actor record (see `Hydrator.hydrateProfiles`).
|
|
556
|
-
const issuerActor = state.actors?.get(issuer)
|
|
557
|
-
|
|
558
|
-
return {
|
|
559
|
-
issuer,
|
|
560
|
-
issuerDisplayName: issuerActor?.profile?.displayName,
|
|
561
|
-
issuerHandle: issuerActor?.handle,
|
|
562
|
-
uri,
|
|
563
|
-
isValid,
|
|
564
|
-
createdAt,
|
|
565
|
-
}
|
|
566
|
-
},
|
|
567
|
-
)
|
|
568
|
-
const hasValidVerification = verifications.some((v) => v.isValid)
|
|
569
|
-
|
|
570
|
-
const verifiedStatus = verifications.length
|
|
571
|
-
? hasValidVerification && !isImpersonation
|
|
572
|
-
? 'valid'
|
|
573
|
-
: 'invalid'
|
|
574
|
-
: 'none'
|
|
575
|
-
const trustedVerifierStatus = actor.trustedVerifier
|
|
576
|
-
? isImpersonation
|
|
577
|
-
? 'invalid'
|
|
578
|
-
: 'valid'
|
|
579
|
-
: 'none'
|
|
580
|
-
|
|
581
|
-
if (
|
|
582
|
-
verifications.length === 0 &&
|
|
583
|
-
verifiedStatus === 'none' &&
|
|
584
|
-
trustedVerifierStatus === 'none'
|
|
585
|
-
) {
|
|
586
|
-
return undefined
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
return {
|
|
590
|
-
verifications,
|
|
591
|
-
verifiedStatus,
|
|
592
|
-
trustedVerifierStatus,
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
status(did: DidString, state: HydrationState): StatusView | undefined {
|
|
597
|
-
const actor = state.actors?.get(did)
|
|
598
|
-
if (!actor?.status) return
|
|
599
|
-
|
|
600
|
-
const isViewerStatusOwner = did === state.ctx?.viewer
|
|
601
|
-
const { status } = actor
|
|
602
|
-
const { record, sortedAt, cid, takedownRef } = status
|
|
603
|
-
const isTakenDown = !!takedownRef
|
|
604
|
-
|
|
605
|
-
/*
|
|
606
|
-
* Manual filter for takendown status records. If this is ever removed, we
|
|
607
|
-
* need to reinstate `includeTakedowns` handling in the `Actor.getActors`
|
|
608
|
-
* hydrator.
|
|
609
|
-
*/
|
|
610
|
-
if (isTakenDown && !isViewerStatusOwner) {
|
|
611
|
-
return undefined
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
const uri = atUri(did, app.bsky.actor.status)
|
|
615
|
-
const labels = state.labels?.getBySubject(uri)
|
|
616
|
-
|
|
617
|
-
const minDuration = 5 * MINUTE
|
|
618
|
-
const maxDuration = 4 * HOUR
|
|
619
|
-
|
|
620
|
-
const expiresAtMs = record.durationMinutes
|
|
621
|
-
? sortedAt.getTime() +
|
|
622
|
-
Math.max(
|
|
623
|
-
Math.min(record.durationMinutes * MINUTE, maxDuration),
|
|
624
|
-
minDuration,
|
|
625
|
-
)
|
|
626
|
-
: undefined
|
|
627
|
-
const expiresAt = expiresAtMs
|
|
628
|
-
? (new Date(expiresAtMs).toISOString() as DatetimeString)
|
|
629
|
-
: undefined
|
|
630
|
-
|
|
631
|
-
const isActive = expiresAtMs ? expiresAtMs > Date.now() : undefined
|
|
632
|
-
|
|
633
|
-
const response: StatusView = {
|
|
634
|
-
uri,
|
|
635
|
-
cid,
|
|
636
|
-
record: record,
|
|
637
|
-
status: record.status,
|
|
638
|
-
embed:
|
|
639
|
-
record.embed && isExternalEmbedType(record.embed)
|
|
640
|
-
? this.externalEmbed(did, record.embed, state)
|
|
641
|
-
: undefined,
|
|
642
|
-
labels,
|
|
643
|
-
expiresAt,
|
|
644
|
-
isActive,
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
if (isViewerStatusOwner) {
|
|
648
|
-
response.isDisabled = isTakenDown
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
return response
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
blockedProfileViewer(
|
|
655
|
-
did: DidString,
|
|
656
|
-
state: HydrationState,
|
|
657
|
-
): ProfileViewer | undefined {
|
|
658
|
-
const viewer = state.profileViewers?.get(did)
|
|
659
|
-
if (!viewer) return
|
|
660
|
-
const blockedByUri = viewer.blockedBy || this.blockedByList(viewer, state)
|
|
661
|
-
const blockingUri = viewer.blocking || this.blockingByList(viewer, state)
|
|
662
|
-
return {
|
|
663
|
-
blockedBy: !!blockedByUri,
|
|
664
|
-
blocking: blockingUri,
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// Graph
|
|
669
|
-
// ------------
|
|
670
|
-
|
|
671
|
-
list(
|
|
672
|
-
uri: AtUriString,
|
|
673
|
-
state: HydrationState,
|
|
674
|
-
): Un$Typed<ListView> | undefined {
|
|
675
|
-
const creatorDid = creatorFromUri(uri)
|
|
676
|
-
const list = state.lists?.get(uri)
|
|
677
|
-
if (!list) return
|
|
678
|
-
const creator = this.profile(creatorDid, state)
|
|
679
|
-
if (!creator) return
|
|
680
|
-
const basicView = this.listBasic(uri, state)
|
|
681
|
-
if (!basicView) return
|
|
682
|
-
|
|
683
|
-
return {
|
|
684
|
-
...basicView,
|
|
685
|
-
creator,
|
|
686
|
-
description: list.record.description,
|
|
687
|
-
descriptionFacets: list.record.descriptionFacets,
|
|
688
|
-
indexedAt: this.indexedAt(list).toISOString() as DatetimeString,
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
listBasic(
|
|
693
|
-
uri: AtUriString,
|
|
694
|
-
state: HydrationState,
|
|
695
|
-
): Un$Typed<ListViewBasic> | undefined {
|
|
696
|
-
const list = state.lists?.get(uri)
|
|
697
|
-
if (!list) {
|
|
698
|
-
return undefined
|
|
699
|
-
}
|
|
700
|
-
const listAgg = state.listAggs?.get(uri)
|
|
701
|
-
const listViewer = state.listViewers?.get(uri)
|
|
702
|
-
const labels = state.labels?.getBySubject(uri) ?? []
|
|
703
|
-
const creator = creatorFromUri(uri)
|
|
704
|
-
return {
|
|
705
|
-
uri,
|
|
706
|
-
cid: list.cid,
|
|
707
|
-
name: list.record.name,
|
|
708
|
-
purpose: list.record.purpose,
|
|
709
|
-
avatar: list.record.avatar
|
|
710
|
-
? this.imgUriBuilder.getPresetUri(
|
|
711
|
-
'avatar',
|
|
712
|
-
creator,
|
|
713
|
-
getBlobCidString(list.record.avatar),
|
|
714
|
-
)
|
|
715
|
-
: undefined,
|
|
716
|
-
listItemCount: listAgg?.listItems ?? 0,
|
|
717
|
-
indexedAt: this.indexedAt(list).toISOString() as DatetimeString,
|
|
718
|
-
labels,
|
|
719
|
-
viewer: listViewer
|
|
720
|
-
? {
|
|
721
|
-
muted: !!listViewer.viewerMuted,
|
|
722
|
-
blocked: listViewer.viewerListBlockUri,
|
|
723
|
-
}
|
|
724
|
-
: undefined,
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
listItemView(
|
|
729
|
-
uri: AtUriString,
|
|
730
|
-
did: DidString,
|
|
731
|
-
state: HydrationState,
|
|
732
|
-
): Un$Typed<ListItemView> | undefined {
|
|
733
|
-
const subject = this.profile(did, state)
|
|
734
|
-
if (!subject) return
|
|
735
|
-
return { uri, subject }
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
starterPackBasic(
|
|
739
|
-
uri: AtUriString,
|
|
740
|
-
state: HydrationState,
|
|
741
|
-
): Un$Typed<StarterPackViewBasic> | undefined {
|
|
742
|
-
const sp = state.starterPacks?.get(uri)
|
|
743
|
-
if (!sp) return
|
|
744
|
-
const parsedUri = new AtUri(uri)
|
|
745
|
-
const creator = this.profileBasic(parsedUri.did, state)
|
|
746
|
-
if (!creator) return
|
|
747
|
-
const agg = state.starterPackAggs?.get(uri)
|
|
748
|
-
const labels = state.labels?.getBySubject(uri) ?? []
|
|
749
|
-
return {
|
|
750
|
-
uri,
|
|
751
|
-
cid: sp.cid,
|
|
752
|
-
record: sp.record,
|
|
753
|
-
creator,
|
|
754
|
-
joinedAllTimeCount: agg?.joinedAllTime ?? 0,
|
|
755
|
-
joinedWeekCount: agg?.joinedWeek ?? 0,
|
|
756
|
-
labels,
|
|
757
|
-
indexedAt: this.indexedAt(sp).toISOString() as DatetimeString,
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
starterPack(
|
|
762
|
-
uri: AtUriString,
|
|
763
|
-
state: HydrationState,
|
|
764
|
-
): Un$Typed<StarterPackView> | undefined {
|
|
765
|
-
const sp = state.starterPacks?.get(uri)
|
|
766
|
-
const basicView = this.starterPackBasic(uri, state)
|
|
767
|
-
if (!sp || !basicView) return
|
|
768
|
-
const agg = state.starterPackAggs?.get(uri)
|
|
769
|
-
const feeds = mapDefined(sp.record.feeds ?? [], (feed) =>
|
|
770
|
-
this.feedGenerator(feed.uri, state),
|
|
771
|
-
)
|
|
772
|
-
const list = this.listBasic(sp.record.list, state)
|
|
773
|
-
const listItemsSample = mapDefined(agg?.listItemSampleUris ?? [], (uri) => {
|
|
774
|
-
const li = state.listItems?.get(uri)
|
|
775
|
-
if (!li) return
|
|
776
|
-
const subject = this.profile(li.record.subject, state)
|
|
777
|
-
if (!subject) return
|
|
778
|
-
return { uri, subject }
|
|
779
|
-
})
|
|
780
|
-
return {
|
|
781
|
-
...basicView,
|
|
782
|
-
feeds,
|
|
783
|
-
list,
|
|
784
|
-
listItemsSample,
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
// Labels
|
|
789
|
-
// ------------
|
|
790
|
-
|
|
791
|
-
selfLabels({
|
|
792
|
-
uri,
|
|
793
|
-
cid,
|
|
794
|
-
record,
|
|
795
|
-
}: {
|
|
796
|
-
uri?: AtUriString
|
|
797
|
-
cid?: string
|
|
798
|
-
record?:
|
|
799
|
-
| PostRecord
|
|
800
|
-
| LikeRecord
|
|
801
|
-
| RepostRecord
|
|
802
|
-
| FollowRecord
|
|
803
|
-
| ProfileRecord
|
|
804
|
-
| LabelerRecord
|
|
805
|
-
| VerificationRecord
|
|
806
|
-
| NotificationRecordDeleted
|
|
807
|
-
}): Label[] {
|
|
808
|
-
if (!uri || !cid || !record) return []
|
|
809
|
-
|
|
810
|
-
// Only these have a "labels" property:
|
|
811
|
-
if (
|
|
812
|
-
!isPostRecordType(record) &&
|
|
813
|
-
!isProfileRecordType(record) &&
|
|
814
|
-
!isLabelerRecordType(record)
|
|
815
|
-
) {
|
|
816
|
-
return []
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
// Ignore if no labels defines
|
|
820
|
-
if (
|
|
821
|
-
!record.labels ||
|
|
822
|
-
!isSelfLabelsType(record.labels) ||
|
|
823
|
-
!record.labels.values.length
|
|
824
|
-
) {
|
|
825
|
-
return []
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
const src = creatorFromUri(uri) // record creator
|
|
829
|
-
const cts =
|
|
830
|
-
typeof record.createdAt === 'string'
|
|
831
|
-
? normalizeDatetimeAlways(record.createdAt)
|
|
832
|
-
: (new Date(0).toISOString() as DatetimeString)
|
|
833
|
-
return record.labels.values.map(({ val }) => {
|
|
834
|
-
return { src, uri, cid, val, cts }
|
|
835
|
-
})
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
labeler(
|
|
839
|
-
did: DidString,
|
|
840
|
-
state: HydrationState,
|
|
841
|
-
): Un$Typed<LabelerView> | undefined {
|
|
842
|
-
const labeler = state.labelers?.get(did)
|
|
843
|
-
if (!labeler) return
|
|
844
|
-
const creator = this.profile(did, state)
|
|
845
|
-
if (!creator) return
|
|
846
|
-
const viewer = state.labelerViewers?.get(did)
|
|
847
|
-
const aggs = state.labelerAggs?.get(did)
|
|
848
|
-
|
|
849
|
-
const uri = atUri(did, app.bsky.labeler.service)
|
|
850
|
-
const labels = [
|
|
851
|
-
...(state.labels?.getBySubject(uri) ?? []),
|
|
852
|
-
...this.selfLabels({
|
|
853
|
-
uri,
|
|
854
|
-
cid: labeler.cid.toString(),
|
|
855
|
-
record: labeler.record,
|
|
856
|
-
}),
|
|
857
|
-
]
|
|
858
|
-
|
|
859
|
-
return {
|
|
860
|
-
uri,
|
|
861
|
-
cid: labeler.cid.toString(),
|
|
862
|
-
creator,
|
|
863
|
-
likeCount: aggs?.likes ?? 0,
|
|
864
|
-
viewer: viewer
|
|
865
|
-
? {
|
|
866
|
-
like: viewer.like,
|
|
867
|
-
}
|
|
868
|
-
: undefined,
|
|
869
|
-
indexedAt: this.indexedAt(labeler).toISOString() as DatetimeString,
|
|
870
|
-
labels,
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
labelerDetailed(
|
|
875
|
-
did: DidString,
|
|
876
|
-
state: HydrationState,
|
|
877
|
-
): Un$Typed<LabelerViewDetailed> | undefined {
|
|
878
|
-
const baseView = this.labeler(did, state)
|
|
879
|
-
if (!baseView) return
|
|
880
|
-
const labeler = state.labelers?.get(did)
|
|
881
|
-
if (!labeler) return
|
|
882
|
-
|
|
883
|
-
return {
|
|
884
|
-
...baseView,
|
|
885
|
-
policies: labeler.record.policies,
|
|
886
|
-
reasonTypes: labeler.record.reasonTypes,
|
|
887
|
-
subjectTypes: labeler.record.subjectTypes,
|
|
888
|
-
subjectCollections: labeler.record.subjectCollections,
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
// Feed
|
|
893
|
-
// ------------
|
|
894
|
-
|
|
895
|
-
feedItemBlocksAndMutes(
|
|
896
|
-
item: FeedItem,
|
|
897
|
-
state: HydrationState,
|
|
898
|
-
): {
|
|
899
|
-
originatorMuted: boolean
|
|
900
|
-
originatorBlocked: boolean
|
|
901
|
-
authorMuted: boolean
|
|
902
|
-
authorBlocked: boolean
|
|
903
|
-
ancestorAuthorBlocked: boolean
|
|
904
|
-
} {
|
|
905
|
-
const authorDid = creatorFromUri(item.post.uri)
|
|
906
|
-
const originatorDid = item.repost
|
|
907
|
-
? creatorFromUri(item.repost.uri)
|
|
908
|
-
: authorDid
|
|
909
|
-
const post = state.posts?.get(item.post.uri)
|
|
910
|
-
const parentUri = post?.record.reply?.parent.uri
|
|
911
|
-
const parentAuthorDid = parentUri && creatorFromUri(parentUri)
|
|
912
|
-
const parent = parentUri ? state.posts?.get(parentUri) : undefined
|
|
913
|
-
const grandparentUri = parent?.record.reply?.parent.uri
|
|
914
|
-
const grandparentAuthorDid =
|
|
915
|
-
grandparentUri && creatorFromUri(grandparentUri)
|
|
916
|
-
return {
|
|
917
|
-
originatorMuted: this.viewerMuteExists(originatorDid, state),
|
|
918
|
-
originatorBlocked: this.viewerBlockExists(originatorDid, state),
|
|
919
|
-
authorMuted: this.viewerMuteExists(authorDid, state),
|
|
920
|
-
authorBlocked: this.viewerBlockExists(authorDid, state),
|
|
921
|
-
ancestorAuthorBlocked:
|
|
922
|
-
(!!parentAuthorDid && this.viewerBlockExists(parentAuthorDid, state)) ||
|
|
923
|
-
(!!grandparentAuthorDid &&
|
|
924
|
-
this.viewerBlockExists(grandparentAuthorDid, state)),
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
feedGenerator(
|
|
929
|
-
uri: AtUriString,
|
|
930
|
-
state: HydrationState,
|
|
931
|
-
): Un$Typed<GeneratorView> | undefined {
|
|
932
|
-
const feedgen = state.feedgens?.get(uri)
|
|
933
|
-
if (!feedgen) return
|
|
934
|
-
const creatorDid = creatorFromUri(uri)
|
|
935
|
-
const creator = this.profile(creatorDid, state)
|
|
936
|
-
if (!creator) return
|
|
937
|
-
const viewer = state.feedgenViewers?.get(uri)
|
|
938
|
-
const aggs = state.feedgenAggs?.get(uri)
|
|
939
|
-
const labels = state.labels?.getBySubject(uri) ?? []
|
|
940
|
-
|
|
941
|
-
return {
|
|
942
|
-
uri,
|
|
943
|
-
cid: feedgen.cid,
|
|
944
|
-
did: feedgen.record.did,
|
|
945
|
-
creator,
|
|
946
|
-
displayName: feedgen.record.displayName,
|
|
947
|
-
description: feedgen.record.description,
|
|
948
|
-
descriptionFacets: feedgen.record.descriptionFacets,
|
|
949
|
-
avatar: feedgen.record.avatar
|
|
950
|
-
? this.imgUriBuilder.getPresetUri(
|
|
951
|
-
'avatar',
|
|
952
|
-
creatorDid,
|
|
953
|
-
getBlobCidString(feedgen.record.avatar),
|
|
954
|
-
)
|
|
955
|
-
: undefined,
|
|
956
|
-
likeCount: aggs?.likes ?? 0,
|
|
957
|
-
acceptsInteractions: feedgen.record.acceptsInteractions,
|
|
958
|
-
labels,
|
|
959
|
-
viewer: viewer
|
|
960
|
-
? {
|
|
961
|
-
like: viewer.like,
|
|
962
|
-
}
|
|
963
|
-
: undefined,
|
|
964
|
-
contentMode: feedgen.record.contentMode,
|
|
965
|
-
indexedAt: this.indexedAt(feedgen).toISOString() as DatetimeString,
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
threadgate(
|
|
970
|
-
uri: AtUriString,
|
|
971
|
-
state: HydrationState,
|
|
972
|
-
): Un$Typed<ThreadgateView> | undefined {
|
|
973
|
-
const gate = state.threadgates?.get(uri)
|
|
974
|
-
if (!gate) return
|
|
975
|
-
return {
|
|
976
|
-
uri,
|
|
977
|
-
cid: gate.cid,
|
|
978
|
-
record: gate.record,
|
|
979
|
-
lists: mapDefined(gate.record.allow ?? [], (rule) => {
|
|
980
|
-
if (!isListRuleType(rule)) return
|
|
981
|
-
return this.listBasic(rule.list, state)
|
|
982
|
-
}),
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
post(
|
|
987
|
-
uri: AtUriString,
|
|
988
|
-
state: HydrationState,
|
|
989
|
-
depth = 0,
|
|
990
|
-
): Un$Typed<PostView> | undefined {
|
|
991
|
-
const post = state.posts?.get(uri)
|
|
992
|
-
if (!post) return
|
|
993
|
-
const parsedUri = new AtUri(uri)
|
|
994
|
-
const authorDid = parsedUri.did
|
|
995
|
-
const author = this.profileBasic(authorDid, state)
|
|
996
|
-
if (!author) return
|
|
997
|
-
const aggs = state.postAggs?.get(uri)
|
|
998
|
-
const viewer = state.postViewers?.get(uri)
|
|
999
|
-
const threadgateUri = postUriToThreadgateUri(uri)
|
|
1000
|
-
const labels = [
|
|
1001
|
-
...(state.labels?.getBySubject(uri) ?? []),
|
|
1002
|
-
...this.selfLabels({
|
|
1003
|
-
uri,
|
|
1004
|
-
cid: post.cid,
|
|
1005
|
-
record: post.record,
|
|
1006
|
-
}),
|
|
1007
|
-
]
|
|
1008
|
-
return {
|
|
1009
|
-
uri,
|
|
1010
|
-
cid: post.cid,
|
|
1011
|
-
author,
|
|
1012
|
-
record: post.record,
|
|
1013
|
-
embed:
|
|
1014
|
-
depth < 2 && post.record.embed
|
|
1015
|
-
? this.embed(uri, post.record.embed, state, depth + 1)
|
|
1016
|
-
: undefined,
|
|
1017
|
-
bookmarkCount: aggs?.bookmarks ?? 0,
|
|
1018
|
-
replyCount: aggs?.replies ?? 0,
|
|
1019
|
-
repostCount: aggs?.reposts ?? 0,
|
|
1020
|
-
likeCount: aggs?.likes ?? 0,
|
|
1021
|
-
quoteCount: aggs?.quotes ?? 0,
|
|
1022
|
-
indexedAt: this.indexedAt(post).toISOString() as DatetimeString,
|
|
1023
|
-
viewer: viewer
|
|
1024
|
-
? {
|
|
1025
|
-
repost: viewer.repost,
|
|
1026
|
-
like: viewer.like,
|
|
1027
|
-
bookmarked: viewer.bookmarked,
|
|
1028
|
-
threadMuted: viewer.threadMuted,
|
|
1029
|
-
replyDisabled: this.userReplyDisabled(uri, state),
|
|
1030
|
-
embeddingDisabled: this.userPostEmbeddingDisabled(uri, state),
|
|
1031
|
-
pinned: this.viewerPinned(uri, state, authorDid),
|
|
1032
|
-
}
|
|
1033
|
-
: undefined,
|
|
1034
|
-
labels,
|
|
1035
|
-
threadgate: !post.record.reply // only hydrate gate on root post
|
|
1036
|
-
? this.threadgate(threadgateUri, state)
|
|
1037
|
-
: undefined,
|
|
1038
|
-
debug: state.ctx?.includeDebugField
|
|
1039
|
-
? { post: post.debug, author: author.debug }
|
|
1040
|
-
: undefined,
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
feedViewPost(
|
|
1045
|
-
item: FeedItem,
|
|
1046
|
-
state: HydrationState,
|
|
1047
|
-
): Un$Typed<FeedViewPost> | undefined {
|
|
1048
|
-
const postInfo = state.posts?.get(item.post.uri)
|
|
1049
|
-
let reason: $Typed<ReasonRepost> | $Typed<ReasonPin> | undefined
|
|
1050
|
-
if (item.authorPinned) {
|
|
1051
|
-
reason = this.reasonPin()
|
|
1052
|
-
} else if (item.repost) {
|
|
1053
|
-
const repost = state.reposts?.get(item.repost.uri)
|
|
1054
|
-
if (!repost) return
|
|
1055
|
-
if (repost.record.subject.uri !== item.post.uri) return
|
|
1056
|
-
reason = this.reasonRepost(item.repost.uri, repost, state)
|
|
1057
|
-
if (!reason) return
|
|
1058
|
-
}
|
|
1059
|
-
const post = this.post(item.post.uri, state)
|
|
1060
|
-
if (!post) return
|
|
1061
|
-
const reply = !postInfo?.violatesThreadGate
|
|
1062
|
-
? this.replyRef(item.post.uri, state)
|
|
1063
|
-
: undefined
|
|
1064
|
-
return {
|
|
1065
|
-
post,
|
|
1066
|
-
reason,
|
|
1067
|
-
reply,
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
replyRef(
|
|
1072
|
-
uri: AtUriString,
|
|
1073
|
-
state: HydrationState,
|
|
1074
|
-
): Un$Typed<ReplyRef> | undefined {
|
|
1075
|
-
const postRecord = state.posts?.get(uri)?.record
|
|
1076
|
-
if (!postRecord?.reply) return
|
|
1077
|
-
let root = this.maybePost(postRecord.reply.root.uri, state)
|
|
1078
|
-
let parent = this.maybePost(postRecord.reply.parent.uri, state)
|
|
1079
|
-
if (!state.ctx?.include3pBlocks) {
|
|
1080
|
-
const childBlocks = state.postBlocks?.get(uri)
|
|
1081
|
-
const parentBlocks = state.postBlocks?.get(parent.uri)
|
|
1082
|
-
// if child blocks parent, block parent
|
|
1083
|
-
if (isPostViewType(parent) && childBlocks?.parent) {
|
|
1084
|
-
parent = this.blockedPost(parent.uri, parent.author.did, state)
|
|
1085
|
-
}
|
|
1086
|
-
// if child or parent blocks root, block root
|
|
1087
|
-
if (isPostViewType(root) && (childBlocks?.root || parentBlocks?.root)) {
|
|
1088
|
-
root = this.blockedPost(root.uri, root.author.did, state)
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
let grandparentAuthor: ProfileViewBasic | undefined
|
|
1092
|
-
if (isPostViewType(parent)) {
|
|
1093
|
-
// @NOTE The "parent.record" property is of type "unknown" in the lexicon
|
|
1094
|
-
// schema, which means that it is typed as LexMap here. In order to avoid
|
|
1095
|
-
// (expensive) validation using "isPostRecord(parent.record)", we only
|
|
1096
|
-
// check that the "$type" property is a post record type, then use a
|
|
1097
|
-
// try/catch to "validate" the post uri.
|
|
1098
|
-
if (isPostRecordType(parent.record) && parent.record.reply != null) {
|
|
1099
|
-
const uri = (parent.record.reply as any).parent?.uri
|
|
1100
|
-
if (typeof uri === 'string') {
|
|
1101
|
-
try {
|
|
1102
|
-
grandparentAuthor = this.profileBasic(creatorFromUri(uri), state)
|
|
1103
|
-
} catch {
|
|
1104
|
-
// ignore (just as if validation had failed)
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
return {
|
|
1110
|
-
root,
|
|
1111
|
-
parent,
|
|
1112
|
-
grandparentAuthor,
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
maybePost(uri: AtUriString, state: HydrationState): $Typed<MaybePostView> {
|
|
1117
|
-
const post = this.post(uri, state)
|
|
1118
|
-
if (!post) {
|
|
1119
|
-
return this.notFoundPost(uri)
|
|
1120
|
-
}
|
|
1121
|
-
if (this.viewerBlockExists(post.author.did, state)) {
|
|
1122
|
-
return this.blockedPost(uri, post.author.did, state)
|
|
1123
|
-
}
|
|
1124
|
-
return app.bsky.feed.defs.postView.$build(post)
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
blockedPost(
|
|
1128
|
-
uri: AtUriString,
|
|
1129
|
-
authorDid: DidString,
|
|
1130
|
-
state: HydrationState,
|
|
1131
|
-
): $Typed<BlockedPost> {
|
|
1132
|
-
return app.bsky.feed.defs.blockedPost.$build({
|
|
1133
|
-
uri,
|
|
1134
|
-
blocked: true,
|
|
1135
|
-
author: {
|
|
1136
|
-
did: authorDid,
|
|
1137
|
-
viewer: this.blockedProfileViewer(authorDid, state),
|
|
1138
|
-
},
|
|
1139
|
-
})
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
notFoundPost(uri: AtUriString): $Typed<NotFoundPost> {
|
|
1143
|
-
return app.bsky.feed.defs.notFoundPost.$build({
|
|
1144
|
-
uri,
|
|
1145
|
-
notFound: true,
|
|
1146
|
-
})
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
reasonRepost(
|
|
1150
|
-
uri: AtUriString,
|
|
1151
|
-
repost: Repost,
|
|
1152
|
-
state: HydrationState,
|
|
1153
|
-
): $Typed<ReasonRepost> | undefined {
|
|
1154
|
-
const creatorDid = creatorFromUri(uri)
|
|
1155
|
-
const creator = this.profileBasic(creatorDid, state)
|
|
1156
|
-
if (!creator) return
|
|
1157
|
-
return app.bsky.feed.defs.reasonRepost.$build({
|
|
1158
|
-
by: creator,
|
|
1159
|
-
uri,
|
|
1160
|
-
cid: repost.cid,
|
|
1161
|
-
indexedAt: this.indexedAt(repost).toISOString() as DatetimeString,
|
|
1162
|
-
})
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
reasonPin(): $Typed<ReasonPin> {
|
|
1166
|
-
return app.bsky.feed.defs.reasonPin.$build({})
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
// Bookmarks
|
|
1170
|
-
// ------------
|
|
1171
|
-
bookmark(
|
|
1172
|
-
key: string,
|
|
1173
|
-
state: HydrationState,
|
|
1174
|
-
): Un$Typed<BookmarkView> | undefined {
|
|
1175
|
-
const viewer = state.ctx?.viewer
|
|
1176
|
-
if (!viewer) return
|
|
1177
|
-
|
|
1178
|
-
const bookmark = state.bookmarks?.get(viewer)?.get(key)
|
|
1179
|
-
if (!bookmark) return
|
|
1180
|
-
|
|
1181
|
-
const atUri = new AtUri(bookmark.subjectUri)
|
|
1182
|
-
if (atUri.collection !== app.bsky.feed.post.$type) return
|
|
1183
|
-
|
|
1184
|
-
const item = this.maybePost(atUri.href, state)
|
|
1185
|
-
return {
|
|
1186
|
-
createdAt: bookmark.indexedAt
|
|
1187
|
-
? (bookmark.indexedAt.toISOString() as DatetimeString)
|
|
1188
|
-
: undefined,
|
|
1189
|
-
subject: {
|
|
1190
|
-
uri: atUri.href,
|
|
1191
|
-
cid: bookmark.subjectCid,
|
|
1192
|
-
},
|
|
1193
|
-
item,
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
// Threads
|
|
1198
|
-
// ------------
|
|
1199
|
-
|
|
1200
|
-
thread(
|
|
1201
|
-
skele: { anchor: AtUriString; uris: AtUriString[] },
|
|
1202
|
-
state: HydrationState,
|
|
1203
|
-
opts: { height: number; depth: number },
|
|
1204
|
-
): $Typed<ThreadViewPost> | $Typed<NotFoundPost> | $Typed<BlockedPost> {
|
|
1205
|
-
const { anchor, uris } = skele
|
|
1206
|
-
const post = this.post(anchor, state)
|
|
1207
|
-
const postInfo = state.posts?.get(anchor)
|
|
1208
|
-
if (!postInfo || !post) return this.notFoundPost(anchor)
|
|
1209
|
-
if (this.viewerBlockExists(post.author.did, state)) {
|
|
1210
|
-
return this.blockedPost(anchor, post.author.did, state)
|
|
1211
|
-
}
|
|
1212
|
-
const includedPosts = new Set<string>([anchor])
|
|
1213
|
-
const childrenByParentUri: Record<AtUriString, AtUriString[]> = {}
|
|
1214
|
-
uris.forEach((uri) => {
|
|
1215
|
-
const post = state.posts?.get(uri)
|
|
1216
|
-
const parentUri = post?.record.reply?.parent.uri
|
|
1217
|
-
if (!parentUri) return
|
|
1218
|
-
if (includedPosts.has(uri)) return
|
|
1219
|
-
includedPosts.add(uri)
|
|
1220
|
-
childrenByParentUri[parentUri] ??= []
|
|
1221
|
-
childrenByParentUri[parentUri].push(uri)
|
|
1222
|
-
})
|
|
1223
|
-
const rootUri = getRootUri(anchor, postInfo)
|
|
1224
|
-
const violatesThreadGate = postInfo.violatesThreadGate
|
|
1225
|
-
|
|
1226
|
-
return app.bsky.feed.defs.threadViewPost.$build({
|
|
1227
|
-
post,
|
|
1228
|
-
parent: !violatesThreadGate
|
|
1229
|
-
? this.threadParent(anchor, rootUri, state, opts.height)
|
|
1230
|
-
: undefined,
|
|
1231
|
-
replies: !violatesThreadGate
|
|
1232
|
-
? this.threadReplies(
|
|
1233
|
-
anchor,
|
|
1234
|
-
rootUri,
|
|
1235
|
-
childrenByParentUri,
|
|
1236
|
-
state,
|
|
1237
|
-
opts.depth,
|
|
1238
|
-
)
|
|
1239
|
-
: undefined,
|
|
1240
|
-
threadContext: {
|
|
1241
|
-
rootAuthorLike: state.threadContexts?.get(post.uri)?.like,
|
|
1242
|
-
},
|
|
1243
|
-
})
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
threadParent(
|
|
1247
|
-
childUri: AtUriString,
|
|
1248
|
-
rootUri: string,
|
|
1249
|
-
state: HydrationState,
|
|
1250
|
-
height: number,
|
|
1251
|
-
):
|
|
1252
|
-
| $Typed<ThreadViewPost>
|
|
1253
|
-
| $Typed<NotFoundPost>
|
|
1254
|
-
| $Typed<BlockedPost>
|
|
1255
|
-
| undefined {
|
|
1256
|
-
if (height < 1) return undefined
|
|
1257
|
-
const parentUri = state.posts?.get(childUri)?.record.reply?.parent.uri
|
|
1258
|
-
if (!parentUri) return undefined
|
|
1259
|
-
if (
|
|
1260
|
-
!state.ctx?.include3pBlocks &&
|
|
1261
|
-
state.postBlocks?.get(childUri)?.parent
|
|
1262
|
-
) {
|
|
1263
|
-
return this.blockedPost(parentUri, creatorFromUri(parentUri), state)
|
|
1264
|
-
}
|
|
1265
|
-
const post = this.post(parentUri, state)
|
|
1266
|
-
const postInfo = state.posts?.get(parentUri)
|
|
1267
|
-
if (!postInfo || !post) return this.notFoundPost(parentUri)
|
|
1268
|
-
if (rootUri !== getRootUri(parentUri, postInfo)) return // outside thread boundary
|
|
1269
|
-
if (this.viewerBlockExists(post.author.did, state)) {
|
|
1270
|
-
return this.blockedPost(parentUri, post.author.did, state)
|
|
1271
|
-
}
|
|
1272
|
-
return app.bsky.feed.defs.threadViewPost.$build({
|
|
1273
|
-
post,
|
|
1274
|
-
parent: this.threadParent(parentUri, rootUri, state, height - 1),
|
|
1275
|
-
threadContext: {
|
|
1276
|
-
rootAuthorLike: state.threadContexts?.get(post.uri)?.like,
|
|
1277
|
-
},
|
|
1278
|
-
})
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
threadReplies(
|
|
1282
|
-
parentUri: AtUriString,
|
|
1283
|
-
rootUri: AtUriString,
|
|
1284
|
-
childrenByParentUri: Record<AtUriString, AtUriString[]>,
|
|
1285
|
-
state: HydrationState,
|
|
1286
|
-
depth: number,
|
|
1287
|
-
): ($Typed<ThreadViewPost> | $Typed<BlockedPost>)[] | undefined {
|
|
1288
|
-
if (depth < 1) return undefined
|
|
1289
|
-
const childrenUris = childrenByParentUri[parentUri] ?? []
|
|
1290
|
-
return mapDefined(childrenUris, (uri) => {
|
|
1291
|
-
const postInfo = state.posts?.get(uri)
|
|
1292
|
-
if (postInfo?.violatesThreadGate) {
|
|
1293
|
-
return undefined
|
|
1294
|
-
}
|
|
1295
|
-
if (!state.ctx?.include3pBlocks && state.postBlocks?.get(uri)?.parent) {
|
|
1296
|
-
return undefined
|
|
1297
|
-
}
|
|
1298
|
-
const post = this.post(uri, state)
|
|
1299
|
-
if (!postInfo || !post) {
|
|
1300
|
-
// in the future we might consider keeping a placeholder for deleted
|
|
1301
|
-
// posts that have replies under them, but not supported at the moment.
|
|
1302
|
-
// this case is mostly likely hit when a takedown was applied to a post.
|
|
1303
|
-
return undefined
|
|
1304
|
-
}
|
|
1305
|
-
if (rootUri !== getRootUri(uri, postInfo)) return // outside thread boundary
|
|
1306
|
-
if (this.viewerBlockExists(post.author.did, state)) {
|
|
1307
|
-
return this.blockedPost(uri, post.author.did, state)
|
|
1308
|
-
}
|
|
1309
|
-
if (!this.viewerSeesNeedsReview({ uri, did: post.author.did }, state)) {
|
|
1310
|
-
return undefined
|
|
1311
|
-
}
|
|
1312
|
-
return app.bsky.feed.defs.threadViewPost.$build({
|
|
1313
|
-
post,
|
|
1314
|
-
replies: this.threadReplies(
|
|
1315
|
-
uri,
|
|
1316
|
-
rootUri,
|
|
1317
|
-
childrenByParentUri,
|
|
1318
|
-
state,
|
|
1319
|
-
depth - 1,
|
|
1320
|
-
),
|
|
1321
|
-
threadContext: {
|
|
1322
|
-
rootAuthorLike: state.threadContexts?.get(post.uri)?.like,
|
|
1323
|
-
},
|
|
1324
|
-
})
|
|
1325
|
-
})
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
// Threads V2
|
|
1329
|
-
// ------------
|
|
1330
|
-
|
|
1331
|
-
threadV2(
|
|
1332
|
-
skeleton: { anchor: AtUriString; uris: AtUriString[] },
|
|
1333
|
-
state: HydrationState,
|
|
1334
|
-
{
|
|
1335
|
-
above,
|
|
1336
|
-
below,
|
|
1337
|
-
branchingFactor,
|
|
1338
|
-
sort,
|
|
1339
|
-
}: {
|
|
1340
|
-
above: number
|
|
1341
|
-
below: number
|
|
1342
|
-
branchingFactor: number
|
|
1343
|
-
sort: GetPostThreadV2QueryParams['sort']
|
|
1344
|
-
},
|
|
1345
|
-
): { hasOtherReplies: boolean; thread: ThreadItem[] } {
|
|
1346
|
-
const { anchor: anchorUri, uris } = skeleton
|
|
1347
|
-
|
|
1348
|
-
// Not found.
|
|
1349
|
-
const postView = this.post(anchorUri, state)
|
|
1350
|
-
const post = state.posts?.get(anchorUri)
|
|
1351
|
-
if (!post || !postView) {
|
|
1352
|
-
return {
|
|
1353
|
-
hasOtherReplies: false,
|
|
1354
|
-
thread: [
|
|
1355
|
-
this.threadV2ItemNotFound({
|
|
1356
|
-
uri: anchorUri,
|
|
1357
|
-
depth: 0,
|
|
1358
|
-
}),
|
|
1359
|
-
],
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
// Blocked (only 1p for anchor).
|
|
1364
|
-
if (this.viewerBlockExists(postView.author.did, state)) {
|
|
1365
|
-
return {
|
|
1366
|
-
hasOtherReplies: false,
|
|
1367
|
-
thread: [
|
|
1368
|
-
this.threadV2ItemBlocked({
|
|
1369
|
-
uri: anchorUri,
|
|
1370
|
-
depth: 0,
|
|
1371
|
-
authorDid: postView.author.did,
|
|
1372
|
-
state,
|
|
1373
|
-
}),
|
|
1374
|
-
],
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
const childrenByParentUri = this.groupThreadChildrenByParent(
|
|
1379
|
-
anchorUri,
|
|
1380
|
-
uris,
|
|
1381
|
-
state,
|
|
1382
|
-
)
|
|
1383
|
-
const rootUri = getRootUri(anchorUri, post)
|
|
1384
|
-
const opDid = uriToDid(rootUri)
|
|
1385
|
-
const authorDid = postView.author.did
|
|
1386
|
-
const isOPPost = authorDid === opDid
|
|
1387
|
-
const anchorViolatesThreadGate = post.violatesThreadGate
|
|
1388
|
-
|
|
1389
|
-
// Builds the parent tree, and whether it is a contiguous OP thread.
|
|
1390
|
-
const parentTree = !anchorViolatesThreadGate
|
|
1391
|
-
? this.threadV2Parent(
|
|
1392
|
-
{
|
|
1393
|
-
childUri: anchorUri,
|
|
1394
|
-
opDid,
|
|
1395
|
-
rootUri,
|
|
1396
|
-
|
|
1397
|
-
above,
|
|
1398
|
-
depth: -1,
|
|
1399
|
-
},
|
|
1400
|
-
state,
|
|
1401
|
-
)
|
|
1402
|
-
: undefined
|
|
1403
|
-
|
|
1404
|
-
const { tree: parent, isOPThread: isOPThreadFromRootToParent } =
|
|
1405
|
-
parentTree ?? { tree: undefined, isOPThread: false }
|
|
1406
|
-
|
|
1407
|
-
const isOPThread = parent
|
|
1408
|
-
? isOPThreadFromRootToParent && isOPPost
|
|
1409
|
-
: isOPPost
|
|
1410
|
-
|
|
1411
|
-
const anchorDepth = 0 // The depth of the anchor post is always 0.
|
|
1412
|
-
let anchorTree: ThreadTree
|
|
1413
|
-
let hasOtherReplies = false
|
|
1414
|
-
|
|
1415
|
-
if (this.noUnauthenticatedPost(state, postView)) {
|
|
1416
|
-
anchorTree = {
|
|
1417
|
-
type: 'noUnauthenticated',
|
|
1418
|
-
item: this.threadV2ItemNoUnauthenticated({
|
|
1419
|
-
uri: anchorUri,
|
|
1420
|
-
depth: anchorDepth,
|
|
1421
|
-
}),
|
|
1422
|
-
parent,
|
|
1423
|
-
}
|
|
1424
|
-
} else {
|
|
1425
|
-
const { replies, hasOtherReplies: hasOtherRepliesShadow } =
|
|
1426
|
-
!anchorViolatesThreadGate
|
|
1427
|
-
? this.threadV2Replies(
|
|
1428
|
-
{
|
|
1429
|
-
parentUri: anchorUri,
|
|
1430
|
-
isOPThread,
|
|
1431
|
-
opDid,
|
|
1432
|
-
rootUri,
|
|
1433
|
-
childrenByParentUri,
|
|
1434
|
-
below,
|
|
1435
|
-
depth: 1,
|
|
1436
|
-
branchingFactor,
|
|
1437
|
-
},
|
|
1438
|
-
state,
|
|
1439
|
-
)
|
|
1440
|
-
: { replies: undefined, hasOtherReplies: false }
|
|
1441
|
-
hasOtherReplies = hasOtherRepliesShadow
|
|
1442
|
-
|
|
1443
|
-
anchorTree = {
|
|
1444
|
-
type: 'post',
|
|
1445
|
-
item: this.threadV2ItemPost({
|
|
1446
|
-
depth: anchorDepth,
|
|
1447
|
-
isOPThread,
|
|
1448
|
-
postView,
|
|
1449
|
-
repliesAllowance: Infinity, // While we don't have pagination.
|
|
1450
|
-
uri: anchorUri,
|
|
1451
|
-
}),
|
|
1452
|
-
tags: post.tags,
|
|
1453
|
-
hasOPLike: !!state.threadContexts?.get(postView.uri)?.like,
|
|
1454
|
-
parent,
|
|
1455
|
-
replies,
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
const thread = sortTrimFlattenThreadTree(
|
|
1460
|
-
anchorTree,
|
|
1461
|
-
{
|
|
1462
|
-
opDid,
|
|
1463
|
-
branchingFactor,
|
|
1464
|
-
sort,
|
|
1465
|
-
viewer: state.ctx?.viewer ?? null,
|
|
1466
|
-
threadTagsBumpDown: this.threadTagsBumpDown,
|
|
1467
|
-
threadTagsHide: this.threadTagsHide,
|
|
1468
|
-
visibilityTagRankPrefix: this.visibilityTagRankPrefix,
|
|
1469
|
-
},
|
|
1470
|
-
state.ctx?.features.checkGate(
|
|
1471
|
-
state.ctx.features.Gate.ThreadsReplyRankingExplorationEnable,
|
|
1472
|
-
),
|
|
1473
|
-
)
|
|
1474
|
-
|
|
1475
|
-
return {
|
|
1476
|
-
hasOtherReplies,
|
|
1477
|
-
thread,
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
private threadV2Parent(
|
|
1482
|
-
{
|
|
1483
|
-
childUri,
|
|
1484
|
-
opDid,
|
|
1485
|
-
rootUri,
|
|
1486
|
-
above,
|
|
1487
|
-
depth,
|
|
1488
|
-
}: {
|
|
1489
|
-
childUri: AtUriString
|
|
1490
|
-
opDid: DidString
|
|
1491
|
-
rootUri: AtUriString
|
|
1492
|
-
above: number
|
|
1493
|
-
depth: number
|
|
1494
|
-
},
|
|
1495
|
-
state: HydrationState,
|
|
1496
|
-
): { tree: ThreadTreeVisible; isOPThread: boolean } | undefined {
|
|
1497
|
-
// Reached the `above` limit.
|
|
1498
|
-
if (Math.abs(depth) > above) {
|
|
1499
|
-
return undefined
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
// Not found.
|
|
1503
|
-
const uri = state.posts?.get(childUri)?.record.reply?.parent.uri
|
|
1504
|
-
if (!uri) {
|
|
1505
|
-
return undefined
|
|
1506
|
-
}
|
|
1507
|
-
const postView = this.post(uri, state)
|
|
1508
|
-
const post = state.posts?.get(uri)
|
|
1509
|
-
if (!post || !postView) {
|
|
1510
|
-
return {
|
|
1511
|
-
tree: {
|
|
1512
|
-
type: 'notFound',
|
|
1513
|
-
item: this.threadV2ItemNotFound({ uri, depth }),
|
|
1514
|
-
},
|
|
1515
|
-
isOPThread: false,
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
if (rootUri !== getRootUri(uri, post)) {
|
|
1519
|
-
// Outside thread boundary.
|
|
1520
|
-
return undefined
|
|
1521
|
-
}
|
|
1522
|
-
|
|
1523
|
-
// Blocked (1p and 3p for parent).
|
|
1524
|
-
const authorDid = postView.author.did
|
|
1525
|
-
const has1pBlock = this.viewerBlockExists(authorDid, state)
|
|
1526
|
-
const has3pBlock =
|
|
1527
|
-
!state.ctx?.include3pBlocks && state.postBlocks?.get(childUri)?.parent
|
|
1528
|
-
if (has1pBlock || has3pBlock) {
|
|
1529
|
-
return {
|
|
1530
|
-
tree: {
|
|
1531
|
-
type: 'blocked',
|
|
1532
|
-
item: this.threadV2ItemBlocked({
|
|
1533
|
-
uri,
|
|
1534
|
-
depth,
|
|
1535
|
-
authorDid,
|
|
1536
|
-
state,
|
|
1537
|
-
}),
|
|
1538
|
-
},
|
|
1539
|
-
isOPThread: false,
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
// Recurse up.
|
|
1544
|
-
const parentTree = this.threadV2Parent(
|
|
1545
|
-
{
|
|
1546
|
-
childUri: uri,
|
|
1547
|
-
opDid,
|
|
1548
|
-
rootUri,
|
|
1549
|
-
above,
|
|
1550
|
-
depth: depth - 1,
|
|
1551
|
-
},
|
|
1552
|
-
state,
|
|
1553
|
-
)
|
|
1554
|
-
const { tree: parent, isOPThread: isOPThreadFromRootToParent } =
|
|
1555
|
-
parentTree ?? { tree: undefined, isOPThread: false }
|
|
1556
|
-
|
|
1557
|
-
const isOPPost = authorDid === opDid
|
|
1558
|
-
const isOPThread = parent
|
|
1559
|
-
? isOPThreadFromRootToParent && isOPPost
|
|
1560
|
-
: isOPPost
|
|
1561
|
-
|
|
1562
|
-
if (this.noUnauthenticatedPost(state, postView)) {
|
|
1563
|
-
return {
|
|
1564
|
-
tree: {
|
|
1565
|
-
type: 'noUnauthenticated',
|
|
1566
|
-
item: this.threadV2ItemNoUnauthenticated({
|
|
1567
|
-
uri,
|
|
1568
|
-
depth,
|
|
1569
|
-
}),
|
|
1570
|
-
parent,
|
|
1571
|
-
},
|
|
1572
|
-
isOPThread,
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
const parentUri = post.record.reply?.parent.uri
|
|
1577
|
-
const hasMoreParents = !!parentUri && !parent
|
|
1578
|
-
|
|
1579
|
-
return {
|
|
1580
|
-
tree: {
|
|
1581
|
-
type: 'post',
|
|
1582
|
-
item: this.threadV2ItemPost({
|
|
1583
|
-
depth,
|
|
1584
|
-
isOPThread,
|
|
1585
|
-
moreParents: hasMoreParents,
|
|
1586
|
-
postView,
|
|
1587
|
-
uri,
|
|
1588
|
-
}),
|
|
1589
|
-
tags: post.tags,
|
|
1590
|
-
hasOPLike: !!state.threadContexts?.get(postView.uri)?.like,
|
|
1591
|
-
parent,
|
|
1592
|
-
replies: undefined,
|
|
1593
|
-
},
|
|
1594
|
-
isOPThread,
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
private threadV2Replies(
|
|
1599
|
-
{
|
|
1600
|
-
parentUri,
|
|
1601
|
-
isOPThread: isOPThreadFromRootToParent,
|
|
1602
|
-
opDid,
|
|
1603
|
-
rootUri,
|
|
1604
|
-
childrenByParentUri,
|
|
1605
|
-
below,
|
|
1606
|
-
depth,
|
|
1607
|
-
branchingFactor,
|
|
1608
|
-
}: {
|
|
1609
|
-
parentUri: AtUriString
|
|
1610
|
-
isOPThread: boolean
|
|
1611
|
-
opDid: string
|
|
1612
|
-
rootUri: AtUriString
|
|
1613
|
-
childrenByParentUri: Record<AtUriString, AtUriString[]>
|
|
1614
|
-
below: number
|
|
1615
|
-
depth: number
|
|
1616
|
-
branchingFactor: number
|
|
1617
|
-
},
|
|
1618
|
-
state: HydrationState,
|
|
1619
|
-
): { replies: ThreadTreeVisible[] | undefined; hasOtherReplies: boolean } {
|
|
1620
|
-
// Reached the `below` limit.
|
|
1621
|
-
if (depth > below) {
|
|
1622
|
-
return { replies: undefined, hasOtherReplies: false }
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
const childrenUris: AtUriString[] = childrenByParentUri[parentUri] ?? []
|
|
1626
|
-
let hasOtherReplies = false
|
|
1627
|
-
const replies = mapDefined(childrenUris, (uri) => {
|
|
1628
|
-
const replyInclusion = this.checkThreadV2ReplyInclusion({
|
|
1629
|
-
uri,
|
|
1630
|
-
rootUri,
|
|
1631
|
-
state,
|
|
1632
|
-
})
|
|
1633
|
-
if (!replyInclusion) {
|
|
1634
|
-
return undefined
|
|
1635
|
-
}
|
|
1636
|
-
const { authorDid, post, postView } = replyInclusion
|
|
1637
|
-
|
|
1638
|
-
// Hidden.
|
|
1639
|
-
const { isOther } = this.isOtherThreadPost(
|
|
1640
|
-
{ post, postView, rootUri, uri },
|
|
1641
|
-
state,
|
|
1642
|
-
)
|
|
1643
|
-
if (isOther) {
|
|
1644
|
-
// Only care about anchor replies
|
|
1645
|
-
if (depth === 1) {
|
|
1646
|
-
hasOtherReplies = true
|
|
1647
|
-
}
|
|
1648
|
-
return undefined
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
// Recurse down.
|
|
1652
|
-
const isOPThread = isOPThreadFromRootToParent && authorDid === opDid
|
|
1653
|
-
const { replies: nestedReplies } = this.threadV2Replies(
|
|
1654
|
-
{
|
|
1655
|
-
parentUri: uri,
|
|
1656
|
-
isOPThread,
|
|
1657
|
-
opDid,
|
|
1658
|
-
rootUri,
|
|
1659
|
-
childrenByParentUri,
|
|
1660
|
-
below,
|
|
1661
|
-
depth: depth + 1,
|
|
1662
|
-
branchingFactor,
|
|
1663
|
-
},
|
|
1664
|
-
state,
|
|
1665
|
-
)
|
|
1666
|
-
|
|
1667
|
-
const reachedDepth = depth === below
|
|
1668
|
-
const repliesAllowance = reachedDepth ? 0 : branchingFactor
|
|
1669
|
-
|
|
1670
|
-
const tree: ThreadTree = {
|
|
1671
|
-
type: 'post',
|
|
1672
|
-
item: this.threadV2ItemPost({
|
|
1673
|
-
depth,
|
|
1674
|
-
isOPThread,
|
|
1675
|
-
postView,
|
|
1676
|
-
repliesAllowance,
|
|
1677
|
-
uri,
|
|
1678
|
-
}),
|
|
1679
|
-
tags: post.tags,
|
|
1680
|
-
hasOPLike: !!state.threadContexts?.get(postView.uri)?.like,
|
|
1681
|
-
parent: undefined,
|
|
1682
|
-
replies: nestedReplies,
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
return tree
|
|
1686
|
-
})
|
|
1687
|
-
|
|
1688
|
-
return {
|
|
1689
|
-
replies,
|
|
1690
|
-
hasOtherReplies,
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
private threadV2ItemPost({
|
|
1695
|
-
depth,
|
|
1696
|
-
isOPThread,
|
|
1697
|
-
moreParents,
|
|
1698
|
-
postView,
|
|
1699
|
-
repliesAllowance,
|
|
1700
|
-
uri,
|
|
1701
|
-
}: {
|
|
1702
|
-
depth: number
|
|
1703
|
-
isOPThread: boolean
|
|
1704
|
-
moreParents?: boolean
|
|
1705
|
-
postView: PostView
|
|
1706
|
-
repliesAllowance?: number
|
|
1707
|
-
uri: AtUriString
|
|
1708
|
-
}): ThreadItemValuePost {
|
|
1709
|
-
const moreReplies =
|
|
1710
|
-
repliesAllowance === undefined
|
|
1711
|
-
? 0
|
|
1712
|
-
: Math.max((postView.replyCount ?? 0) - repliesAllowance, 0)
|
|
1713
|
-
|
|
1714
|
-
return {
|
|
1715
|
-
uri,
|
|
1716
|
-
depth,
|
|
1717
|
-
value: app.bsky.unspecced.defs.threadItemPost.$build({
|
|
1718
|
-
post: postView,
|
|
1719
|
-
moreParents: moreParents ?? false,
|
|
1720
|
-
moreReplies,
|
|
1721
|
-
opThread: isOPThread,
|
|
1722
|
-
hiddenByThreadgate: false, // Hidden posts are handled by threadOtherV2
|
|
1723
|
-
mutedByViewer: false, // Hidden posts are handled by threadOtherV2
|
|
1724
|
-
}),
|
|
1725
|
-
}
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
private threadV2ItemNoUnauthenticated({
|
|
1729
|
-
uri,
|
|
1730
|
-
depth,
|
|
1731
|
-
}: {
|
|
1732
|
-
uri: AtUriString
|
|
1733
|
-
depth: number
|
|
1734
|
-
}): ThreadItemValueNoUnauthenticated {
|
|
1735
|
-
return {
|
|
1736
|
-
uri,
|
|
1737
|
-
depth,
|
|
1738
|
-
value: app.bsky.unspecced.defs.threadItemNoUnauthenticated.$build({}),
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
private threadV2ItemNotFound({
|
|
1743
|
-
uri,
|
|
1744
|
-
depth,
|
|
1745
|
-
}: {
|
|
1746
|
-
uri: AtUriString
|
|
1747
|
-
depth: number
|
|
1748
|
-
}): ThreadItemValueNotFound {
|
|
1749
|
-
return {
|
|
1750
|
-
uri,
|
|
1751
|
-
depth,
|
|
1752
|
-
value: app.bsky.unspecced.defs.threadItemNotFound.$build({}),
|
|
1753
|
-
}
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
|
-
private threadV2ItemBlocked({
|
|
1757
|
-
uri,
|
|
1758
|
-
depth,
|
|
1759
|
-
authorDid,
|
|
1760
|
-
state,
|
|
1761
|
-
}: {
|
|
1762
|
-
uri: AtUriString
|
|
1763
|
-
depth: number
|
|
1764
|
-
authorDid: DidString
|
|
1765
|
-
state: HydrationState
|
|
1766
|
-
}): ThreadItemValueBlocked {
|
|
1767
|
-
return {
|
|
1768
|
-
uri,
|
|
1769
|
-
depth,
|
|
1770
|
-
value: app.bsky.unspecced.defs.threadItemBlocked.$build({
|
|
1771
|
-
author: {
|
|
1772
|
-
did: authorDid,
|
|
1773
|
-
viewer: this.blockedProfileViewer(authorDid, state),
|
|
1774
|
-
},
|
|
1775
|
-
}),
|
|
1776
|
-
}
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
threadOtherV2(
|
|
1780
|
-
skeleton: { anchor: AtUriString; uris: AtUriString[] },
|
|
1781
|
-
state: HydrationState,
|
|
1782
|
-
{
|
|
1783
|
-
below,
|
|
1784
|
-
branchingFactor,
|
|
1785
|
-
}: {
|
|
1786
|
-
below: number
|
|
1787
|
-
branchingFactor: number
|
|
1788
|
-
},
|
|
1789
|
-
): ThreadOtherItem[] {
|
|
1790
|
-
const { anchor: anchorUri, uris } = skeleton
|
|
1791
|
-
|
|
1792
|
-
// Not found.
|
|
1793
|
-
const postView = this.post(anchorUri, state)
|
|
1794
|
-
const post = state.posts?.get(anchorUri)
|
|
1795
|
-
if (!post || !postView) {
|
|
1796
|
-
return []
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
// Blocked (only 1p for anchor).
|
|
1800
|
-
if (this.viewerBlockExists(postView.author.did, state)) {
|
|
1801
|
-
return []
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
const childrenByParentUri = this.groupThreadChildrenByParent(
|
|
1805
|
-
anchorUri,
|
|
1806
|
-
uris,
|
|
1807
|
-
state,
|
|
1808
|
-
)
|
|
1809
|
-
const rootUri = getRootUri(anchorUri, post)
|
|
1810
|
-
const opDid = uriToDid(rootUri)
|
|
1811
|
-
|
|
1812
|
-
const anchorTree: ThreadOtherAnchorPostNode = {
|
|
1813
|
-
type: 'hiddenAnchor',
|
|
1814
|
-
item: this.threadOtherV2ItemPostAnchor({ depth: 0, uri: anchorUri }),
|
|
1815
|
-
replies: this.threadOtherV2Replies(
|
|
1816
|
-
{
|
|
1817
|
-
parentUri: anchorUri,
|
|
1818
|
-
rootUri,
|
|
1819
|
-
childrenByParentUri,
|
|
1820
|
-
below,
|
|
1821
|
-
depth: 1,
|
|
1822
|
-
},
|
|
1823
|
-
state,
|
|
1824
|
-
),
|
|
1825
|
-
}
|
|
1826
|
-
|
|
1827
|
-
return sortTrimFlattenThreadTree(
|
|
1828
|
-
anchorTree,
|
|
1829
|
-
{
|
|
1830
|
-
opDid,
|
|
1831
|
-
branchingFactor,
|
|
1832
|
-
viewer: state.ctx?.viewer ?? null,
|
|
1833
|
-
threadTagsBumpDown: this.threadTagsBumpDown,
|
|
1834
|
-
threadTagsHide: this.threadTagsHide,
|
|
1835
|
-
visibilityTagRankPrefix: this.visibilityTagRankPrefix,
|
|
1836
|
-
},
|
|
1837
|
-
state.ctx?.features.checkGate(
|
|
1838
|
-
state.ctx.features.Gate.ThreadsReplyRankingExplorationEnable,
|
|
1839
|
-
),
|
|
1840
|
-
)
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
|
-
private threadOtherV2Replies(
|
|
1844
|
-
{
|
|
1845
|
-
parentUri,
|
|
1846
|
-
rootUri,
|
|
1847
|
-
childrenByParentUri,
|
|
1848
|
-
below,
|
|
1849
|
-
depth,
|
|
1850
|
-
}: {
|
|
1851
|
-
parentUri: AtUriString
|
|
1852
|
-
rootUri: AtUriString
|
|
1853
|
-
childrenByParentUri: Record<AtUriString, AtUriString[]>
|
|
1854
|
-
below: number
|
|
1855
|
-
depth: number
|
|
1856
|
-
},
|
|
1857
|
-
state: HydrationState,
|
|
1858
|
-
): ThreadOtherPostNode[] | undefined {
|
|
1859
|
-
// Reached the `below` limit.
|
|
1860
|
-
if (depth > below) {
|
|
1861
|
-
return undefined
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1864
|
-
const childrenUris: AtUriString[] = childrenByParentUri[parentUri] ?? []
|
|
1865
|
-
return mapDefined(childrenUris, (uri) => {
|
|
1866
|
-
const replyInclusion = this.checkThreadV2ReplyInclusion({
|
|
1867
|
-
uri,
|
|
1868
|
-
rootUri,
|
|
1869
|
-
state,
|
|
1870
|
-
})
|
|
1871
|
-
if (!replyInclusion) {
|
|
1872
|
-
return undefined
|
|
1873
|
-
}
|
|
1874
|
-
const { post, postView } = replyInclusion
|
|
1875
|
-
|
|
1876
|
-
// Other posts to pull out
|
|
1877
|
-
const { isOther, hiddenByThreadgate, mutedByViewer } =
|
|
1878
|
-
this.isOtherThreadPost({ post, postView, rootUri, uri }, state)
|
|
1879
|
-
if (isOther) {
|
|
1880
|
-
// Only show hidden anchor replies, not all hidden.
|
|
1881
|
-
if (depth > 1) {
|
|
1882
|
-
return undefined
|
|
1883
|
-
}
|
|
1884
|
-
} else if (depth === 1) {
|
|
1885
|
-
// Don't include non-hidden anchor replies.
|
|
1886
|
-
return undefined
|
|
1887
|
-
}
|
|
1888
|
-
|
|
1889
|
-
// Recurse down.
|
|
1890
|
-
const replies = this.threadOtherV2Replies(
|
|
1891
|
-
{
|
|
1892
|
-
parentUri: uri,
|
|
1893
|
-
rootUri,
|
|
1894
|
-
childrenByParentUri,
|
|
1895
|
-
below,
|
|
1896
|
-
depth: depth + 1,
|
|
1897
|
-
},
|
|
1898
|
-
state,
|
|
1899
|
-
)
|
|
1900
|
-
|
|
1901
|
-
const item = this.threadOtherV2ItemPost({
|
|
1902
|
-
depth,
|
|
1903
|
-
hiddenByThreadgate,
|
|
1904
|
-
mutedByViewer,
|
|
1905
|
-
postView,
|
|
1906
|
-
uri,
|
|
1907
|
-
})
|
|
1908
|
-
|
|
1909
|
-
const tree: ThreadOtherPostNode = {
|
|
1910
|
-
type: 'hiddenPost',
|
|
1911
|
-
item: item,
|
|
1912
|
-
tags: post.tags,
|
|
1913
|
-
replies,
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
return tree
|
|
1917
|
-
})
|
|
1918
|
-
}
|
|
1919
|
-
|
|
1920
|
-
private threadOtherV2ItemPostAnchor({
|
|
1921
|
-
depth,
|
|
1922
|
-
uri,
|
|
1923
|
-
}: {
|
|
1924
|
-
depth: number
|
|
1925
|
-
uri: AtUriString
|
|
1926
|
-
}): ThreadOtherAnchorPostNode['item'] {
|
|
1927
|
-
return {
|
|
1928
|
-
uri,
|
|
1929
|
-
depth,
|
|
1930
|
-
// In hidden replies, the anchor value is undefined, so it doesn't include the anchor in the result.
|
|
1931
|
-
// This is helpful so we can use the same internal structure for hidden and non-hidden, while omitting anchor for hidden.
|
|
1932
|
-
value: undefined,
|
|
1933
|
-
}
|
|
1934
|
-
}
|
|
1935
|
-
|
|
1936
|
-
private threadOtherV2ItemPost({
|
|
1937
|
-
depth,
|
|
1938
|
-
hiddenByThreadgate,
|
|
1939
|
-
mutedByViewer,
|
|
1940
|
-
postView,
|
|
1941
|
-
uri,
|
|
1942
|
-
}: {
|
|
1943
|
-
depth: number
|
|
1944
|
-
hiddenByThreadgate: boolean
|
|
1945
|
-
mutedByViewer: boolean
|
|
1946
|
-
postView: PostView
|
|
1947
|
-
uri: AtUriString
|
|
1948
|
-
}): ThreadOtherItemValuePost {
|
|
1949
|
-
const base = this.threadOtherV2ItemPostAnchor({ depth, uri })
|
|
1950
|
-
return {
|
|
1951
|
-
...base,
|
|
1952
|
-
value: app.bsky.unspecced.defs.threadItemPost.$build({
|
|
1953
|
-
post: postView,
|
|
1954
|
-
hiddenByThreadgate,
|
|
1955
|
-
mutedByViewer,
|
|
1956
|
-
moreParents: false, // "Other" replies don't have parents.
|
|
1957
|
-
moreReplies: 0, // "Other" replies don't have replies hydrated.
|
|
1958
|
-
opThread: false, // "Other" replies don't contain OP threads.
|
|
1959
|
-
}),
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1962
|
-
|
|
1963
|
-
private checkThreadV2ReplyInclusion({
|
|
1964
|
-
uri,
|
|
1965
|
-
rootUri,
|
|
1966
|
-
state,
|
|
1967
|
-
}: {
|
|
1968
|
-
uri: AtUriString
|
|
1969
|
-
rootUri: string
|
|
1970
|
-
state: HydrationState
|
|
1971
|
-
}): {
|
|
1972
|
-
authorDid: string
|
|
1973
|
-
post: Post
|
|
1974
|
-
postView: PostView
|
|
1975
|
-
} | null {
|
|
1976
|
-
// Not found.
|
|
1977
|
-
const post = state.posts?.get(uri)
|
|
1978
|
-
if (post?.violatesThreadGate) {
|
|
1979
|
-
return null
|
|
1980
|
-
}
|
|
1981
|
-
const postView = this.post(uri, state)
|
|
1982
|
-
if (!post || !postView) {
|
|
1983
|
-
return null
|
|
1984
|
-
}
|
|
1985
|
-
const authorDid = postView.author.did
|
|
1986
|
-
if (rootUri !== getRootUri(uri, post)) {
|
|
1987
|
-
// outside thread boundary
|
|
1988
|
-
return null
|
|
1989
|
-
}
|
|
1990
|
-
|
|
1991
|
-
// Blocked (1p and 3p for replies).
|
|
1992
|
-
const has1pBlock = this.viewerBlockExists(authorDid, state)
|
|
1993
|
-
const has3pBlock =
|
|
1994
|
-
!state.ctx?.include3pBlocks && state.postBlocks?.get(uri)?.parent
|
|
1995
|
-
if (has1pBlock || has3pBlock) {
|
|
1996
|
-
return null
|
|
1997
|
-
}
|
|
1998
|
-
if (!this.viewerSeesNeedsReview({ uri, did: authorDid }, state)) {
|
|
1999
|
-
return null
|
|
2000
|
-
}
|
|
2001
|
-
|
|
2002
|
-
// No unauthenticated.
|
|
2003
|
-
if (this.noUnauthenticatedPost(state, postView)) {
|
|
2004
|
-
return null
|
|
2005
|
-
}
|
|
2006
|
-
|
|
2007
|
-
return { authorDid, post, postView }
|
|
2008
|
-
}
|
|
2009
|
-
|
|
2010
|
-
private isOtherThreadPost(
|
|
2011
|
-
{
|
|
2012
|
-
post,
|
|
2013
|
-
postView,
|
|
2014
|
-
rootUri,
|
|
2015
|
-
uri,
|
|
2016
|
-
}: {
|
|
2017
|
-
post: Post
|
|
2018
|
-
postView: PostView
|
|
2019
|
-
rootUri: AtUriString
|
|
2020
|
-
uri: AtUriString
|
|
2021
|
-
},
|
|
2022
|
-
state: HydrationState,
|
|
2023
|
-
): {
|
|
2024
|
-
isOther: boolean
|
|
2025
|
-
hiddenByTag: boolean
|
|
2026
|
-
hiddenByThreadgate: boolean
|
|
2027
|
-
mutedByViewer: boolean
|
|
2028
|
-
} {
|
|
2029
|
-
const opDid = creatorFromUri(rootUri)
|
|
2030
|
-
const authorDid = creatorFromUri(uri)
|
|
2031
|
-
|
|
2032
|
-
let hiddenByTag = false
|
|
2033
|
-
if (
|
|
2034
|
-
state.ctx?.features.checkGate(
|
|
2035
|
-
state.ctx.features.Gate.ThreadsReplyRankingExplorationEnable,
|
|
2036
|
-
)
|
|
2037
|
-
) {
|
|
2038
|
-
hiddenByTag = authorDid !== opDid && post.tags.has(this.visibilityTagHide)
|
|
2039
|
-
} else {
|
|
2040
|
-
const showBecauseFollowing = !!postView.author.viewer?.following
|
|
2041
|
-
hiddenByTag =
|
|
2042
|
-
authorDid !== opDid &&
|
|
2043
|
-
authorDid !== state.ctx?.viewer &&
|
|
2044
|
-
!showBecauseFollowing &&
|
|
2045
|
-
this.threadTagsHide.some((t) => post.tags.has(t))
|
|
2046
|
-
}
|
|
2047
|
-
|
|
2048
|
-
const hiddenByThreadgate =
|
|
2049
|
-
state.ctx?.viewer !== authorDid &&
|
|
2050
|
-
this.replyIsHiddenByThreadgate(uri, rootUri, state)
|
|
2051
|
-
|
|
2052
|
-
const mutedByViewer = this.viewerMuteExists(authorDid, state)
|
|
2053
|
-
const isPushPin =
|
|
2054
|
-
isPostRecordType(post.record) && post.record.text.trim() === '📌'
|
|
2055
|
-
|
|
2056
|
-
return {
|
|
2057
|
-
isOther: hiddenByTag || hiddenByThreadgate || mutedByViewer || isPushPin,
|
|
2058
|
-
hiddenByTag,
|
|
2059
|
-
hiddenByThreadgate,
|
|
2060
|
-
mutedByViewer,
|
|
2061
|
-
}
|
|
2062
|
-
}
|
|
2063
|
-
|
|
2064
|
-
private groupThreadChildrenByParent(
|
|
2065
|
-
anchorUri: AtUriString,
|
|
2066
|
-
uris: AtUriString[],
|
|
2067
|
-
state: HydrationState,
|
|
2068
|
-
): Record<AtUriString, AtUriString[]> {
|
|
2069
|
-
// Groups children of each parent.
|
|
2070
|
-
const includedPosts = new Set<AtUriString>([anchorUri])
|
|
2071
|
-
const childrenByParentUri: Record<AtUriString, AtUriString[]> = {}
|
|
2072
|
-
uris.forEach((uri) => {
|
|
2073
|
-
const post = state.posts?.get(uri)
|
|
2074
|
-
const parentUri = post?.record.reply?.parent.uri
|
|
2075
|
-
if (!parentUri) return
|
|
2076
|
-
if (includedPosts.has(uri)) return
|
|
2077
|
-
includedPosts.add(uri)
|
|
2078
|
-
childrenByParentUri[parentUri] ??= []
|
|
2079
|
-
childrenByParentUri[parentUri].push(uri)
|
|
2080
|
-
})
|
|
2081
|
-
return childrenByParentUri
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2084
|
-
// Embeds
|
|
2085
|
-
// ------------
|
|
2086
|
-
|
|
2087
|
-
embed(
|
|
2088
|
-
postUri: AtUriString,
|
|
2089
|
-
embed: $Typed<Embed> | Unknown$TypedObject,
|
|
2090
|
-
state: HydrationState,
|
|
2091
|
-
depth: number,
|
|
2092
|
-
): $Typed<EmbedView> | undefined {
|
|
2093
|
-
if (isImagesEmbedType(embed)) {
|
|
2094
|
-
return this.imagesEmbed(creatorFromUri(postUri), embed)
|
|
2095
|
-
} else if (isVideoEmbedType(embed)) {
|
|
2096
|
-
return this.videoEmbed(creatorFromUri(postUri), embed)
|
|
2097
|
-
} else if (isGalleryEmbedType(embed)) {
|
|
2098
|
-
return this.galleryEmbed(creatorFromUri(postUri), embed)
|
|
2099
|
-
} else if (isExternalEmbedType(embed)) {
|
|
2100
|
-
return this.externalEmbed(creatorFromUri(postUri), embed, state)
|
|
2101
|
-
} else if (isRecordEmbedType(embed)) {
|
|
2102
|
-
return this.recordEmbed(postUri, embed, state, depth)
|
|
2103
|
-
} else if (isRecordWithMediaType(embed)) {
|
|
2104
|
-
return this.recordWithMediaEmbed(postUri, embed, state, depth)
|
|
2105
|
-
} else {
|
|
2106
|
-
return undefined
|
|
2107
|
-
}
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
imagesEmbed(did: DidString, embed: ImagesEmbed): $Typed<ImagesEmbedView> {
|
|
2111
|
-
const imgViews = embed.images.map((img) => ({
|
|
2112
|
-
thumb: this.imgUriBuilder.getPresetUri(
|
|
2113
|
-
'feed_thumbnail',
|
|
2114
|
-
did,
|
|
2115
|
-
getBlobCidString(img.image),
|
|
2116
|
-
),
|
|
2117
|
-
fullsize: this.imgUriBuilder.getPresetUri(
|
|
2118
|
-
'feed_fullsize',
|
|
2119
|
-
did,
|
|
2120
|
-
getBlobCidString(img.image),
|
|
2121
|
-
),
|
|
2122
|
-
alt: img.alt,
|
|
2123
|
-
aspectRatio: img.aspectRatio,
|
|
2124
|
-
}))
|
|
2125
|
-
return app.bsky.embed.images.view.$build({
|
|
2126
|
-
images: imgViews,
|
|
2127
|
-
})
|
|
2128
|
-
}
|
|
2129
|
-
|
|
2130
|
-
videoEmbed(did: DidString, embed: VideoEmbed): $Typed<VideoEmbedView> {
|
|
2131
|
-
const cid = getBlobCidString(embed.video)
|
|
2132
|
-
return app.bsky.embed.video.view.$build({
|
|
2133
|
-
cid,
|
|
2134
|
-
playlist: this.videoUriBuilder.playlist({ did, cid }),
|
|
2135
|
-
thumbnail: this.videoUriBuilder.thumbnail({ did, cid }),
|
|
2136
|
-
alt: embed.alt,
|
|
2137
|
-
aspectRatio: embed.aspectRatio,
|
|
2138
|
-
presentation: embed.presentation,
|
|
2139
|
-
})
|
|
2140
|
-
}
|
|
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
|
-
|
|
2183
|
-
externalEmbed(
|
|
2184
|
-
did: DidString,
|
|
2185
|
-
embed: ExternalEmbed,
|
|
2186
|
-
state: HydrationState,
|
|
2187
|
-
): $Typed<ExternalEmbedView> {
|
|
2188
|
-
// Start from the post-author-supplied embed values, then spread the
|
|
2189
|
-
// SS-derived view on top so any field the hydrated doc/publication
|
|
2190
|
-
// supplies wins (title, description, thumb, source, etc). When no SS
|
|
2191
|
-
// records were hydrated `ssView` is `undefined` and the spread is a
|
|
2192
|
-
// no-op, leaving the base values in place. `associatedRefs` is set
|
|
2193
|
-
// last because the post is authoritative about what was pinned.
|
|
2194
|
-
const ssView = this.externalEmbedFromStandardSite({
|
|
2195
|
-
associatedRefs: embed.external.associatedRefs,
|
|
2196
|
-
state,
|
|
2197
|
-
assumedUrl: embed.external.uri,
|
|
2198
|
-
})
|
|
2199
|
-
// The author-supplied (scraped) thumbnail always wins when present —
|
|
2200
|
-
// it's the per-article OG image. Only when the embed has no thumb do
|
|
2201
|
-
// we fall back to whatever the SS overlay provides (the document's
|
|
2202
|
-
// `coverImage`). `thumb` is set after the `...ssView` spread so the
|
|
2203
|
-
// overlay's `coverImage`-derived thumb can't clobber the embed's.
|
|
2204
|
-
const embeddedThumb = embed.external.thumb
|
|
2205
|
-
? this.imgUriBuilder.getPresetUri(
|
|
2206
|
-
'feed_thumbnail',
|
|
2207
|
-
did,
|
|
2208
|
-
getBlobCidString(embed.external.thumb),
|
|
2209
|
-
)
|
|
2210
|
-
: undefined
|
|
2211
|
-
return app.bsky.embed.external.view.$build({
|
|
2212
|
-
external: {
|
|
2213
|
-
uri: embed.external.uri,
|
|
2214
|
-
title: embed.external.title,
|
|
2215
|
-
description: embed.external.description,
|
|
2216
|
-
...ssView,
|
|
2217
|
-
thumb: embeddedThumb ?? ssView?.thumb,
|
|
2218
|
-
associatedRefs: embed.external.associatedRefs,
|
|
2219
|
-
},
|
|
2220
|
-
})
|
|
2221
|
-
}
|
|
2222
|
-
|
|
2223
|
-
/**
|
|
2224
|
-
* Read-path entry point: caller has the post's `external.associatedRefs[]`
|
|
2225
|
-
* and the global hydration state, and we resolve the matching SS records
|
|
2226
|
-
* by ref. Used by `externalEmbed`.
|
|
2227
|
-
*
|
|
2228
|
-
* Returns a fully-populated `viewExternal['external']` shape (including
|
|
2229
|
-
* the required `uri`/`title`/`description`), or `undefined` if the
|
|
2230
|
-
* records didn't hydrate, didn't pass URL validation, or couldn't
|
|
2231
|
-
* supply the required fields. Callers spread the result over a base
|
|
2232
|
-
* view to layer SS-derived fields on top of post-author-supplied ones.
|
|
2233
|
-
*
|
|
2234
|
-
* `assumedUrl` is the canonical web URL the embed claims to represent
|
|
2235
|
-
* (the post's `external.uri` on the read path, the request's `url` on
|
|
2236
|
-
* compose). Validation logic uses it to confirm the SS records actually
|
|
2237
|
-
* back that URL, and the same value is echoed back as `uri` on the
|
|
2238
|
-
* returned overlay.
|
|
2239
|
-
*/
|
|
2240
|
-
externalEmbedFromStandardSite({
|
|
2241
|
-
associatedRefs,
|
|
2242
|
-
state,
|
|
2243
|
-
assumedUrl,
|
|
2244
|
-
}: {
|
|
2245
|
-
associatedRefs: ExternalEmbed['external']['associatedRefs']
|
|
2246
|
-
state: HydrationState
|
|
2247
|
-
assumedUrl: string
|
|
2248
|
-
}): ExternalEmbedView['external'] | undefined {
|
|
2249
|
-
const { document, publication } =
|
|
2250
|
-
getSiteStandardRecordsFromHydrationMapsByRefs(
|
|
2251
|
-
associatedRefs,
|
|
2252
|
-
state.siteStandardDocuments,
|
|
2253
|
-
state.siteStandardPublications,
|
|
2254
|
-
)
|
|
2255
|
-
return this.externalEmbedFromStandardSiteRecords({
|
|
2256
|
-
document,
|
|
2257
|
-
publication,
|
|
2258
|
-
state,
|
|
2259
|
-
assumedUrl,
|
|
2260
|
-
})
|
|
2261
|
-
}
|
|
2262
|
-
|
|
2263
|
-
/**
|
|
2264
|
-
* Compose-path entry point: caller already knows which document and
|
|
2265
|
-
* publication backed the request and has them in hand (e.g. from
|
|
2266
|
-
* iterating the SS hydration maps directly). Skips the by-ref lookup
|
|
2267
|
-
* that `externalEmbedFromStandardSite` does. Used by
|
|
2268
|
-
* `getEmbedExternalView`.
|
|
2269
|
-
*
|
|
2270
|
-
* Returns a fully-populated `viewExternal['external']` shape (including
|
|
2271
|
-
* the required `uri`/`title`/`description`), or `undefined` if any of
|
|
2272
|
-
* the validation gates rejected the pair or the records couldn't
|
|
2273
|
-
* supply the required fields.
|
|
2274
|
-
*
|
|
2275
|
-
* `state` is still needed for label hydration. `assumedUrl` is the
|
|
2276
|
-
* canonical web URL the embed claims to represent (the post's
|
|
2277
|
-
* `external.uri` on the read path, the request's `url` on compose);
|
|
2278
|
-
* validation logic uses it to confirm the SS records actually back that
|
|
2279
|
-
* URL, and the same value is echoed back as `uri` on the returned
|
|
2280
|
-
* overlay.
|
|
2281
|
-
*/
|
|
2282
|
-
externalEmbedFromStandardSiteRecords({
|
|
2283
|
-
document,
|
|
2284
|
-
publication,
|
|
2285
|
-
state,
|
|
2286
|
-
assumedUrl,
|
|
2287
|
-
}: {
|
|
2288
|
-
document: AssociatedSiteStandardRecord<SiteStandardDocument> | undefined
|
|
2289
|
-
publication:
|
|
2290
|
-
| AssociatedSiteStandardRecord<SiteStandardPublication>
|
|
2291
|
-
| undefined
|
|
2292
|
-
state: HydrationState
|
|
2293
|
-
assumedUrl: string
|
|
2294
|
-
}): ExternalEmbedView['external'] | undefined {
|
|
2295
|
-
// Three layers of validation gate this overlay:
|
|
2296
|
-
//
|
|
2297
|
-
// 1. Hydrator nulls taken-down records and mirrors the null across
|
|
2298
|
-
// doc/pub pairs (see `actionSiteStandardTakedownLabels` in
|
|
2299
|
-
// `hydration/hydrator.ts`).
|
|
2300
|
-
// 2. The lookups guarantee structural agreement between doc and pub
|
|
2301
|
-
// — matching `site`/`uri`, no orphan doc claiming a missing
|
|
2302
|
-
// publication (see `getSiteStandardRecordsFromHydrationMapsByRefs`
|
|
2303
|
-
// and `…ByDocumentUri` in `hydration/external.ts`).
|
|
2304
|
-
// 3. `validateStandardSiteForUrl` below checks that the records back
|
|
2305
|
-
// the URL the embed claims (see `util/standard-site.ts`).
|
|
2306
|
-
//
|
|
2307
|
-
// If any layer rejected, callers see `undefined` and fall back to the
|
|
2308
|
-
// bare embed rather than render partial / disagreeing enrichment.
|
|
2309
|
-
if (!document && !publication) return undefined
|
|
2310
|
-
if (!validateStandardSiteForUrl(document, publication, assumedUrl)) {
|
|
2311
|
-
viewsLogger.warn(
|
|
2312
|
-
{
|
|
2313
|
-
documentUri: document?.ref.uri,
|
|
2314
|
-
publicationUri: publication?.ref.uri,
|
|
2315
|
-
},
|
|
2316
|
-
'site.standard URL validation failed',
|
|
2317
|
-
)
|
|
2318
|
-
return undefined
|
|
2319
|
-
}
|
|
2320
|
-
|
|
2321
|
-
// viewExternal requires `title` and `description`. If neither side of
|
|
2322
|
-
// the pair supplies usable values for both, there's no enrichment to
|
|
2323
|
-
// render — return undefined and let the caller fall back.
|
|
2324
|
-
let title: string
|
|
2325
|
-
let description: string
|
|
2326
|
-
if (document?.info.record) {
|
|
2327
|
-
// Treat the document as authoritative for both fields as a unit, so
|
|
2328
|
-
// we never blend a doc's title with a publication's description.
|
|
2329
|
-
title = document.info.record.title
|
|
2330
|
-
description = document.info.record.description ?? ''
|
|
2331
|
-
} else if (publication) {
|
|
2332
|
-
title = publication.info.record.name
|
|
2333
|
-
description = publication.info.record.description ?? ''
|
|
2334
|
-
} else {
|
|
2335
|
-
return undefined
|
|
2336
|
-
}
|
|
2337
|
-
|
|
2338
|
-
// if we don't have a title at this point, something is wrong with the SS
|
|
2339
|
-
// record (it's a required field) and therefore the enrichment isn't worth
|
|
2340
|
-
// showing
|
|
2341
|
-
if (!title) return undefined
|
|
2342
|
-
|
|
2343
|
-
const overlay: ExternalEmbedView['external'] = {
|
|
2344
|
-
// @ts-ignore this is mis-typed
|
|
2345
|
-
uri: assumedUrl,
|
|
2346
|
-
title,
|
|
2347
|
-
description,
|
|
2348
|
-
}
|
|
2349
|
-
|
|
2350
|
-
const docCover = document?.info.record.coverImage
|
|
2351
|
-
if (docCover) {
|
|
2352
|
-
overlay.thumb = this.imgUriBuilder.getPresetUri(
|
|
2353
|
-
'feed_thumbnail',
|
|
2354
|
-
creatorFromUri(document.ref.uri),
|
|
2355
|
-
getBlobCidString(docCover),
|
|
2356
|
-
)
|
|
2357
|
-
}
|
|
2358
|
-
|
|
2359
|
-
if (document?.info.record.publishedAt) {
|
|
2360
|
-
overlay.createdAt = document.info.record.publishedAt
|
|
2361
|
-
}
|
|
2362
|
-
if (document?.info.record.updatedAt) {
|
|
2363
|
-
overlay.updatedAt = document.info.record.updatedAt
|
|
2364
|
-
}
|
|
2365
|
-
const readingTime = document?.info.record.textContent
|
|
2366
|
-
? estimateReadingTimeMinutes(document.info.record.textContent)
|
|
2367
|
-
: undefined
|
|
2368
|
-
if (readingTime !== undefined) overlay.readingTime = readingTime
|
|
2369
|
-
|
|
2370
|
-
// Merge labels applied to either the document or the publication onto the
|
|
2371
|
-
// same `viewExternal.labels` array — clients moderate the embed as single
|
|
2372
|
-
// unit, so doc-scoped and publication-scoped labels end up in the same
|
|
2373
|
-
// bucket.
|
|
2374
|
-
const labels = [
|
|
2375
|
-
...(document ? state.labels?.getBySubject(document.ref.uri) ?? [] : []),
|
|
2376
|
-
...(publication
|
|
2377
|
-
? state.labels?.getBySubject(publication.ref.uri) ?? []
|
|
2378
|
-
: []),
|
|
2379
|
-
]
|
|
2380
|
-
if (labels.length) overlay.labels = labels
|
|
2381
|
-
|
|
2382
|
-
if (publication) {
|
|
2383
|
-
overlay.source = this.externalEmbedSource(publication)
|
|
2384
|
-
}
|
|
2385
|
-
|
|
2386
|
-
// Profiles of the owners of the records backing this embed. Hydrator
|
|
2387
|
-
// covers these DIDs alongside post-author profiles, so misses here
|
|
2388
|
-
// only happen when an actor is unavailable (suspended, deleted, etc.)
|
|
2389
|
-
// — drop those rather than emit `undefined` slots.
|
|
2390
|
-
const uniqueDids = dedupeStrs(
|
|
2391
|
-
mapDefined([document?.ref.uri, publication?.ref.uri], (uri) =>
|
|
2392
|
-
uri ? uriToDid(uri) : undefined,
|
|
2393
|
-
) as DidString[],
|
|
2394
|
-
)
|
|
2395
|
-
const associatedProfiles = mapDefined(uniqueDids, (did) =>
|
|
2396
|
-
this.profileBasic(did, state),
|
|
2397
|
-
) as ProfileViewBasic[]
|
|
2398
|
-
if (associatedProfiles.length)
|
|
2399
|
-
overlay.associatedProfiles = associatedProfiles
|
|
2400
|
-
|
|
2401
|
-
return overlay
|
|
2402
|
-
}
|
|
2403
|
-
|
|
2404
|
-
externalEmbedSource(
|
|
2405
|
-
publication: AssociatedSiteStandardRecord<SiteStandardPublication>,
|
|
2406
|
-
): $Typed<ExternalEmbedSourceView> {
|
|
2407
|
-
const { record } = publication.info
|
|
2408
|
-
const pubDid = creatorFromUri(publication.ref.uri)
|
|
2409
|
-
return app.bsky.embed.external.viewExternalSource.$build({
|
|
2410
|
-
uri: record.url,
|
|
2411
|
-
icon: record.icon
|
|
2412
|
-
? this.imgUriBuilder.getPresetUri(
|
|
2413
|
-
'avatar',
|
|
2414
|
-
pubDid,
|
|
2415
|
-
getBlobCidString(record.icon),
|
|
2416
|
-
)
|
|
2417
|
-
: undefined,
|
|
2418
|
-
title: record.name,
|
|
2419
|
-
description: record.description,
|
|
2420
|
-
theme: record.basicTheme
|
|
2421
|
-
? externalEmbedSourceTheme(record.basicTheme)
|
|
2422
|
-
: undefined,
|
|
2423
|
-
})
|
|
2424
|
-
}
|
|
2425
|
-
|
|
2426
|
-
embedNotFound(uri: AtUriString): $Typed<RecordEmbedView> {
|
|
2427
|
-
return app.bsky.embed.record.view.$build({
|
|
2428
|
-
record: app.bsky.embed.record.viewNotFound.$build({
|
|
2429
|
-
uri,
|
|
2430
|
-
notFound: true,
|
|
2431
|
-
}),
|
|
2432
|
-
})
|
|
2433
|
-
}
|
|
2434
|
-
|
|
2435
|
-
embedDetached(uri: AtUriString): $Typed<RecordEmbedView> {
|
|
2436
|
-
return app.bsky.embed.record.view.$build({
|
|
2437
|
-
record: app.bsky.embed.record.viewDetached.$build({
|
|
2438
|
-
uri,
|
|
2439
|
-
detached: true,
|
|
2440
|
-
}),
|
|
2441
|
-
})
|
|
2442
|
-
}
|
|
2443
|
-
|
|
2444
|
-
embedBlocked(
|
|
2445
|
-
uri: AtUriString,
|
|
2446
|
-
state: HydrationState,
|
|
2447
|
-
): $Typed<RecordEmbedView> {
|
|
2448
|
-
const creator = creatorFromUri(uri)
|
|
2449
|
-
return app.bsky.embed.record.view.$build({
|
|
2450
|
-
record: app.bsky.embed.record.viewBlocked.$build({
|
|
2451
|
-
uri,
|
|
2452
|
-
blocked: true,
|
|
2453
|
-
author: {
|
|
2454
|
-
did: creator,
|
|
2455
|
-
viewer: this.blockedProfileViewer(creator, state),
|
|
2456
|
-
},
|
|
2457
|
-
}),
|
|
2458
|
-
})
|
|
2459
|
-
}
|
|
2460
|
-
|
|
2461
|
-
embedPostView(
|
|
2462
|
-
uri: AtUriString,
|
|
2463
|
-
state: HydrationState,
|
|
2464
|
-
depth: number,
|
|
2465
|
-
): $Typed<PostEmbedView> | undefined {
|
|
2466
|
-
const postView = this.post(uri, state, depth)
|
|
2467
|
-
if (!postView) return
|
|
2468
|
-
return app.bsky.embed.record.viewRecord.$build({
|
|
2469
|
-
uri: postView.uri,
|
|
2470
|
-
cid: postView.cid,
|
|
2471
|
-
author: postView.author,
|
|
2472
|
-
value: postView.record,
|
|
2473
|
-
labels: postView.labels,
|
|
2474
|
-
likeCount: postView.likeCount,
|
|
2475
|
-
replyCount: postView.replyCount,
|
|
2476
|
-
repostCount: postView.repostCount,
|
|
2477
|
-
quoteCount: postView.quoteCount,
|
|
2478
|
-
indexedAt: postView.indexedAt,
|
|
2479
|
-
embeds: depth > 1 ? undefined : postView.embed ? [postView.embed] : [],
|
|
2480
|
-
})
|
|
2481
|
-
}
|
|
2482
|
-
|
|
2483
|
-
recordEmbed(
|
|
2484
|
-
postUri: AtUriString,
|
|
2485
|
-
embed: RecordEmbed,
|
|
2486
|
-
state: HydrationState,
|
|
2487
|
-
depth: number,
|
|
2488
|
-
withTypeTag: false,
|
|
2489
|
-
): RecordEmbedView
|
|
2490
|
-
recordEmbed(
|
|
2491
|
-
postUri: AtUriString,
|
|
2492
|
-
embed: RecordEmbed,
|
|
2493
|
-
state: HydrationState,
|
|
2494
|
-
depth: number,
|
|
2495
|
-
withTypeTag?: true,
|
|
2496
|
-
): $Typed<RecordEmbedView>
|
|
2497
|
-
recordEmbed(
|
|
2498
|
-
postUri: AtUriString,
|
|
2499
|
-
embed: RecordEmbed,
|
|
2500
|
-
state: HydrationState,
|
|
2501
|
-
depth: number,
|
|
2502
|
-
withTypeTag = true,
|
|
2503
|
-
): RecordEmbedView {
|
|
2504
|
-
const uri = embed.record.uri
|
|
2505
|
-
const parsedUri = new AtUri(uri)
|
|
2506
|
-
if (
|
|
2507
|
-
this.viewerBlockExists(parsedUri.did, state) ||
|
|
2508
|
-
(!state.ctx?.include3pBlocks && state.postBlocks?.get(postUri)?.embed)
|
|
2509
|
-
) {
|
|
2510
|
-
return this.embedBlocked(uri, state)
|
|
2511
|
-
}
|
|
2512
|
-
|
|
2513
|
-
const post = state.posts?.get(postUri)
|
|
2514
|
-
if (post?.violatesEmbeddingRules) {
|
|
2515
|
-
return this.embedDetached(uri)
|
|
2516
|
-
}
|
|
2517
|
-
|
|
2518
|
-
if (parsedUri.collection === app.bsky.feed.post.$type) {
|
|
2519
|
-
const view = this.embedPostView(uri, state, depth)
|
|
2520
|
-
if (!view) return this.embedNotFound(uri)
|
|
2521
|
-
const postgateRecordUri = postUriToPostgateUri(parsedUri.toString())
|
|
2522
|
-
const postgate = state.postgates?.get(postgateRecordUri)
|
|
2523
|
-
if (postgate?.record?.detachedEmbeddingUris?.includes(postUri)) {
|
|
2524
|
-
return this.embedDetached(uri)
|
|
2525
|
-
}
|
|
2526
|
-
return this.recordEmbedWrapper(view, withTypeTag)
|
|
2527
|
-
} else if (parsedUri.collection === app.bsky.feed.generator.$type) {
|
|
2528
|
-
const view = this.feedGenerator(uri, state)
|
|
2529
|
-
if (!view) return this.embedNotFound(uri)
|
|
2530
|
-
return this.recordEmbedWrapper(
|
|
2531
|
-
app.bsky.feed.defs.generatorView.$build(view),
|
|
2532
|
-
withTypeTag,
|
|
2533
|
-
)
|
|
2534
|
-
} else if (parsedUri.collection === app.bsky.graph.list.$type) {
|
|
2535
|
-
const view = this.list(uri, state)
|
|
2536
|
-
if (!view) return this.embedNotFound(uri)
|
|
2537
|
-
return this.recordEmbedWrapper(
|
|
2538
|
-
app.bsky.graph.defs.listView.$build(view),
|
|
2539
|
-
withTypeTag,
|
|
2540
|
-
)
|
|
2541
|
-
} else if (parsedUri.collection === app.bsky.labeler.service.$type) {
|
|
2542
|
-
const view = this.labeler(parsedUri.did, state)
|
|
2543
|
-
if (!view) return this.embedNotFound(uri)
|
|
2544
|
-
return this.recordEmbedWrapper(
|
|
2545
|
-
app.bsky.labeler.defs.labelerView.$build(view),
|
|
2546
|
-
withTypeTag,
|
|
2547
|
-
)
|
|
2548
|
-
} else if (parsedUri.collection === app.bsky.graph.starterpack.$type) {
|
|
2549
|
-
const view = this.starterPackBasic(uri, state)
|
|
2550
|
-
if (!view) return this.embedNotFound(uri)
|
|
2551
|
-
return this.recordEmbedWrapper(
|
|
2552
|
-
app.bsky.graph.defs.starterPackViewBasic.$build(view),
|
|
2553
|
-
withTypeTag,
|
|
2554
|
-
)
|
|
2555
|
-
}
|
|
2556
|
-
return this.embedNotFound(uri)
|
|
2557
|
-
}
|
|
2558
|
-
|
|
2559
|
-
private recordEmbedWrapper<T extends $Typed<RecordEmbedViewInternal>>(
|
|
2560
|
-
record: T,
|
|
2561
|
-
withTypeTag: boolean,
|
|
2562
|
-
): RecordEmbedView {
|
|
2563
|
-
return withTypeTag
|
|
2564
|
-
? app.bsky.embed.record.view.$build({ record })
|
|
2565
|
-
: { record }
|
|
2566
|
-
}
|
|
2567
|
-
|
|
2568
|
-
recordWithMediaEmbed(
|
|
2569
|
-
postUri: AtUriString,
|
|
2570
|
-
embed: RecordWithMedia,
|
|
2571
|
-
state: HydrationState,
|
|
2572
|
-
depth: number,
|
|
2573
|
-
): $Typed<RecordWithMediaView> | undefined {
|
|
2574
|
-
const creator = creatorFromUri(postUri)
|
|
2575
|
-
let mediaEmbed:
|
|
2576
|
-
| $Typed<ImagesEmbedView>
|
|
2577
|
-
| $Typed<VideoEmbedView>
|
|
2578
|
-
| $Typed<GalleryEmbedView>
|
|
2579
|
-
| $Typed<ExternalEmbedView>
|
|
2580
|
-
if (isImagesEmbedType(embed.media)) {
|
|
2581
|
-
mediaEmbed = this.imagesEmbed(creator, embed.media)
|
|
2582
|
-
} else if (isVideoEmbedType(embed.media)) {
|
|
2583
|
-
mediaEmbed = this.videoEmbed(creator, embed.media)
|
|
2584
|
-
} else if (isGalleryEmbedType(embed.media)) {
|
|
2585
|
-
mediaEmbed = this.galleryEmbed(creator, embed.media)
|
|
2586
|
-
} else if (isExternalEmbedType(embed.media)) {
|
|
2587
|
-
mediaEmbed = this.externalEmbed(creator, embed.media, state)
|
|
2588
|
-
} else {
|
|
2589
|
-
return
|
|
2590
|
-
}
|
|
2591
|
-
return app.bsky.embed.recordWithMedia.view.$build({
|
|
2592
|
-
media: mediaEmbed,
|
|
2593
|
-
record: this.recordEmbed(postUri, embed.record, state, depth, false),
|
|
2594
|
-
})
|
|
2595
|
-
}
|
|
2596
|
-
|
|
2597
|
-
userReplyDisabled(
|
|
2598
|
-
uri: AtUriString,
|
|
2599
|
-
state: HydrationState,
|
|
2600
|
-
): boolean | undefined {
|
|
2601
|
-
const post = state.posts?.get(uri)
|
|
2602
|
-
if (post?.violatesThreadGate) {
|
|
2603
|
-
return true
|
|
2604
|
-
}
|
|
2605
|
-
const rootUriStr = post?.record.reply?.root.uri ?? uri
|
|
2606
|
-
const gate = state.threadgates?.get(
|
|
2607
|
-
postUriToThreadgateUri(rootUriStr),
|
|
2608
|
-
)?.record
|
|
2609
|
-
const viewer = state.ctx?.viewer
|
|
2610
|
-
if (!gate || !viewer) {
|
|
2611
|
-
return undefined
|
|
2612
|
-
}
|
|
2613
|
-
const rootPost = state.posts?.get(rootUriStr)?.record
|
|
2614
|
-
const ownerDid = creatorFromUri(rootUriStr)
|
|
2615
|
-
const {
|
|
2616
|
-
canReply,
|
|
2617
|
-
allowFollower,
|
|
2618
|
-
allowFollowing,
|
|
2619
|
-
allowListUris = [],
|
|
2620
|
-
} = parseThreadGate(viewer, ownerDid, rootPost ?? null, gate)
|
|
2621
|
-
if (canReply) {
|
|
2622
|
-
return false
|
|
2623
|
-
}
|
|
2624
|
-
if (allowFollower && state.profileViewers?.get(ownerDid)?.following) {
|
|
2625
|
-
return false
|
|
2626
|
-
}
|
|
2627
|
-
if (allowFollowing && state.profileViewers?.get(ownerDid)?.followedBy) {
|
|
2628
|
-
return false
|
|
2629
|
-
}
|
|
2630
|
-
for (const listUri of allowListUris) {
|
|
2631
|
-
const list = state.listViewers?.get(listUri)
|
|
2632
|
-
if (list?.viewerInList) {
|
|
2633
|
-
return false
|
|
2634
|
-
}
|
|
2635
|
-
}
|
|
2636
|
-
return true
|
|
2637
|
-
}
|
|
2638
|
-
|
|
2639
|
-
userPostEmbeddingDisabled(
|
|
2640
|
-
uri: AtUriString,
|
|
2641
|
-
state: HydrationState,
|
|
2642
|
-
): boolean | undefined {
|
|
2643
|
-
const post = state.posts?.get(uri)
|
|
2644
|
-
if (!post) {
|
|
2645
|
-
return true
|
|
2646
|
-
}
|
|
2647
|
-
const postgateRecordUri = postUriToPostgateUri(uri)
|
|
2648
|
-
const gate = state.postgates?.get(postgateRecordUri)?.record
|
|
2649
|
-
const viewerDid = state.ctx?.viewer ?? undefined
|
|
2650
|
-
const {
|
|
2651
|
-
embeddingRules: { canEmbed },
|
|
2652
|
-
} = parsePostgate({
|
|
2653
|
-
gate,
|
|
2654
|
-
viewerDid,
|
|
2655
|
-
authorDid: creatorFromUri(uri),
|
|
2656
|
-
})
|
|
2657
|
-
if (canEmbed) {
|
|
2658
|
-
return false
|
|
2659
|
-
}
|
|
2660
|
-
return true
|
|
2661
|
-
}
|
|
2662
|
-
|
|
2663
|
-
viewerPinned(uri: AtUriString, state: HydrationState, authorDid: string) {
|
|
2664
|
-
if (!state.ctx?.viewer || state.ctx.viewer !== authorDid) return
|
|
2665
|
-
const actor = state.actors?.get(authorDid)
|
|
2666
|
-
if (!actor) return
|
|
2667
|
-
const pinnedPost = safePinnedPost(actor.profile?.pinnedPost)
|
|
2668
|
-
if (!pinnedPost) return undefined
|
|
2669
|
-
return pinnedPost.uri === uri
|
|
2670
|
-
}
|
|
2671
|
-
|
|
2672
|
-
notification(
|
|
2673
|
-
notif: Notification,
|
|
2674
|
-
lastSeenAt: string | undefined,
|
|
2675
|
-
state: HydrationState,
|
|
2676
|
-
): Un$Typed<NotificationView> | undefined {
|
|
2677
|
-
if (!notif.timestamp || !notif.reason) return
|
|
2678
|
-
const uri = new AtUri(notif.uri)
|
|
2679
|
-
|
|
2680
|
-
const author = this.profile(uri.did, state)
|
|
2681
|
-
if (!author) return
|
|
2682
|
-
|
|
2683
|
-
let recordInfo:
|
|
2684
|
-
| Post
|
|
2685
|
-
| Like
|
|
2686
|
-
| Repost
|
|
2687
|
-
| Follow
|
|
2688
|
-
| RecordInfo<ProfileRecord>
|
|
2689
|
-
| Verification
|
|
2690
|
-
| Pick<RecordInfo<Required<NotificationRecordDeleted>>, 'cid' | 'record'>
|
|
2691
|
-
| undefined
|
|
2692
|
-
| null
|
|
2693
|
-
|
|
2694
|
-
if (uri.collection === app.bsky.feed.post.$type) {
|
|
2695
|
-
recordInfo = state.posts?.get(notif.uri as AtUriString)
|
|
2696
|
-
} else if (uri.collection === app.bsky.feed.like.$type) {
|
|
2697
|
-
recordInfo = state.likes?.get(notif.uri as AtUriString)
|
|
2698
|
-
} else if (uri.collection === app.bsky.feed.repost.$type) {
|
|
2699
|
-
recordInfo = state.reposts?.get(notif.uri as AtUriString)
|
|
2700
|
-
} else if (uri.collection === app.bsky.graph.follow.$type) {
|
|
2701
|
-
recordInfo = state.follows?.get(notif.uri as AtUriString)
|
|
2702
|
-
} else if (uri.collection === app.bsky.graph.verification.$type) {
|
|
2703
|
-
// When a verification record is removed, the record won't be found,
|
|
2704
|
-
// both for the `verified` and `unverified` notifications.
|
|
2705
|
-
recordInfo = state.verifications?.get(notif.uri as AtUriString) ?? {
|
|
2706
|
-
record: notificationDeletedRecord,
|
|
2707
|
-
cid: notificationDeletedRecordCid,
|
|
2708
|
-
}
|
|
2709
|
-
} else if (uri.collection === app.bsky.actor.profile.$type) {
|
|
2710
|
-
const actor = state.actors?.get(author.did)
|
|
2711
|
-
recordInfo =
|
|
2712
|
-
actor && actor.profile && actor.profileCid
|
|
2713
|
-
? ({
|
|
2714
|
-
record: actor.profile,
|
|
2715
|
-
cid: actor.profileCid,
|
|
2716
|
-
sortedAt: actor.sortedAt ?? new Date(0), // @NOTE will be present since profile record is present
|
|
2717
|
-
indexedAt: actor.indexedAt ?? new Date(0), // @NOTE will be present since profile record is present
|
|
2718
|
-
takedownRef: actor.profileTakedownRef,
|
|
2719
|
-
} satisfies RecordInfo<ProfileRecord>)
|
|
2720
|
-
: undefined
|
|
2721
|
-
}
|
|
2722
|
-
if (!recordInfo) return
|
|
2723
|
-
|
|
2724
|
-
const labels = state.labels?.getBySubject(notif.uri as AtUriString) ?? []
|
|
2725
|
-
const selfLabels = this.selfLabels({
|
|
2726
|
-
uri: parseString<AtUriString>(notif.uri),
|
|
2727
|
-
cid: recordInfo.cid,
|
|
2728
|
-
record: recordInfo.record,
|
|
2729
|
-
})
|
|
2730
|
-
const indexedAt = notif.timestamp.toDate().toISOString()
|
|
2731
|
-
return {
|
|
2732
|
-
uri: notif.uri as AtUriString,
|
|
2733
|
-
cid: recordInfo.cid,
|
|
2734
|
-
author,
|
|
2735
|
-
reason: notif.reason,
|
|
2736
|
-
reasonSubject: parseString<AtUriString>(notif.reasonSubject),
|
|
2737
|
-
record: recordInfo.record,
|
|
2738
|
-
// @NOTE works with a hack in listNotifications so that when there's no last-seen time,
|
|
2739
|
-
// the user's first notification is marked unread, and all previous read. in this case,
|
|
2740
|
-
// the last seen time will be equal to the first notification's indexed time.
|
|
2741
|
-
isRead: lastSeenAt ? lastSeenAt > indexedAt : true,
|
|
2742
|
-
indexedAt: notif.timestamp.toDate().toISOString() as DatetimeString,
|
|
2743
|
-
labels: [...labels, ...selfLabels],
|
|
2744
|
-
}
|
|
2745
|
-
}
|
|
2746
|
-
|
|
2747
|
-
indexedAt({ sortedAt, indexedAt }: { sortedAt: Date; indexedAt: Date }) {
|
|
2748
|
-
if (!this.indexedAtEpoch) return sortedAt
|
|
2749
|
-
return indexedAt && indexedAt > this.indexedAtEpoch ? indexedAt : sortedAt
|
|
2750
|
-
}
|
|
2751
|
-
}
|
|
2752
|
-
|
|
2753
|
-
const getRootUri = (uri: AtUriString, post: Post): AtUriString => {
|
|
2754
|
-
return post.record.reply?.root.uri ?? uri
|
|
2755
|
-
}
|
|
2756
|
-
|
|
2757
|
-
const externalEmbedSourceTheme = (
|
|
2758
|
-
theme: SiteStandardPublicationRecord['basicTheme'],
|
|
2759
|
-
): ExternalEmbedSourceThemeView | undefined => {
|
|
2760
|
-
if (!theme) return undefined
|
|
2761
|
-
const view: ExternalEmbedSourceThemeView = {}
|
|
2762
|
-
const background = colorRGB(theme.background)
|
|
2763
|
-
const foreground = colorRGB(theme.foreground)
|
|
2764
|
-
const accent = colorRGB(theme.accent)
|
|
2765
|
-
const accentForeground = colorRGB(theme.accentForeground)
|
|
2766
|
-
if (background) view.backgroundRGB = background
|
|
2767
|
-
if (foreground) view.foregroundRGB = foreground
|
|
2768
|
-
if (accent) view.accentRGB = accent
|
|
2769
|
-
if (accentForeground) view.accentForegroundRGB = accentForeground
|
|
2770
|
-
// No fields set -> don't decorate the view at all.
|
|
2771
|
-
return Object.keys(view).length === 0 ? undefined : view
|
|
2772
|
-
}
|
|
2773
|
-
|
|
2774
|
-
const colorRGB = (
|
|
2775
|
-
color: { $type?: unknown } | undefined,
|
|
2776
|
-
): ExternalEmbedColorRgb | undefined => {
|
|
2777
|
-
if (!color || !site.standard.theme.color.rgb.isTypeOf(color)) return undefined
|
|
2778
|
-
// `isTypeOf` narrows $type but not the shape; pull rgb fields off the
|
|
2779
|
-
// unchecked input. Records that fail full validation are dropped above
|
|
2780
|
-
// by the SS record parser, so by the time we get here the shape matches.
|
|
2781
|
-
const { r, g, b } = color as unknown as { r: number; g: number; b: number }
|
|
2782
|
-
return app.bsky.embed.external.colorRGB.$build({ r, g, b })
|
|
2783
|
-
}
|