Demo using Slices Network GraphQL Relay API to make a teal.fm client

Compare changes

Choose any two refs to compare.

+3 -3
README.md
··· 39 39 2. **Fetch the GraphQL schema** 40 40 41 41 ```bash 42 - npm run schema 42 + npm run schema:prod 43 43 ``` 44 44 45 45 3. **Generate Relay types** ··· 63 63 The project connects to the Slices API. To update the schema: 64 64 65 65 ```bash 66 - npm run schema 66 + npm run schema:prod 67 67 npx relay-compiler 68 68 ``` 69 69 ··· 109 109 - `npm run build` - Build for production 110 110 - `npm run preview` - Preview production build 111 111 - `npm run lint` - Run ESLint 112 - - `npm run schema` - Fetch GraphQL schema from production API 112 + - `npm run schema:prod` - Fetch GraphQL schema from production API 113 113 114 114 ## Features in Detail 115 115
+2 -2
package.json
··· 8 8 "build": "tsc -b && vite build", 9 9 "lint": "eslint .", 10 10 "preview": "vite preview", 11 - "schema:dev": "npx get-graphql-schema 'http://localhost:3000/graphql?slice=at://did:plc:fpruhuo22xkm5o7ttr2ktxdo/network.slices.slice/3m257yljpbg2a' > schema.graphql", 12 - "schema:prod": "npx get-graphql-schema 'https://api.slices.network/graphql?slice=at://did:plc:fpruhuo22xkm5o7ttr2ktxdo/network.slices.slice/3m257yljpbg2a' > schema.graphql" 11 + "schema:dev": "npx get-graphql-schema 'http://localhost:8080/graphql' > schema.graphql", 12 + "schema:prod": "npx get-graphql-schema 'https://quickslice-production-d668.up.railway.app/graphql' > schema.graphql" 13 13 }, 14 14 "dependencies": { 15 15 "graphql-ws": "^6.0.6",
+909 -473
schema.graphql
··· 1 - """ 2 - Indicates that an Input Object is a OneOf Input Object (and thus requires exactly one of its field be provided) 3 - """ 4 - directive @oneOf on INPUT_OBJECT 5 - 6 - """ 7 - Provides a scalar specification URL for specifying the behavior of custom scalar types. 8 - """ 9 - directive @specifiedBy( 10 - """URL that specifies the behavior of this scalar.""" 11 - url: String! 12 - ) on SCALAR 13 - 1 + """Order aggregation results by count""" 14 2 input AggregationOrderBy { 3 + """Order by count (asc or desc)""" 15 4 count: SortDirection 16 5 } 17 6 7 + """Record type: app.bsky.actor.profile""" 18 8 type AppBskyActorProfile { 19 - uri: String! 20 - cid: String! 21 - did: String! 22 - indexedAt: String! 9 + """Record URI""" 10 + uri: String 11 + 12 + """Record CID""" 13 + cid: String 14 + 15 + """DID of record author""" 16 + did: String 17 + 18 + """Collection name""" 19 + collection: String 20 + 21 + """When record was indexed""" 22 + indexedAt: String 23 + 24 + """Handle of the actor who created this record""" 23 25 actorHandle: String 26 + 27 + """Field from lexicon""" 24 28 avatar: Blob 29 + 30 + """Field from lexicon""" 25 31 banner: Blob 32 + 33 + """Field from lexicon""" 26 34 createdAt: String 35 + 36 + """Field from lexicon""" 27 37 description: String 38 + 39 + """Field from lexicon""" 28 40 displayName: String 29 - joinedViaStarterPack: JSON 30 - labels: JSON 31 - pinnedPost: JSON 32 - appBskyFeedPostgate(limit: Int): [AppBskyFeedPostgate!]! 33 - appBskyFeedThreadgate(limit: Int): [AppBskyFeedThreadgate!]! 34 - appBskyActorProfile: AppBskyActorProfile 35 - fmTealAlphaFeedPlay(limit: Int): [FmTealAlphaFeedPlay!]! 36 - appBskyFeedPostgates(limit: Int): [AppBskyFeedPostgate!]! 37 - appBskyFeedThreadgates(limit: Int): [AppBskyFeedThreadgate!]! 38 - appBskyActorProfiles(limit: Int): [AppBskyActorProfile!]! 39 - fmTealAlphaFeedPlays(limit: Int): [FmTealAlphaFeedPlay!]! 41 + 42 + """Field from lexicon""" 43 + joinedViaStarterPack: String 44 + 45 + """Field from lexicon""" 46 + labels: String 47 + 48 + """Field from lexicon""" 49 + pinnedPost: String 50 + 51 + """Field from lexicon""" 52 + pronouns: String 53 + 54 + """Field from lexicon""" 55 + website: String 56 + 57 + """Forward join to referenced record""" 58 + pinnedPostResolved: Record 59 + 60 + """Forward join to referenced record""" 61 + joinedViaStarterPackResolved: Record 62 + 63 + """ 64 + DID join: records in fm.teal.alpha.feed.play that share the same DID as this record 65 + """ 66 + fmTealAlphaFeedPlayByDid( 67 + """Returns the first n items from the list""" 68 + first: Int 69 + 70 + """Returns items after the given cursor""" 71 + after: String 72 + 73 + """Returns the last n items from the list""" 74 + last: Int 75 + 76 + """Returns items before the given cursor""" 77 + before: String 78 + 79 + """Sort order for the connection""" 80 + sortBy: [FmTealAlphaFeedPlaySortFieldInput!] 81 + 82 + """Filter conditions for the query""" 83 + where: FmTealAlphaFeedPlayWhereInput 84 + ): FmTealAlphaFeedPlayConnection 40 85 } 41 86 87 + """Aggregated results for app.bsky.actor.profile""" 42 88 type AppBskyActorProfileAggregated { 43 - avatar: JSON 44 - banner: JSON 45 - createdAt: JSON 46 - description: JSON 47 - displayName: JSON 48 - joinedViaStarterPack: JSON 49 - labels: JSON 50 - pinnedPost: JSON 89 + """Grouped field value""" 90 + uri: String 91 + 92 + """Grouped field value""" 93 + cid: String 94 + 95 + """Grouped field value""" 96 + did: String 97 + 98 + """Grouped field value""" 99 + collection: String 100 + 101 + """Grouped field value""" 102 + indexed_at: String 103 + 104 + """Grouped field value""" 105 + avatar: String 106 + 107 + """Grouped field value""" 108 + banner: String 109 + 110 + """Grouped field value""" 111 + createdAt: String 112 + 113 + """Grouped field value""" 114 + description: String 115 + 116 + """Grouped field value""" 117 + displayName: String 118 + 119 + """Grouped field value""" 120 + joinedViaStarterPack: String 121 + 122 + """Grouped field value""" 123 + labels: String 124 + 125 + """Grouped field value""" 126 + pinnedPost: String 127 + 128 + """Grouped field value""" 129 + pronouns: String 130 + 131 + """Grouped field value""" 132 + website: String 133 + 134 + """Count of records in this group""" 51 135 count: Int! 52 136 } 53 137 138 + """A connection to a list of items for AppBskyActorProfile""" 54 139 type AppBskyActorProfileConnection { 55 - totalCount: Int! 140 + """A list of edges""" 141 + edges: [AppBskyActorProfileEdge!]! 142 + 143 + """Information to aid in pagination""" 56 144 pageInfo: PageInfo! 57 - edges: [AppBskyActorProfileEdge!]! 58 - nodes: [AppBskyActorProfile!]! 145 + 146 + """Total number of items in the connection""" 147 + totalCount: Int 59 148 } 60 149 150 + """An edge in a connection for AppBskyActorProfile""" 61 151 type AppBskyActorProfileEdge { 152 + """The item at the end of the edge""" 62 153 node: AppBskyActorProfile! 154 + 155 + """A cursor for use in pagination""" 63 156 cursor: String! 64 157 } 65 158 66 - type AppBskyEmbedExternal { 67 - uri: String! 68 - cid: String! 69 - did: String! 70 - indexedAt: String! 71 - actorHandle: String 72 - external: JSON! 73 - appBskyFeedPostgate(limit: Int): [AppBskyFeedPostgate!]! 74 - appBskyFeedThreadgate(limit: Int): [AppBskyFeedThreadgate!]! 75 - appBskyActorProfile: AppBskyActorProfile 76 - fmTealAlphaFeedPlay(limit: Int): [FmTealAlphaFeedPlay!]! 77 - appBskyFeedPostgates(limit: Int): [AppBskyFeedPostgate!]! 78 - appBskyFeedThreadgates(limit: Int): [AppBskyFeedThreadgate!]! 79 - appBskyActorProfiles(limit: Int): [AppBskyActorProfile!]! 80 - fmTealAlphaFeedPlays(limit: Int): [FmTealAlphaFeedPlay!]! 159 + """Filter operators for AppBskyActorProfile fields""" 160 + input AppBskyActorProfileFieldCondition { 161 + """Exact match (equals)""" 162 + eq: String 163 + 164 + """Match any value in the list""" 165 + in: [String!] 166 + 167 + """Case-insensitive substring match (string fields only)""" 168 + contains: String 169 + 170 + """Greater than""" 171 + gt: String 172 + 173 + """Greater than or equal to""" 174 + gte: String 175 + 176 + """Less than""" 177 + lt: String 178 + 179 + """Less than or equal to""" 180 + lte: String 81 181 } 82 182 83 - type AppBskyEmbedExternalAggregated { 84 - external: JSON 85 - count: Int! 183 + """Available groupBy fields for AppBskyActorProfile""" 184 + enum AppBskyActorProfileGroupByField { 185 + """Group by uri""" 186 + uri 187 + 188 + """Group by cid""" 189 + cid 190 + 191 + """Group by did""" 192 + did 193 + 194 + """Group by collection""" 195 + collection 196 + 197 + """Group by indexedAt""" 198 + indexedAt 199 + 200 + """Group by actorHandle""" 201 + actorHandle 202 + 203 + """Group by createdAt""" 204 + createdAt 205 + 206 + """Group by description""" 207 + description 208 + 209 + """Group by displayName""" 210 + displayName 211 + 212 + """Group by labels""" 213 + labels 214 + 215 + """Group by pronouns""" 216 + pronouns 217 + 218 + """Group by website""" 219 + website 86 220 } 87 221 88 - type AppBskyEmbedExternalConnection { 89 - totalCount: Int! 90 - pageInfo: PageInfo! 91 - edges: [AppBskyEmbedExternalEdge!]! 92 - nodes: [AppBskyEmbedExternal!]! 222 + """Specifies a field to group by with optional date truncation""" 223 + input AppBskyActorProfileGroupByFieldInput { 224 + """Field name to group by""" 225 + field: AppBskyActorProfileGroupByField! 226 + 227 + """Date truncation interval (for datetime fields)""" 228 + interval: DateInterval 93 229 } 94 230 95 - type AppBskyEmbedExternalEdge { 96 - node: AppBskyEmbedExternal! 97 - cursor: String! 98 - } 231 + """Input type for AppBskyActorProfileInput""" 232 + input AppBskyActorProfileInput { 233 + """Input field for avatar""" 234 + avatar: BlobInput 99 235 100 - type AppBskyEmbedImages { 101 - uri: String! 102 - cid: String! 103 - did: String! 104 - indexedAt: String! 105 - actorHandle: String 106 - images: JSON! 107 - appBskyFeedPostgate(limit: Int): [AppBskyFeedPostgate!]! 108 - appBskyFeedThreadgate(limit: Int): [AppBskyFeedThreadgate!]! 109 - appBskyActorProfile: AppBskyActorProfile 110 - fmTealAlphaFeedPlay(limit: Int): [FmTealAlphaFeedPlay!]! 111 - appBskyFeedPostgates(limit: Int): [AppBskyFeedPostgate!]! 112 - appBskyFeedThreadgates(limit: Int): [AppBskyFeedThreadgate!]! 113 - appBskyActorProfiles(limit: Int): [AppBskyActorProfile!]! 114 - fmTealAlphaFeedPlays(limit: Int): [FmTealAlphaFeedPlay!]! 115 - } 236 + """Input field for banner""" 237 + banner: BlobInput 116 238 117 - type AppBskyEmbedImagesAggregated { 118 - images: JSON 119 - count: Int! 120 - } 239 + """Input field for createdAt""" 240 + createdAt: String 121 241 122 - type AppBskyEmbedImagesConnection { 123 - totalCount: Int! 124 - pageInfo: PageInfo! 125 - edges: [AppBskyEmbedImagesEdge!]! 126 - nodes: [AppBskyEmbedImages!]! 127 - } 242 + """Input field for description""" 243 + description: String 128 244 129 - type AppBskyEmbedImagesEdge { 130 - node: AppBskyEmbedImages! 131 - cursor: String! 245 + """Input field for displayName""" 246 + displayName: String 247 + 248 + """Input field for joinedViaStarterPack""" 249 + joinedViaStarterPack: String 250 + 251 + """Input field for labels""" 252 + labels: String 253 + 254 + """Input field for pinnedPost""" 255 + pinnedPost: String 256 + 257 + """Input field for pronouns""" 258 + pronouns: String 259 + 260 + """Input field for website""" 261 + website: String 132 262 } 133 263 134 - type AppBskyEmbedRecord { 135 - uri: String! 136 - cid: String! 137 - did: String! 138 - indexedAt: String! 139 - actorHandle: String 140 - record: JSON! 141 - appBskyFeedPostgate(limit: Int): [AppBskyFeedPostgate!]! 142 - appBskyFeedThreadgate(limit: Int): [AppBskyFeedThreadgate!]! 143 - appBskyActorProfile: AppBskyActorProfile 144 - fmTealAlphaFeedPlay(limit: Int): [FmTealAlphaFeedPlay!]! 145 - appBskyFeedPostgates(limit: Int): [AppBskyFeedPostgate!]! 146 - appBskyFeedThreadgates(limit: Int): [AppBskyFeedThreadgate!]! 147 - appBskyActorProfiles(limit: Int): [AppBskyActorProfile!]! 148 - fmTealAlphaFeedPlays(limit: Int): [FmTealAlphaFeedPlay!]! 149 - } 264 + """Available sort fields for AppBskyActorProfile""" 265 + enum AppBskyActorProfileSortField { 266 + """Sort by uri""" 267 + uri 268 + 269 + """Sort by cid""" 270 + cid 271 + 272 + """Sort by did""" 273 + did 274 + 275 + """Sort by collection""" 276 + collection 150 277 151 - type AppBskyEmbedRecordAggregated { 152 - record: JSON 153 - count: Int! 154 - } 278 + """Sort by indexedAt""" 279 + indexedAt 155 280 156 - type AppBskyEmbedRecordConnection { 157 - totalCount: Int! 158 - pageInfo: PageInfo! 159 - edges: [AppBskyEmbedRecordEdge!]! 160 - nodes: [AppBskyEmbedRecord!]! 161 - } 281 + """Sort by createdAt""" 282 + createdAt 162 283 163 - type AppBskyEmbedRecordEdge { 164 - node: AppBskyEmbedRecord! 165 - cursor: String! 166 - } 284 + """Sort by description""" 285 + description 167 286 168 - type AppBskyEmbedRecordWithMedia { 169 - uri: String! 170 - cid: String! 171 - did: String! 172 - indexedAt: String! 173 - actorHandle: String 174 - media: JSON! 175 - record: JSON! 176 - appBskyFeedPostgate(limit: Int): [AppBskyFeedPostgate!]! 177 - appBskyFeedThreadgate(limit: Int): [AppBskyFeedThreadgate!]! 178 - appBskyActorProfile: AppBskyActorProfile 179 - fmTealAlphaFeedPlay(limit: Int): [FmTealAlphaFeedPlay!]! 180 - appBskyFeedPostgates(limit: Int): [AppBskyFeedPostgate!]! 181 - appBskyFeedThreadgates(limit: Int): [AppBskyFeedThreadgate!]! 182 - appBskyActorProfiles(limit: Int): [AppBskyActorProfile!]! 183 - fmTealAlphaFeedPlays(limit: Int): [FmTealAlphaFeedPlay!]! 184 - } 287 + """Sort by displayName""" 288 + displayName 185 289 186 - type AppBskyEmbedRecordWithMediaAggregated { 187 - media: JSON 188 - record: JSON 189 - count: Int! 190 - } 290 + """Sort by labels""" 291 + labels 191 292 192 - type AppBskyEmbedRecordWithMediaConnection { 193 - totalCount: Int! 194 - pageInfo: PageInfo! 195 - edges: [AppBskyEmbedRecordWithMediaEdge!]! 196 - nodes: [AppBskyEmbedRecordWithMedia!]! 197 - } 293 + """Sort by pronouns""" 294 + pronouns 198 295 199 - type AppBskyEmbedRecordWithMediaEdge { 200 - node: AppBskyEmbedRecordWithMedia! 201 - cursor: String! 296 + """Sort by website""" 297 + website 202 298 } 203 299 204 - type AppBskyEmbedVideo { 205 - uri: String! 206 - cid: String! 207 - did: String! 208 - indexedAt: String! 209 - actorHandle: String 210 - alt: String 211 - aspectRatio: JSON 212 - captions: JSON 213 - video: Blob! 214 - appBskyFeedPostgate(limit: Int): [AppBskyFeedPostgate!]! 215 - appBskyFeedThreadgate(limit: Int): [AppBskyFeedThreadgate!]! 216 - appBskyActorProfile: AppBskyActorProfile 217 - fmTealAlphaFeedPlay(limit: Int): [FmTealAlphaFeedPlay!]! 218 - appBskyFeedPostgates(limit: Int): [AppBskyFeedPostgate!]! 219 - appBskyFeedThreadgates(limit: Int): [AppBskyFeedThreadgate!]! 220 - appBskyActorProfiles(limit: Int): [AppBskyActorProfile!]! 221 - fmTealAlphaFeedPlays(limit: Int): [FmTealAlphaFeedPlay!]! 222 - } 300 + """Specifies a field to sort by and its direction for AppBskyActorProfile""" 301 + input AppBskyActorProfileSortFieldInput { 302 + """Field to sort by""" 303 + field: AppBskyActorProfileSortField! 223 304 224 - type AppBskyEmbedVideoAggregated { 225 - alt: JSON 226 - aspectRatio: JSON 227 - captions: JSON 228 - video: JSON 229 - count: Int! 305 + """Sort direction (ASC or DESC)""" 306 + direction: SortDirection! 230 307 } 231 308 232 - type AppBskyEmbedVideoConnection { 233 - totalCount: Int! 234 - pageInfo: PageInfo! 235 - edges: [AppBskyEmbedVideoEdge!]! 236 - nodes: [AppBskyEmbedVideo!]! 237 - } 309 + """Filter conditions for AppBskyActorProfile with nested AND/OR support""" 310 + input AppBskyActorProfileWhereInput { 311 + """Filter by uri""" 312 + uri: AppBskyActorProfileFieldCondition 238 313 239 - type AppBskyEmbedVideoEdge { 240 - node: AppBskyEmbedVideo! 241 - cursor: String! 242 - } 314 + """Filter by cid""" 315 + cid: AppBskyActorProfileFieldCondition 243 316 244 - type AppBskyFeedPostgate { 245 - uri: String! 246 - cid: String! 247 - did: String! 248 - indexedAt: String! 249 - actorHandle: String 250 - createdAt: String! 251 - detachedEmbeddingUris: [String] 252 - embeddingRules: JSON 253 - post: String! 254 - appBskyFeedPostgate(limit: Int): [AppBskyFeedPostgate!]! 255 - appBskyFeedThreadgate(limit: Int): [AppBskyFeedThreadgate!]! 256 - appBskyActorProfile: AppBskyActorProfile 257 - fmTealAlphaFeedPlay(limit: Int): [FmTealAlphaFeedPlay!]! 258 - appBskyFeedPostgates(limit: Int): [AppBskyFeedPostgate!]! 259 - appBskyFeedThreadgates(limit: Int): [AppBskyFeedThreadgate!]! 260 - appBskyActorProfiles(limit: Int): [AppBskyActorProfile!]! 261 - fmTealAlphaFeedPlays(limit: Int): [FmTealAlphaFeedPlay!]! 262 - } 317 + """Filter by did""" 318 + did: AppBskyActorProfileFieldCondition 263 319 264 - type AppBskyFeedPostgateAggregated { 265 - createdAt: JSON 266 - detachedEmbeddingUris: JSON 267 - embeddingRules: JSON 268 - post: JSON 269 - count: Int! 270 - } 320 + """Filter by collection""" 321 + collection: AppBskyActorProfileFieldCondition 271 322 272 - type AppBskyFeedPostgateConnection { 273 - totalCount: Int! 274 - pageInfo: PageInfo! 275 - edges: [AppBskyFeedPostgateEdge!]! 276 - nodes: [AppBskyFeedPostgate!]! 277 - } 323 + """Filter by indexedAt""" 324 + indexedAt: AppBskyActorProfileFieldCondition 278 325 279 - type AppBskyFeedPostgateEdge { 280 - node: AppBskyFeedPostgate! 281 - cursor: String! 282 - } 326 + """Filter by actorHandle""" 327 + actorHandle: AppBskyActorProfileFieldCondition 283 328 284 - type AppBskyFeedThreadgate { 285 - uri: String! 286 - cid: String! 287 - did: String! 288 - indexedAt: String! 289 - actorHandle: String 290 - allow: JSON 291 - createdAt: String! 292 - hiddenReplies: [String] 293 - post: String! 294 - appBskyFeedPostgate(limit: Int): [AppBskyFeedPostgate!]! 295 - appBskyFeedThreadgate(limit: Int): [AppBskyFeedThreadgate!]! 296 - appBskyActorProfile: AppBskyActorProfile 297 - fmTealAlphaFeedPlay(limit: Int): [FmTealAlphaFeedPlay!]! 298 - appBskyFeedPostgates(limit: Int): [AppBskyFeedPostgate!]! 299 - appBskyFeedThreadgates(limit: Int): [AppBskyFeedThreadgate!]! 300 - appBskyActorProfiles(limit: Int): [AppBskyActorProfile!]! 301 - fmTealAlphaFeedPlays(limit: Int): [FmTealAlphaFeedPlay!]! 302 - } 329 + """Filter by createdAt""" 330 + createdAt: AppBskyActorProfileFieldCondition 303 331 304 - type AppBskyFeedThreadgateAggregated { 305 - allow: JSON 306 - createdAt: JSON 307 - hiddenReplies: JSON 308 - post: JSON 309 - count: Int! 310 - } 332 + """Filter by description""" 333 + description: AppBskyActorProfileFieldCondition 311 334 312 - type AppBskyFeedThreadgateConnection { 313 - totalCount: Int! 314 - pageInfo: PageInfo! 315 - edges: [AppBskyFeedThreadgateEdge!]! 316 - nodes: [AppBskyFeedThreadgate!]! 317 - } 335 + """Filter by displayName""" 336 + displayName: AppBskyActorProfileFieldCondition 318 337 319 - type AppBskyFeedThreadgateEdge { 320 - node: AppBskyFeedThreadgate! 321 - cursor: String! 322 - } 338 + """Filter by labels""" 339 + labels: AppBskyActorProfileFieldCondition 323 340 324 - type AppBskyRichtextFacet { 325 - uri: String! 326 - cid: String! 327 - did: String! 328 - indexedAt: String! 329 - actorHandle: String 330 - features: JSON! 331 - index: JSON! 332 - appBskyFeedPostgate(limit: Int): [AppBskyFeedPostgate!]! 333 - appBskyFeedThreadgate(limit: Int): [AppBskyFeedThreadgate!]! 334 - appBskyActorProfile: AppBskyActorProfile 335 - fmTealAlphaFeedPlay(limit: Int): [FmTealAlphaFeedPlay!]! 336 - appBskyFeedPostgates(limit: Int): [AppBskyFeedPostgate!]! 337 - appBskyFeedThreadgates(limit: Int): [AppBskyFeedThreadgate!]! 338 - appBskyActorProfiles(limit: Int): [AppBskyActorProfile!]! 339 - fmTealAlphaFeedPlays(limit: Int): [FmTealAlphaFeedPlay!]! 340 - } 341 + """Filter by pronouns""" 342 + pronouns: AppBskyActorProfileFieldCondition 341 343 342 - type AppBskyRichtextFacetAggregated { 343 - features: JSON 344 - index: JSON 345 - count: Int! 346 - } 344 + """Filter by website""" 345 + website: AppBskyActorProfileFieldCondition 347 346 348 - type AppBskyRichtextFacetConnection { 349 - totalCount: Int! 350 - pageInfo: PageInfo! 351 - edges: [AppBskyRichtextFacetEdge!]! 352 - nodes: [AppBskyRichtextFacet!]! 353 - } 347 + """All conditions must match (AND logic)""" 348 + and: [AppBskyActorProfileWhereInput!] 354 349 355 - type AppBskyRichtextFacetEdge { 356 - node: AppBskyRichtextFacet! 357 - cursor: String! 350 + """Any condition must match (OR logic)""" 351 + or: [AppBskyActorProfileWhereInput!] 358 352 } 359 353 354 + """A blob reference with metadata and URL generation""" 360 355 type Blob { 356 + """CID reference to the blob""" 361 357 ref: String! 358 + 359 + """MIME type of the blob""" 362 360 mimeType: String! 361 + 362 + """Size in bytes""" 363 363 size: Int! 364 364 365 365 """ 366 366 Generate CDN URL for the blob with the specified preset (avatar, banner, feed_thumbnail, feed_fullsize) 367 367 """ 368 - url(preset: String): String! 368 + url( 369 + """Image preset: avatar, banner, feed_thumbnail, feed_fullsize""" 370 + preset: String 371 + ): String! 369 372 } 370 373 371 - type ComAtprotoRepoStrongRef { 372 - did: String! 373 - indexedAt: String! 374 - actorHandle: String 375 - cid: String! 376 - uri: String! 377 - appBskyFeedPostgate(limit: Int): [AppBskyFeedPostgate!]! 378 - appBskyFeedThreadgate(limit: Int): [AppBskyFeedThreadgate!]! 379 - appBskyActorProfile: AppBskyActorProfile 380 - fmTealAlphaFeedPlay(limit: Int): [FmTealAlphaFeedPlay!]! 381 - appBskyFeedPostgates(limit: Int): [AppBskyFeedPostgate!]! 382 - appBskyFeedThreadgates(limit: Int): [AppBskyFeedThreadgate!]! 383 - appBskyActorProfiles(limit: Int): [AppBskyActorProfile!]! 384 - fmTealAlphaFeedPlays(limit: Int): [FmTealAlphaFeedPlay!]! 374 + """Input type for blob references""" 375 + input BlobInput { 376 + """CID reference to the blob""" 377 + ref: String! 378 + 379 + """MIME type of the blob""" 380 + mimeType: String! 381 + 382 + """Size in bytes""" 383 + size: Int! 385 384 } 386 385 387 - type ComAtprotoRepoStrongRefAggregated { 388 - cid: JSON 389 - uri: JSON 390 - count: Int! 386 + """Response from uploading a blob""" 387 + type BlobUploadResponse { 388 + """CID reference to the blob""" 389 + ref: String! 390 + 391 + """MIME type of the blob""" 392 + mimeType: String! 393 + 394 + """Size in bytes""" 395 + size: Int! 391 396 } 392 397 393 - type ComAtprotoRepoStrongRefConnection { 394 - totalCount: Int! 395 - pageInfo: PageInfo! 396 - edges: [ComAtprotoRepoStrongRefEdge!]! 397 - nodes: [ComAtprotoRepoStrongRef!]! 398 + """Date truncation intervals for aggregation""" 399 + enum DateInterval { 400 + """Truncate to hour""" 401 + HOUR 402 + 403 + """Truncate to day""" 404 + DAY 405 + 406 + """Truncate to week""" 407 + WEEK 408 + 409 + """Truncate to month""" 410 + MONTH 398 411 } 399 412 400 - type ComAtprotoRepoStrongRefEdge { 401 - node: ComAtprotoRepoStrongRef! 402 - cursor: String! 413 + """Result of a delete mutation""" 414 + type DeleteResult { 415 + """URI of deleted record""" 416 + uri: String 403 417 } 404 418 419 + """Object type from lexicon definition""" 420 + type FmTealAlphaFeedDefsArtist { 421 + """Field from object definition""" 422 + artistMbId: String 423 + 424 + """Field from object definition""" 425 + artistName: String! 426 + } 427 + 428 + """Record type: fm.teal.alpha.feed.play""" 405 429 type FmTealAlphaFeedPlay { 406 - uri: String! 407 - cid: String! 408 - did: String! 409 - indexedAt: String! 430 + """Record URI""" 431 + uri: String 432 + 433 + """Record CID""" 434 + cid: String 435 + 436 + """DID of record author""" 437 + did: String 438 + 439 + """Collection name""" 440 + collection: String 441 + 442 + """When record was indexed""" 443 + indexedAt: String 444 + 445 + """Handle of the actor who created this record""" 410 446 actorHandle: String 411 - artistMbIds: [String] 412 - artistNames: [String] 413 - artists: JSON 447 + 448 + """Field from lexicon""" 449 + artistMbIds: [String!] 450 + 451 + """Field from lexicon""" 452 + artistNames: [String!] 453 + 454 + """Field from lexicon""" 455 + artists: [FmTealAlphaFeedDefsArtist!] 456 + 457 + """Field from lexicon""" 414 458 duration: Int 459 + 460 + """Field from lexicon""" 415 461 isrc: String 462 + 463 + """Field from lexicon""" 416 464 musicServiceBaseDomain: String 465 + 466 + """Field from lexicon""" 417 467 originUrl: String 468 + 469 + """Field from lexicon""" 418 470 playedTime: String 471 + 472 + """Field from lexicon""" 419 473 recordingMbId: String 474 + 475 + """Field from lexicon""" 420 476 releaseMbId: String 477 + 478 + """Field from lexicon""" 421 479 releaseName: String 480 + 481 + """Field from lexicon""" 422 482 submissionClientAgent: String 483 + 484 + """Field from lexicon""" 423 485 trackMbId: String 424 - trackName: String! 425 - appBskyFeedPostgate(limit: Int): [AppBskyFeedPostgate!]! 426 - appBskyFeedThreadgate(limit: Int): [AppBskyFeedThreadgate!]! 427 - appBskyActorProfile: AppBskyActorProfile 428 - fmTealAlphaFeedPlay(limit: Int): [FmTealAlphaFeedPlay!]! 429 - appBskyFeedPostgates(limit: Int): [AppBskyFeedPostgate!]! 430 - appBskyFeedThreadgates(limit: Int): [AppBskyFeedThreadgate!]! 431 - appBskyActorProfiles(limit: Int): [AppBskyActorProfile!]! 432 - fmTealAlphaFeedPlays(limit: Int): [FmTealAlphaFeedPlay!]! 486 + 487 + """Field from lexicon""" 488 + trackName: String 489 + 490 + """ 491 + Reverse join: records in app.bsky.actor.profile that reference this record via joinedViaStarterPack 492 + """ 493 + appBskyActorProfileViaJoinedViaStarterPack( 494 + """Returns the first n items from the list""" 495 + first: Int 496 + 497 + """Returns items after the given cursor""" 498 + after: String 499 + 500 + """Returns the last n items from the list""" 501 + last: Int 502 + 503 + """Returns items before the given cursor""" 504 + before: String 505 + 506 + """Sort order for the connection""" 507 + sortBy: [AppBskyActorProfileSortFieldInput!] 508 + 509 + """Filter conditions for the query""" 510 + where: AppBskyActorProfileWhereInput 511 + ): AppBskyActorProfileConnection 512 + 513 + """ 514 + Reverse join: records in app.bsky.actor.profile that reference this record via pinnedPost 515 + """ 516 + appBskyActorProfileViaPinnedPost( 517 + """Returns the first n items from the list""" 518 + first: Int 519 + 520 + """Returns items after the given cursor""" 521 + after: String 522 + 523 + """Returns the last n items from the list""" 524 + last: Int 525 + 526 + """Returns items before the given cursor""" 527 + before: String 528 + 529 + """Sort order for the connection""" 530 + sortBy: [AppBskyActorProfileSortFieldInput!] 531 + 532 + """Filter conditions for the query""" 533 + where: AppBskyActorProfileWhereInput 534 + ): AppBskyActorProfileConnection 535 + 536 + """ 537 + DID join: record in app.bsky.actor.profile that shares the same DID as this record 538 + """ 539 + appBskyActorProfileByDid: AppBskyActorProfile 433 540 } 434 541 542 + """Aggregated results for fm.teal.alpha.feed.play""" 435 543 type FmTealAlphaFeedPlayAggregated { 436 - artistMbIds: JSON 437 - artistNames: JSON 438 - artists: JSON 439 - duration: JSON 440 - isrc: JSON 441 - musicServiceBaseDomain: JSON 442 - originUrl: JSON 443 - playedTime: JSON 444 - recordingMbId: JSON 445 - releaseMbId: JSON 446 - releaseName: JSON 447 - submissionClientAgent: JSON 448 - trackMbId: JSON 449 - trackName: JSON 544 + """Grouped field value""" 545 + uri: String 546 + 547 + """Grouped field value""" 548 + cid: String 549 + 550 + """Grouped field value""" 551 + did: String 552 + 553 + """Grouped field value""" 554 + collection: String 555 + 556 + """Grouped field value""" 557 + indexed_at: String 558 + 559 + """Grouped field value""" 560 + artistMbIds: String 561 + 562 + """Grouped field value""" 563 + artistNames: String 564 + 565 + """Grouped field value""" 566 + artists: String 567 + 568 + """Grouped field value""" 569 + duration: String 570 + 571 + """Grouped field value""" 572 + isrc: String 573 + 574 + """Grouped field value""" 575 + musicServiceBaseDomain: String 576 + 577 + """Grouped field value""" 578 + originUrl: String 579 + 580 + """Grouped field value""" 581 + playedTime: String 582 + 583 + """Grouped field value""" 584 + recordingMbId: String 585 + 586 + """Grouped field value""" 587 + releaseMbId: String 588 + 589 + """Grouped field value""" 590 + releaseName: String 591 + 592 + """Grouped field value""" 593 + submissionClientAgent: String 594 + 595 + """Grouped field value""" 596 + trackMbId: String 597 + 598 + """Grouped field value""" 599 + trackName: String 600 + 601 + """Count of records in this group""" 450 602 count: Int! 451 603 } 452 604 605 + """A connection to a list of items for FmTealAlphaFeedPlay""" 453 606 type FmTealAlphaFeedPlayConnection { 454 - totalCount: Int! 455 - pageInfo: PageInfo! 607 + """A list of edges""" 456 608 edges: [FmTealAlphaFeedPlayEdge!]! 457 - nodes: [FmTealAlphaFeedPlay!]! 609 + 610 + """Information to aid in pagination""" 611 + pageInfo: PageInfo! 612 + 613 + """Total number of items in the connection""" 614 + totalCount: Int 458 615 } 459 616 617 + """An edge in a connection for FmTealAlphaFeedPlay""" 460 618 type FmTealAlphaFeedPlayEdge { 619 + """The item at the end of the edge""" 461 620 node: FmTealAlphaFeedPlay! 621 + 622 + """A cursor for use in pagination""" 462 623 cursor: String! 463 624 } 464 625 465 - scalar JSON 626 + """Filter operators for FmTealAlphaFeedPlay fields""" 627 + input FmTealAlphaFeedPlayFieldCondition { 628 + """Exact match (equals)""" 629 + eq: String 630 + 631 + """Match any value in the list""" 632 + in: [String!] 633 + 634 + """Case-insensitive substring match (string fields only)""" 635 + contains: String 636 + 637 + """Greater than""" 638 + gt: String 639 + 640 + """Greater than or equal to""" 641 + gte: String 642 + 643 + """Less than""" 644 + lt: String 645 + 646 + """Less than or equal to""" 647 + lte: String 648 + } 649 + 650 + """Available groupBy fields for FmTealAlphaFeedPlay""" 651 + enum FmTealAlphaFeedPlayGroupByField { 652 + """Group by uri""" 653 + uri 654 + 655 + """Group by cid""" 656 + cid 657 + 658 + """Group by did""" 659 + did 660 + 661 + """Group by collection""" 662 + collection 663 + 664 + """Group by indexedAt""" 665 + indexedAt 666 + 667 + """Group by actorHandle""" 668 + actorHandle 669 + 670 + """Group by artistMbIds""" 671 + artistMbIds 672 + 673 + """Group by artistNames""" 674 + artistNames 675 + 676 + """Group by artists""" 677 + artists 678 + 679 + """Group by duration""" 680 + duration 681 + 682 + """Group by isrc""" 683 + isrc 684 + 685 + """Group by musicServiceBaseDomain""" 686 + musicServiceBaseDomain 687 + 688 + """Group by originUrl""" 689 + originUrl 690 + 691 + """Group by playedTime""" 692 + playedTime 693 + 694 + """Group by recordingMbId""" 695 + recordingMbId 696 + 697 + """Group by releaseMbId""" 698 + releaseMbId 699 + 700 + """Group by releaseName""" 701 + releaseName 702 + 703 + """Group by submissionClientAgent""" 704 + submissionClientAgent 705 + 706 + """Group by trackMbId""" 707 + trackMbId 708 + 709 + """Group by trackName""" 710 + trackName 711 + } 712 + 713 + """Specifies a field to group by with optional date truncation""" 714 + input FmTealAlphaFeedPlayGroupByFieldInput { 715 + """Field name to group by""" 716 + field: FmTealAlphaFeedPlayGroupByField! 717 + 718 + """Date truncation interval (for datetime fields)""" 719 + interval: DateInterval 720 + } 721 + 722 + """Input type for FmTealAlphaFeedPlayInput""" 723 + input FmTealAlphaFeedPlayInput { 724 + """Input field for artistMbIds""" 725 + artistMbIds: String 726 + 727 + """Input field for artistNames""" 728 + artistNames: String 729 + 730 + """Input field for artists""" 731 + artists: String 732 + 733 + """Input field for duration""" 734 + duration: Int 735 + 736 + """Input field for isrc""" 737 + isrc: String 738 + 739 + """Input field for musicServiceBaseDomain""" 740 + musicServiceBaseDomain: String 741 + 742 + """Input field for originUrl""" 743 + originUrl: String 744 + 745 + """Input field for playedTime""" 746 + playedTime: String 747 + 748 + """Input field for recordingMbId""" 749 + recordingMbId: String 750 + 751 + """Input field for releaseMbId""" 752 + releaseMbId: String 753 + 754 + """Input field for releaseName""" 755 + releaseName: String 756 + 757 + """Input field for submissionClientAgent""" 758 + submissionClientAgent: String 759 + 760 + """Input field for trackMbId""" 761 + trackMbId: String 762 + 763 + """Input field for trackName""" 764 + trackName: String! 765 + } 766 + 767 + """Available sort fields for FmTealAlphaFeedPlay""" 768 + enum FmTealAlphaFeedPlaySortField { 769 + """Sort by uri""" 770 + uri 771 + 772 + """Sort by cid""" 773 + cid 774 + 775 + """Sort by did""" 776 + did 777 + 778 + """Sort by collection""" 779 + collection 780 + 781 + """Sort by indexedAt""" 782 + indexedAt 783 + 784 + """Sort by duration""" 785 + duration 786 + 787 + """Sort by isrc""" 788 + isrc 789 + 790 + """Sort by musicServiceBaseDomain""" 791 + musicServiceBaseDomain 792 + 793 + """Sort by originUrl""" 794 + originUrl 795 + 796 + """Sort by playedTime""" 797 + playedTime 798 + 799 + """Sort by recordingMbId""" 800 + recordingMbId 801 + 802 + """Sort by releaseMbId""" 803 + releaseMbId 804 + 805 + """Sort by releaseName""" 806 + releaseName 807 + 808 + """Sort by submissionClientAgent""" 809 + submissionClientAgent 810 + 811 + """Sort by trackMbId""" 812 + trackMbId 813 + 814 + """Sort by trackName""" 815 + trackName 816 + } 817 + 818 + """Specifies a field to sort by and its direction for FmTealAlphaFeedPlay""" 819 + input FmTealAlphaFeedPlaySortFieldInput { 820 + """Field to sort by""" 821 + field: FmTealAlphaFeedPlaySortField! 822 + 823 + """Sort direction (ASC or DESC)""" 824 + direction: SortDirection! 825 + } 826 + 827 + """Filter conditions for FmTealAlphaFeedPlay with nested AND/OR support""" 828 + input FmTealAlphaFeedPlayWhereInput { 829 + """Filter by uri""" 830 + uri: FmTealAlphaFeedPlayFieldCondition 831 + 832 + """Filter by cid""" 833 + cid: FmTealAlphaFeedPlayFieldCondition 834 + 835 + """Filter by did""" 836 + did: FmTealAlphaFeedPlayFieldCondition 837 + 838 + """Filter by collection""" 839 + collection: FmTealAlphaFeedPlayFieldCondition 840 + 841 + """Filter by indexedAt""" 842 + indexedAt: FmTealAlphaFeedPlayFieldCondition 843 + 844 + """Filter by actorHandle""" 845 + actorHandle: FmTealAlphaFeedPlayFieldCondition 846 + 847 + """Filter by duration""" 848 + duration: FmTealAlphaFeedPlayFieldCondition 849 + 850 + """Filter by isrc""" 851 + isrc: FmTealAlphaFeedPlayFieldCondition 852 + 853 + """Filter by musicServiceBaseDomain""" 854 + musicServiceBaseDomain: FmTealAlphaFeedPlayFieldCondition 855 + 856 + """Filter by originUrl""" 857 + originUrl: FmTealAlphaFeedPlayFieldCondition 858 + 859 + """Filter by playedTime""" 860 + playedTime: FmTealAlphaFeedPlayFieldCondition 861 + 862 + """Filter by recordingMbId""" 863 + recordingMbId: FmTealAlphaFeedPlayFieldCondition 864 + 865 + """Filter by releaseMbId""" 866 + releaseMbId: FmTealAlphaFeedPlayFieldCondition 867 + 868 + """Filter by releaseName""" 869 + releaseName: FmTealAlphaFeedPlayFieldCondition 870 + 871 + """Filter by submissionClientAgent""" 872 + submissionClientAgent: FmTealAlphaFeedPlayFieldCondition 873 + 874 + """Filter by trackMbId""" 875 + trackMbId: FmTealAlphaFeedPlayFieldCondition 876 + 877 + """Filter by trackName""" 878 + trackName: FmTealAlphaFeedPlayFieldCondition 879 + 880 + """All conditions must match (AND logic)""" 881 + and: [FmTealAlphaFeedPlayWhereInput!] 882 + 883 + """Any condition must match (OR logic)""" 884 + or: [FmTealAlphaFeedPlayWhereInput!] 885 + } 466 886 887 + """Root mutation type""" 467 888 type Mutation { 468 - """Sync user collections for a given DID""" 469 - syncUserCollections(did: String!): SyncResult! 889 + """Upload a blob to the PDS""" 890 + uploadBlob( 891 + """Base64 encoded blob data""" 892 + data: String! 893 + 894 + """MIME type of the blob""" 895 + mimeType: String! 896 + ): BlobUploadResponse! 897 + 898 + """Create a new app.bsky.actor.profile record""" 899 + createAppBskyActorProfile( 900 + """Record data""" 901 + input: AppBskyActorProfileInput! 902 + 903 + """Optional record key (defaults to TID)""" 904 + rkey: String 905 + ): AppBskyActorProfile 906 + 907 + """Update an existing app.bsky.actor.profile record""" 908 + updateAppBskyActorProfile( 909 + """Record key to update""" 910 + rkey: String! 911 + 912 + """Updated record data""" 913 + input: AppBskyActorProfileInput! 914 + ): AppBskyActorProfile 915 + 916 + """Delete a app.bsky.actor.profile record""" 917 + deleteAppBskyActorProfile( 918 + """Record key to delete""" 919 + rkey: String! 920 + ): DeleteResult 921 + 922 + """Create a new fm.teal.alpha.feed.play record""" 923 + createFmTealAlphaFeedPlay( 924 + """Record data""" 925 + input: FmTealAlphaFeedPlayInput! 926 + 927 + """Optional record key (defaults to TID)""" 928 + rkey: String 929 + ): FmTealAlphaFeedPlay 930 + 931 + """Update an existing fm.teal.alpha.feed.play record""" 932 + updateFmTealAlphaFeedPlay( 933 + """Record key to update""" 934 + rkey: String! 935 + 936 + """Updated record data""" 937 + input: FmTealAlphaFeedPlayInput! 938 + ): FmTealAlphaFeedPlay 939 + 940 + """Delete a fm.teal.alpha.feed.play record""" 941 + deleteFmTealAlphaFeedPlay( 942 + """Record key to delete""" 943 + rkey: String! 944 + ): DeleteResult 470 945 } 471 946 947 + """Information about pagination in a connection""" 472 948 type PageInfo { 949 + """When paginating forwards, are there more items?""" 473 950 hasNextPage: Boolean! 951 + 952 + """When paginating backwards, are there more items?""" 474 953 hasPreviousPage: Boolean! 954 + 955 + """Cursor corresponding to the first item in the page""" 475 956 startCursor: String 957 + 958 + """Cursor corresponding to the last item in the page""" 476 959 endCursor: String 477 960 } 478 961 962 + """Root query type""" 479 963 type Query { 480 - """Query app.bsky.embed.record records""" 481 - appBskyEmbedRecords(first: Int, after: String, last: Int, before: String, sortBy: [SortField], where: JSON): AppBskyEmbedRecordConnection! 964 + """Query app.bsky.actor.profile with cursor pagination and sorting""" 965 + appBskyActorProfile( 966 + """Returns the first n items from the list""" 967 + first: Int 482 968 483 - """ 484 - Aggregated query for app.bsky.embed.record records with GROUP BY support 485 - """ 486 - appBskyEmbedRecordsAggregated(groupBy: [String!]!, where: JSON, orderBy: AggregationOrderBy, limit: Int): [AppBskyEmbedRecordAggregated!]! 969 + """Returns items after the given cursor""" 970 + after: String 487 971 488 - """Query app.bsky.embed.images records""" 489 - appBskyEmbedImageses(first: Int, after: String, last: Int, before: String, sortBy: [SortField], where: JSON): AppBskyEmbedImagesConnection! 972 + """Returns the last n items from the list""" 973 + last: Int 490 974 491 - """ 492 - Aggregated query for app.bsky.embed.images records with GROUP BY support 493 - """ 494 - appBskyEmbedImagesesAggregated(groupBy: [String!]!, where: JSON, orderBy: AggregationOrderBy, limit: Int): [AppBskyEmbedImagesAggregated!]! 975 + """Returns items before the given cursor""" 976 + before: String 495 977 496 - """Query app.bsky.embed.video records""" 497 - appBskyEmbedVideos(first: Int, after: String, last: Int, before: String, sortBy: [SortField], where: JSON): AppBskyEmbedVideoConnection! 978 + """Sort order for the connection""" 979 + sortBy: [AppBskyActorProfileSortFieldInput!] 498 980 499 - """ 500 - Aggregated query for app.bsky.embed.video records with GROUP BY support 501 - """ 502 - appBskyEmbedVideosAggregated(groupBy: [String!]!, where: JSON, orderBy: AggregationOrderBy, limit: Int): [AppBskyEmbedVideoAggregated!]! 981 + """Filter conditions for the query""" 982 + where: AppBskyActorProfileWhereInput 983 + ): AppBskyActorProfileConnection 503 984 504 - """Query app.bsky.embed.recordWithMedia records""" 505 - appBskyEmbedRecordWithMedias(first: Int, after: String, last: Int, before: String, sortBy: [SortField], where: JSON): AppBskyEmbedRecordWithMediaConnection! 985 + """Query fm.teal.alpha.feed.play with cursor pagination and sorting""" 986 + fmTealAlphaFeedPlay( 987 + """Returns the first n items from the list""" 988 + first: Int 506 989 507 - """ 508 - Aggregated query for app.bsky.embed.recordWithMedia records with GROUP BY support 509 - """ 510 - appBskyEmbedRecordWithMediasAggregated(groupBy: [String!]!, where: JSON, orderBy: AggregationOrderBy, limit: Int): [AppBskyEmbedRecordWithMediaAggregated!]! 990 + """Returns items after the given cursor""" 991 + after: String 511 992 512 - """Query app.bsky.embed.external records""" 513 - appBskyEmbedExternals(first: Int, after: String, last: Int, before: String, sortBy: [SortField], where: JSON): AppBskyEmbedExternalConnection! 514 - 515 - """ 516 - Aggregated query for app.bsky.embed.external records with GROUP BY support 517 - """ 518 - appBskyEmbedExternalsAggregated(groupBy: [String!]!, where: JSON, orderBy: AggregationOrderBy, limit: Int): [AppBskyEmbedExternalAggregated!]! 519 - 520 - """Query app.bsky.feed.postgate records""" 521 - appBskyFeedPostgates(first: Int, after: String, last: Int, before: String, sortBy: [SortField], where: JSON): AppBskyFeedPostgateConnection! 993 + """Returns the last n items from the list""" 994 + last: Int 522 995 523 - """ 524 - Aggregated query for app.bsky.feed.postgate records with GROUP BY support 525 - """ 526 - appBskyFeedPostgatesAggregated(groupBy: [String!]!, where: JSON, orderBy: AggregationOrderBy, limit: Int): [AppBskyFeedPostgateAggregated!]! 996 + """Returns items before the given cursor""" 997 + before: String 527 998 528 - """Query app.bsky.feed.threadgate records""" 529 - appBskyFeedThreadgates(first: Int, after: String, last: Int, before: String, sortBy: [SortField], where: JSON): AppBskyFeedThreadgateConnection! 999 + """Sort order for the connection""" 1000 + sortBy: [FmTealAlphaFeedPlaySortFieldInput!] 530 1001 531 - """ 532 - Aggregated query for app.bsky.feed.threadgate records with GROUP BY support 533 - """ 534 - appBskyFeedThreadgatesAggregated(groupBy: [String!]!, where: JSON, orderBy: AggregationOrderBy, limit: Int): [AppBskyFeedThreadgateAggregated!]! 1002 + """Filter conditions for the query""" 1003 + where: FmTealAlphaFeedPlayWhereInput 1004 + ): FmTealAlphaFeedPlayConnection 535 1005 536 - """Query app.bsky.richtext.facet records""" 537 - appBskyRichtextFacets(first: Int, after: String, last: Int, before: String, sortBy: [SortField], where: JSON): AppBskyRichtextFacetConnection! 1006 + """Aggregated query for app.bsky.actor.profile""" 1007 + appBskyActorProfileAggregated( 1008 + """Fields to group by (required)""" 1009 + groupBy: [AppBskyActorProfileGroupByFieldInput!] 538 1010 539 - """ 540 - Aggregated query for app.bsky.richtext.facet records with GROUP BY support 541 - """ 542 - appBskyRichtextFacetsAggregated(groupBy: [String!]!, where: JSON, orderBy: AggregationOrderBy, limit: Int): [AppBskyRichtextFacetAggregated!]! 1011 + """Filter records before aggregation""" 1012 + where: AppBskyActorProfileWhereInput 543 1013 544 - """Query app.bsky.actor.profile records""" 545 - appBskyActorProfiles(first: Int, after: String, last: Int, before: String, sortBy: [SortField], where: JSON): AppBskyActorProfileConnection! 1014 + """Order by count (default: desc)""" 1015 + orderBy: AggregationOrderBy 546 1016 547 - """ 548 - Aggregated query for app.bsky.actor.profile records with GROUP BY support 549 - """ 550 - appBskyActorProfilesAggregated(groupBy: [String!]!, where: JSON, orderBy: AggregationOrderBy, limit: Int): [AppBskyActorProfileAggregated!]! 1017 + """Maximum number of results (default 50, max 1000)""" 1018 + limit: Int 1019 + ): [AppBskyActorProfileAggregated!] 551 1020 552 - """Query com.atproto.repo.strongRef records""" 553 - comAtprotoRepoStrongRefs(first: Int, after: String, last: Int, before: String, sortBy: [SortField], where: JSON): ComAtprotoRepoStrongRefConnection! 1021 + """Aggregated query for fm.teal.alpha.feed.play""" 1022 + fmTealAlphaFeedPlayAggregated( 1023 + """Fields to group by (required)""" 1024 + groupBy: [FmTealAlphaFeedPlayGroupByFieldInput!] 554 1025 555 - """ 556 - Aggregated query for com.atproto.repo.strongRef records with GROUP BY support 557 - """ 558 - comAtprotoRepoStrongRefsAggregated(groupBy: [String!]!, where: JSON, orderBy: AggregationOrderBy, limit: Int): [ComAtprotoRepoStrongRefAggregated!]! 1026 + """Filter records before aggregation""" 1027 + where: FmTealAlphaFeedPlayWhereInput 559 1028 560 - """Query fm.teal.alpha.feed.play records""" 561 - fmTealAlphaFeedPlays(first: Int, after: String, last: Int, before: String, sortBy: [SortField], where: JSON): FmTealAlphaFeedPlayConnection! 1029 + """Order by count (default: desc)""" 1030 + orderBy: AggregationOrderBy 562 1031 563 - """ 564 - Aggregated query for fm.teal.alpha.feed.play records with GROUP BY support 565 - """ 566 - fmTealAlphaFeedPlaysAggregated(groupBy: [String!]!, where: JSON, orderBy: AggregationOrderBy, limit: Int): [FmTealAlphaFeedPlayAggregated!]! 1032 + """Maximum number of results (default 50, max 1000)""" 1033 + limit: Int 1034 + ): [FmTealAlphaFeedPlayAggregated!] 567 1035 } 568 1036 1037 + union Record = AppBskyActorProfile | FmTealAlphaFeedPlay 1038 + 1039 + """Sort direction for query results""" 569 1040 enum SortDirection { 570 - asc 571 - desc 572 - } 1041 + """Ascending order""" 1042 + ASC 573 1043 574 - input SortField { 575 - field: String! 576 - direction: SortDirection! 1044 + """Descending order""" 1045 + DESC 577 1046 } 578 1047 1048 + """GraphQL subscription root""" 579 1049 type Subscription { 580 - """Subscribe to app.bsky.feed.postgate record creation events""" 581 - appBskyFeedPostgateCreated: AppBskyFeedPostgate! 582 - 583 - """Subscribe to app.bsky.feed.postgate record update events""" 584 - appBskyFeedPostgateUpdated: AppBskyFeedPostgate! 585 - 586 - """ 587 - Subscribe to app.bsky.feed.postgate record deletion events. Returns the URI of deleted records. 588 - """ 589 - appBskyFeedPostgateDeleted: String! 590 - 591 - """Subscribe to app.bsky.feed.threadgate record creation events""" 592 - appBskyFeedThreadgateCreated: AppBskyFeedThreadgate! 593 - 594 - """Subscribe to app.bsky.feed.threadgate record update events""" 595 - appBskyFeedThreadgateUpdated: AppBskyFeedThreadgate! 596 - 597 - """ 598 - Subscribe to app.bsky.feed.threadgate record deletion events. Returns the URI of deleted records. 599 - """ 600 - appBskyFeedThreadgateDeleted: String! 601 - 602 - """Subscribe to app.bsky.actor.profile record creation events""" 1050 + """Emitted when a new app.bsky.actor.profile record is created""" 603 1051 appBskyActorProfileCreated: AppBskyActorProfile! 604 1052 605 - """Subscribe to app.bsky.actor.profile record update events""" 1053 + """Emitted when a app.bsky.actor.profile record is updated""" 606 1054 appBskyActorProfileUpdated: AppBskyActorProfile! 607 1055 608 - """ 609 - Subscribe to app.bsky.actor.profile record deletion events. Returns the URI of deleted records. 610 - """ 611 - appBskyActorProfileDeleted: String! 1056 + """Emitted when a app.bsky.actor.profile record is deleted""" 1057 + appBskyActorProfileDeleted: AppBskyActorProfile! 612 1058 613 - """Subscribe to fm.teal.alpha.feed.play record creation events""" 1059 + """Emitted when a new fm.teal.alpha.feed.play record is created""" 614 1060 fmTealAlphaFeedPlayCreated: FmTealAlphaFeedPlay! 615 1061 616 - """Subscribe to fm.teal.alpha.feed.play record update events""" 1062 + """Emitted when a fm.teal.alpha.feed.play record is updated""" 617 1063 fmTealAlphaFeedPlayUpdated: FmTealAlphaFeedPlay! 618 1064 619 - """ 620 - Subscribe to fm.teal.alpha.feed.play record deletion events. Returns the URI of deleted records. 621 - """ 622 - fmTealAlphaFeedPlayDeleted: String! 623 - } 624 - 625 - type SyncResult { 626 - success: Boolean! 627 - reposProcessed: Int! 628 - recordsSynced: Int! 629 - timedOut: Boolean! 630 - message: String! 1065 + """Emitted when a fm.teal.alpha.feed.play record is deleted""" 1066 + fmTealAlphaFeedPlayDeleted: FmTealAlphaFeedPlay! 631 1067 } 632 1068
+4 -1
src/AlbumItem.tsx
··· 1 1 import AlbumArt from "./AlbumArt"; 2 + import MusicBrainzLink from "./MusicBrainzLink"; 2 3 3 4 interface Artist { 4 5 artistName: string; ··· 58 59 59 60 <div className="flex-1 min-w-0"> 60 61 <h3 className="text-sm font-medium text-zinc-100 truncate"> 61 - {releaseName} 62 + <MusicBrainzLink releaseMbId={releaseMbId}> 63 + {releaseName} 64 + </MusicBrainzLink> 62 65 </h3> 63 66 <p className="text-xs text-zinc-500 truncate">{artistNames}</p> 64 67 </div>
+101 -56
src/App.tsx
··· 4 4 usePaginationFragment, 5 5 useSubscription, 6 6 } from "react-relay"; 7 - import { useEffect, useRef } from "react"; 7 + import { useEffect, useMemo, useRef } from "react"; 8 8 import type { AppQuery } from "./__generated__/AppQuery.graphql"; 9 9 import type { App_plays$key } from "./__generated__/App_plays.graphql"; 10 10 import type { AppSubscription } from "./__generated__/AppSubscription.graphql"; 11 11 import TrackItem from "./TrackItem"; 12 12 import Layout from "./Layout"; 13 + import ScrobbleChart from "./ScrobbleChart"; 13 14 import { 14 15 ConnectionHandler, 15 16 type GraphQLSubscriptionConfig, 16 17 } from "relay-runtime"; 17 18 18 19 export default function App() { 20 + const queryVariables = useMemo(() => { 21 + // Round to start of day to keep timestamp stable 22 + const now = new Date(); 23 + now.setHours(0, 0, 0, 0); 24 + const ninetyDaysAgo = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000); 25 + 26 + return { 27 + chartWhere: { 28 + playedTime: { 29 + gte: ninetyDaysAgo.toISOString(), 30 + }, 31 + }, 32 + }; 33 + }, []); 34 + 19 35 const queryData = useLazyLoadQuery<AppQuery>( 20 36 graphql` 21 - query AppQuery { 37 + query AppQuery($chartWhere: FmTealAlphaFeedPlayWhereInput!) { 22 38 ...App_plays 39 + ...ScrobbleChart_data 23 40 } 24 41 `, 25 - {} 42 + queryVariables, 26 43 ); 27 44 28 45 const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment< ··· 36 53 cursor: { type: "String" } 37 54 count: { type: "Int", defaultValue: 20 } 38 55 ) { 39 - fmTealAlphaFeedPlays( 56 + fmTealAlphaFeedPlay( 40 57 first: $count 41 58 after: $cursor 42 - sortBy: [{ field: "playedTime", direction: desc }] 43 - ) @connection(key: "App_fmTealAlphaFeedPlays", filters: ["sortBy"]) { 59 + sortBy: [{ field: playedTime, direction: DESC }] 60 + ) @connection(key: "App_fmTealAlphaFeedPlay", filters: ["sortBy"]) { 44 61 totalCount 45 62 edges { 46 63 node { ··· 51 68 } 52 69 } 53 70 `, 54 - queryData 71 + queryData, 55 72 ); 56 73 57 74 const loadMoreRef = useRef<HTMLDivElement>(null); 75 + const loadNextRef = useRef(loadNext); 76 + const isLoadingRef = useRef(false); 77 + loadNextRef.current = loadNext; 58 78 59 79 // Subscribe to new plays 60 - const subscriptionConfig: GraphQLSubscriptionConfig<AppSubscription> = { 61 - subscription: graphql` 80 + const subscriptionConfig: GraphQLSubscriptionConfig<AppSubscription> = 81 + useMemo(() => ({ 82 + subscription: graphql` 62 83 subscription AppSubscription { 63 84 fmTealAlphaFeedPlayCreated { 64 85 uri ··· 67 88 } 68 89 } 69 90 `, 70 - variables: {}, 71 - updater: (store) => { 72 - const newPlay = store.getRootField("fmTealAlphaFeedPlayCreated"); 73 - if (!newPlay) return; 91 + variables: {}, 92 + updater: (store) => { 93 + const newPlay = store.getRootField("fmTealAlphaFeedPlayCreated"); 94 + if (!newPlay) return; 74 95 75 - const root = store.getRoot(); 76 - const connection = ConnectionHandler.getConnection( 77 - root, 78 - "App_fmTealAlphaFeedPlays", 79 - { sortBy: [{ field: "playedTime", direction: "desc" }] } 80 - ); 96 + // Only add plays from the last 24 hours 97 + const playedTime = newPlay.getValue("playedTime") as string | null; 98 + if (!playedTime) return; 99 + 100 + const playDate = new Date(playedTime); 101 + const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000); 102 + 103 + if (playDate < cutoff) { 104 + // Play is too old, don't add it to the feed 105 + return; 106 + } 107 + 108 + const root = store.getRoot(); 109 + const connection = ConnectionHandler.getConnection( 110 + root, 111 + "App_fmTealAlphaFeedPlay", 112 + { sortBy: [{ field: "playedTime", direction: "DESC" }] }, 113 + ); 81 114 82 - if (!connection) return; 115 + if (!connection) return; 83 116 84 - const edge = ConnectionHandler.createEdge( 85 - store, 86 - connection, 87 - newPlay, 88 - "FmTealAlphaFeedPlayEdge" 89 - ); 117 + const edge = ConnectionHandler.createEdge( 118 + store, 119 + connection, 120 + newPlay, 121 + "FmTealAlphaFeedPlayEdge", 122 + ); 90 123 91 - ConnectionHandler.insertEdgeBefore(connection, edge); 124 + ConnectionHandler.insertEdgeBefore(connection, edge); 92 125 93 - // Update totalCount 94 - const totalCountRecord = root.getLinkedRecord("fmTealAlphaFeedPlays", { 95 - sortBy: [{ field: "playedTime", direction: "desc" }], 96 - }); 97 - if (totalCountRecord) { 98 - const currentCount = totalCountRecord.getValue("totalCount") as number; 99 - if (typeof currentCount === "number") { 100 - totalCountRecord.setValue(currentCount + 1, "totalCount"); 126 + // Update totalCount 127 + const totalCountRecord = root.getLinkedRecord("fmTealAlphaFeedPlay", { 128 + sortBy: [{ field: "playedTime", direction: "DESC" }], 129 + }); 130 + if (totalCountRecord) { 131 + const currentCount = totalCountRecord.getValue( 132 + "totalCount", 133 + ) as number; 134 + if (typeof currentCount === "number") { 135 + totalCountRecord.setValue(currentCount + 1, "totalCount"); 136 + } 101 137 } 102 - } 103 - }, 104 - }; 138 + }, 139 + }), []); 105 140 106 141 useSubscription(subscriptionConfig); 107 142 ··· 109 144 window.scrollTo(0, 0); 110 145 }, []); 111 146 112 - const plays = 113 - data?.fmTealAlphaFeedPlays?.edges 114 - ?.map((edge) => edge.node) 115 - .filter((n) => n != null) || []; 147 + const plays = data?.fmTealAlphaFeedPlay?.edges 148 + ?.map((edge) => edge.node) 149 + .filter((n) => n != null) || []; 150 + 151 + // Sync the loading ref with isLoadingNext 152 + useEffect(() => { 153 + isLoadingRef.current = isLoadingNext; 154 + }, [isLoadingNext]); 116 155 117 156 useEffect(() => { 118 157 if (!loadMoreRef.current || !hasNext) return; 119 158 159 + const element = loadMoreRef.current; 120 160 const observer = new IntersectionObserver( 121 161 (entries) => { 122 - if (entries[0].isIntersecting && hasNext && !isLoadingNext) { 123 - loadNext(20); 162 + if (entries[0].isIntersecting && !isLoadingRef.current) { 163 + isLoadingRef.current = true; 164 + loadNextRef.current(20); 124 165 } 125 166 }, 126 - { threshold: 0.1 } 167 + { threshold: 0.1 }, 127 168 ); 128 169 129 - observer.observe(loadMoreRef.current); 170 + observer.observe(element); 130 171 131 172 return () => observer.disconnect(); 132 - }, [hasNext, isLoadingNext, loadNext]); 173 + }, [hasNext]); 133 174 134 175 // Group plays by date 135 176 const groupedPlays: { date: string; plays: typeof plays }[] = []; ··· 154 195 }); 155 196 156 197 return ( 157 - <Layout> 198 + <Layout headerChart={<ScrobbleChart queryRef={queryData} />}> 158 199 <div className="mb-8"> 159 200 <p className="text-xs text-zinc-500 uppercase tracking-wider"> 160 - {data?.fmTealAlphaFeedPlays?.totalCount?.toLocaleString()} scrobbles 201 + {data?.fmTealAlphaFeedPlay?.totalCount?.toLocaleString()} scrobbles 161 202 </p> 162 203 </div> 163 204 ··· 178 219 179 220 {hasNext && ( 180 221 <div ref={loadMoreRef} className="py-12 text-center"> 181 - {isLoadingNext ? ( 182 - <p className="text-xs text-zinc-600 uppercase tracking-wider"> 183 - Loading... 184 - </p> 185 - ) : ( 186 - <p className="text-xs text-zinc-700 uppercase tracking-wider">ยท</p> 187 - )} 222 + {isLoadingNext 223 + ? ( 224 + <p className="text-xs text-zinc-600 uppercase tracking-wider"> 225 + Loading... 226 + </p> 227 + ) 228 + : ( 229 + <p className="text-xs text-zinc-700 uppercase tracking-wider"> 230 + ยท 231 + </p> 232 + )} 188 233 </div> 189 234 )} 190 235 </Layout>
+122 -20
src/Layout.tsx
··· 2 2 3 3 interface LayoutProps { 4 4 children: React.ReactNode; 5 + headerChart?: React.ReactNode; 5 6 } 6 7 7 - export default function Layout({ children }: LayoutProps) { 8 + export default function Layout({ children, headerChart }: LayoutProps) { 8 9 const location = useLocation(); 10 + const isTracksPage = location.pathname.startsWith("/tracks"); 11 + const isAlbumsPage = location.pathname.startsWith("/albums"); 9 12 10 13 return ( 11 14 <div className="min-h-screen bg-zinc-950 text-zinc-300 font-mono"> 12 15 <div className="max-w-4xl mx-auto px-6 py-12"> 13 - <div className="mb-12 flex items-end justify-between border-b border-zinc-800 pb-6"> 14 - <div> 15 - <h1 className="text-xs font-medium uppercase tracking-wider text-zinc-500">Listening History</h1> 16 - <p className="text-xs text-zinc-600 mt-1">fm.teal.alpha.feed.play</p> 16 + <div className="mb-4 border-b border-zinc-800 pb-4 relative"> 17 + {headerChart && ( 18 + <div className="absolute inset-0 pointer-events-none opacity-40"> 19 + {headerChart} 20 + </div> 21 + )} 22 + <div className="flex items-end justify-between relative"> 23 + <div> 24 + <h1 className="text-xs font-medium uppercase tracking-wider text-zinc-500">Listening History</h1> 25 + <p className="text-xs text-zinc-600 mt-1">fm.teal.alpha.feed.play</p> 26 + </div> 27 + 28 + <div className="flex gap-4 text-xs"> 29 + <Link 30 + to="/" 31 + className={`px-2 py-1 transition-colors ${ 32 + location.pathname === "/" 33 + ? "text-zinc-400" 34 + : "text-zinc-500 hover:text-zinc-300" 35 + }`} 36 + > 37 + Recent 38 + </Link> 39 + <Link 40 + to="/tracks" 41 + className={`px-2 py-1 transition-colors ${ 42 + isTracksPage 43 + ? "text-zinc-400" 44 + : "text-zinc-500 hover:text-zinc-300" 45 + }`} 46 + > 47 + Top Tracks 48 + </Link> 49 + <Link 50 + to="/albums" 51 + className={`px-2 py-1 transition-colors ${ 52 + isAlbumsPage 53 + ? "text-zinc-400" 54 + : "text-zinc-500 hover:text-zinc-300" 55 + }`} 56 + > 57 + Top Albums 58 + </Link> 59 + </div> 17 60 </div> 61 + </div> 18 62 19 - <div className="flex gap-4 text-xs"> 63 + {isTracksPage && ( 64 + <div className="flex gap-3 text-xs mb-8 pb-4 border-b border-zinc-800"> 20 65 <Link 21 - to="/" 66 + to="/tracks" 22 67 className={`px-2 py-1 transition-colors ${ 23 - location.pathname === "/" 24 - ? "text-zinc-400" 25 - : "text-zinc-500 hover:text-zinc-300" 68 + location.pathname === "/tracks" 69 + ? "text-zinc-300" 70 + : "text-zinc-600 hover:text-zinc-400" 26 71 }`} 27 72 > 28 - Recent 73 + All Time 29 74 </Link> 30 75 <Link 31 - to="/tracks" 76 + to="/tracks/daily" 32 77 className={`px-2 py-1 transition-colors ${ 33 - location.pathname === "/tracks" 34 - ? "text-zinc-400" 35 - : "text-zinc-500 hover:text-zinc-300" 78 + location.pathname === "/tracks/daily" 79 + ? "text-zinc-300" 80 + : "text-zinc-600 hover:text-zinc-400" 36 81 }`} 37 82 > 38 - Top Tracks 83 + Daily 39 84 </Link> 40 85 <Link 86 + to="/tracks/weekly" 87 + className={`px-2 py-1 transition-colors ${ 88 + location.pathname === "/tracks/weekly" 89 + ? "text-zinc-300" 90 + : "text-zinc-600 hover:text-zinc-400" 91 + }`} 92 + > 93 + Weekly 94 + </Link> 95 + <Link 96 + to="/tracks/monthly" 97 + className={`px-2 py-1 transition-colors ${ 98 + location.pathname === "/tracks/monthly" 99 + ? "text-zinc-300" 100 + : "text-zinc-600 hover:text-zinc-400" 101 + }`} 102 + > 103 + Monthly 104 + </Link> 105 + </div> 106 + )} 107 + 108 + {isAlbumsPage && ( 109 + <div className="flex gap-3 text-xs mb-8 pb-4 border-b border-zinc-800"> 110 + <Link 41 111 to="/albums" 42 112 className={`px-2 py-1 transition-colors ${ 43 113 location.pathname === "/albums" 44 - ? "text-zinc-400" 45 - : "text-zinc-500 hover:text-zinc-300" 114 + ? "text-zinc-300" 115 + : "text-zinc-600 hover:text-zinc-400" 116 + }`} 117 + > 118 + All Time 119 + </Link> 120 + <Link 121 + to="/albums/daily" 122 + className={`px-2 py-1 transition-colors ${ 123 + location.pathname === "/albums/daily" 124 + ? "text-zinc-300" 125 + : "text-zinc-600 hover:text-zinc-400" 126 + }`} 127 + > 128 + Daily 129 + </Link> 130 + <Link 131 + to="/albums/weekly" 132 + className={`px-2 py-1 transition-colors ${ 133 + location.pathname === "/albums/weekly" 134 + ? "text-zinc-300" 135 + : "text-zinc-600 hover:text-zinc-400" 136 + }`} 137 + > 138 + Weekly 139 + </Link> 140 + <Link 141 + to="/albums/monthly" 142 + className={`px-2 py-1 transition-colors ${ 143 + location.pathname === "/albums/monthly" 144 + ? "text-zinc-300" 145 + : "text-zinc-600 hover:text-zinc-400" 46 146 }`} 47 147 > 48 - Top Albums 148 + Monthly 49 149 </Link> 50 150 </div> 51 - </div> 151 + )} 152 + 153 + {!isTracksPage && !isAlbumsPage && <div className="mb-8"></div>} 52 154 53 155 {children} 54 156 </div>
+24
src/MusicBrainzLink.tsx
··· 1 + interface MusicBrainzLinkProps { 2 + releaseMbId: string | null | undefined; 3 + children: React.ReactNode; 4 + } 5 + 6 + export default function MusicBrainzLink({ 7 + releaseMbId, 8 + children, 9 + }: MusicBrainzLinkProps) { 10 + if (!releaseMbId) { 11 + return <>{children}</>; 12 + } 13 + 14 + return ( 15 + <a 16 + href={`https://musicbrainz.org/release/${releaseMbId}`} 17 + target="_blank" 18 + rel="noopener noreferrer" 19 + className="hover:text-violet-400 transition-colors" 20 + > 21 + {children} 22 + </a> 23 + ); 24 + }
+83 -44
src/Profile.tsx
··· 1 1 import { graphql, useLazyLoadQuery, usePaginationFragment } from "react-relay"; 2 - import { useParams, Link } from "react-router-dom"; 3 - import { useEffect, useRef } from "react"; 2 + import { Link, useParams } from "react-router-dom"; 3 + import { useEffect, useMemo, useRef } from "react"; 4 4 import type { ProfileQuery as ProfileQueryType } from "./__generated__/ProfileQuery.graphql"; 5 5 import type { Profile_plays$key } from "./__generated__/Profile_plays.graphql"; 6 6 import TrackItem from "./TrackItem"; 7 + import ScrobbleChart from "./ScrobbleChart"; 7 8 8 9 export default function Profile() { 9 10 const { handle } = useParams<{ handle: string }>(); 10 11 12 + const queryVariables = useMemo(() => { 13 + // Round to start of day to keep timestamp stable 14 + const now = new Date(); 15 + now.setHours(0, 0, 0, 0); 16 + const ninetyDaysAgo = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000); 17 + 18 + return { 19 + where: { actorHandle: { eq: handle } }, 20 + chartWhere: { 21 + actorHandle: { eq: handle }, 22 + playedTime: { 23 + gte: ninetyDaysAgo.toISOString(), 24 + }, 25 + }, 26 + }; 27 + }, [handle]); 28 + 11 29 const queryData = useLazyLoadQuery<ProfileQueryType>( 12 30 graphql` 13 - query ProfileQuery($where: JSON!) { 31 + query ProfileQuery($where: FmTealAlphaFeedPlayWhereInput!, $chartWhere: FmTealAlphaFeedPlayWhereInput!) { 14 32 ...Profile_plays @arguments(where: $where) 33 + ...ScrobbleChart_data 15 34 } 16 35 `, 17 - { 18 - where: { actorHandle: { eq: handle } }, 19 - } 36 + queryVariables, 20 37 ); 21 38 22 39 const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment< ··· 29 46 @argumentDefinitions( 30 47 cursor: { type: "String" } 31 48 count: { type: "Int", defaultValue: 20 } 32 - where: { type: "JSON!" } 49 + where: { type: "FmTealAlphaFeedPlayWhereInput!" } 33 50 ) { 34 - fmTealAlphaFeedPlays( 51 + fmTealAlphaFeedPlay( 35 52 first: $count 36 53 after: $cursor 37 - sortBy: [{ field: "playedTime", direction: desc }] 54 + sortBy: [{ field: playedTime, direction: DESC }] 38 55 where: $where 39 56 ) 40 57 @connection( 41 - key: "Profile_fmTealAlphaFeedPlays" 58 + key: "Profile_fmTealAlphaFeedPlay" 42 59 filters: ["where", "sortBy"] 43 60 ) { 44 61 totalCount ··· 46 63 node { 47 64 ...TrackItem_play 48 65 actorHandle 49 - appBskyActorProfile { 66 + appBskyActorProfileByDid { 50 67 displayName 51 68 description 52 69 avatar { ··· 58 75 } 59 76 } 60 77 `, 61 - queryData 78 + queryData, 62 79 ); 63 80 64 81 const loadMoreRef = useRef<HTMLDivElement>(null); 65 82 66 - const plays = data?.fmTealAlphaFeedPlays?.edges?.map((edge) => edge.node).filter((n) => n != null) || []; 67 - const profile = plays?.[0]?.appBskyActorProfile; 83 + const plays = useMemo( 84 + () => 85 + data?.fmTealAlphaFeedPlay?.edges?.map((edge) => edge.node).filter((n) => 86 + n != null 87 + ) || [], 88 + [data?.fmTealAlphaFeedPlay?.edges], 89 + ); 90 + const profile = plays?.[0]?.appBskyActorProfileByDid; 68 91 69 92 useEffect(() => { 70 93 window.scrollTo(0, 0); ··· 79 102 loadNext(20); 80 103 } 81 104 }, 82 - { threshold: 0.1 } 105 + { threshold: 0.1 }, 83 106 ); 84 107 85 108 observer.observe(loadMoreRef.current); ··· 97 120 โ† Back 98 121 </Link> 99 122 100 - <div className="mb-12 flex items-start gap-6 border-b border-zinc-800 pb-6"> 101 - {profile?.avatar?.url && ( 102 - <img 103 - src={profile.avatar.url} 104 - alt={profile.displayName ?? handle ?? "User"} 105 - className="w-16 h-16 flex-shrink-0 object-cover" 106 - /> 107 - )} 108 - <div className="flex-1"> 109 - <h1 className="text-lg font-medium mb-1 text-zinc-100"> 110 - {profile?.displayName ?? handle} 111 - </h1> 112 - <p className="text-xs text-zinc-500 mb-2">@{handle}</p> 113 - {profile?.description && ( 114 - <p className="text-xs text-zinc-400">{profile.description}</p> 123 + <div className="mb-12 border-b border-zinc-800 pb-6 relative"> 124 + <div className="absolute inset-0 pointer-events-none opacity-40"> 125 + <ScrobbleChart queryRef={queryData} /> 126 + </div> 127 + <div className="relative flex items-start gap-6"> 128 + {profile?.avatar?.url && ( 129 + <img 130 + src={profile.avatar.url} 131 + alt={profile.displayName ?? handle ?? "User"} 132 + className="w-16 h-16 flex-shrink-0 object-cover" 133 + /> 115 134 )} 135 + <div className="flex-1"> 136 + <h1 className="text-lg font-medium mb-1 text-zinc-100"> 137 + {profile?.displayName ?? handle} 138 + </h1> 139 + <p className="text-xs text-zinc-500 mb-2">@{handle}</p> 140 + {profile?.description && ( 141 + <p className="text-xs text-zinc-400">{profile.description}</p> 142 + )} 143 + </div> 116 144 </div> 117 145 </div> 118 146 119 147 <div className="mb-8"> 120 - <h2 className="text-sm font-medium uppercase tracking-wider text-zinc-400 mb-2">Recent Tracks</h2> 148 + <h2 className="text-sm font-medium uppercase tracking-wider text-zinc-400 mb-2"> 149 + Recent Tracks 150 + </h2> 121 151 <p className="text-xs text-zinc-500 uppercase tracking-wider"> 122 - {(data?.fmTealAlphaFeedPlays?.totalCount ?? 0).toLocaleString()} scrobbles 152 + {(data?.fmTealAlphaFeedPlay?.totalCount ?? 0).toLocaleString()}{" "} 153 + scrobbles 123 154 </p> 124 155 </div> 125 156 126 157 <div className="space-y-1"> 127 - {plays && plays.length > 0 ? ( 128 - plays.map((play, index) => <TrackItem key={index} play={play} />) 129 - ) : ( 130 - <p className="text-zinc-600 text-center py-8 text-xs uppercase tracking-wider"> 131 - No tracks found for this user 132 - </p> 133 - )} 158 + {plays && plays.length > 0 159 + ? ( 160 + plays.map((play, index) => <TrackItem key={index} play={play} />) 161 + ) 162 + : ( 163 + <p className="text-zinc-600 text-center py-8 text-xs uppercase tracking-wider"> 164 + No tracks found for this user 165 + </p> 166 + )} 134 167 </div> 135 168 136 169 {hasNext && ( 137 170 <div ref={loadMoreRef} className="py-12 text-center"> 138 - {isLoadingNext ? ( 139 - <p className="text-xs text-zinc-600 uppercase tracking-wider">Loading...</p> 140 - ) : ( 141 - <p className="text-xs text-zinc-700 uppercase tracking-wider">ยท</p> 142 - )} 171 + {isLoadingNext 172 + ? ( 173 + <p className="text-xs text-zinc-600 uppercase tracking-wider"> 174 + Loading... 175 + </p> 176 + ) 177 + : ( 178 + <p className="text-xs text-zinc-700 uppercase tracking-wider"> 179 + ยท 180 + </p> 181 + )} 143 182 </div> 144 183 )} 145 184 </div>
+115
src/ScrobbleChart.tsx
··· 1 + import { graphql, useFragment } from "react-relay"; 2 + import { useMemo } from "react"; 3 + import type { ScrobbleChart_data$key } from "./__generated__/ScrobbleChart_data.graphql"; 4 + 5 + interface ScrobbleChartProps { 6 + queryRef: ScrobbleChart_data$key; 7 + } 8 + 9 + export default function ScrobbleChart({ queryRef }: ScrobbleChartProps) { 10 + const data = useFragment( 11 + graphql` 12 + fragment ScrobbleChart_data on Query { 13 + chartData: fmTealAlphaFeedPlayAggregated( 14 + groupBy: [{ field: playedTime, interval: DAY }] 15 + where: $chartWhere 16 + limit: 90 17 + ) { 18 + playedTime 19 + count 20 + } 21 + } 22 + `, 23 + queryRef, 24 + ); 25 + 26 + const chartData = useMemo(() => { 27 + if (!data?.chartData) return []; 28 + 29 + // Convert aggregated data to chart format 30 + const aggregated = data.chartData.map((item) => { 31 + // playedTime comes back as '2025-08-03 00:00:00', extract just the date part 32 + const date = item.playedTime ? item.playedTime.split(" ")[0] : ""; 33 + return { 34 + date, 35 + count: item.count, 36 + }; 37 + }).sort((a, b) => a.date.localeCompare(b.date)); 38 + 39 + // Fill in missing days with zero counts 40 + const now = new Date(); 41 + now.setHours(0, 0, 0, 0); 42 + const filledData = []; 43 + 44 + for (let i = 89; i >= 0; i--) { 45 + const date = new Date(now); 46 + date.setDate(date.getDate() - i); 47 + const dateStr = date.toISOString().split("T")[0]; 48 + 49 + const existing = aggregated.find((d) => d.date === dateStr); 50 + filledData.push({ 51 + date: dateStr, 52 + count: existing ? existing.count : 0, 53 + }); 54 + } 55 + 56 + return filledData; 57 + }, [data?.chartData]); 58 + 59 + if (!chartData || chartData.length === 0) return null; 60 + 61 + const width = 1000; 62 + const height = 100; 63 + const padding = { top: 0, right: 0, bottom: 0, left: 0 }; 64 + const chartWidth = width - padding.left - padding.right; 65 + const chartHeight = height - padding.top - padding.bottom; 66 + 67 + const maxCount = Math.max(...chartData.map((d) => d.count)); 68 + const minCount = Math.min(...chartData.map((d) => d.count)); 69 + const range = maxCount - minCount || 1; 70 + 71 + // Generate points for the line 72 + const points = chartData.map((d, i) => { 73 + const x = padding.left + (i / (chartData.length - 1)) * chartWidth; 74 + const y = padding.top + chartHeight - 75 + ((d.count - minCount) / range) * chartHeight; 76 + return `${x},${y}`; 77 + }).join(" "); 78 + 79 + // Generate area path 80 + const areaPoints = [ 81 + `${padding.left},${padding.top + chartHeight}`, 82 + ...chartData.map((d, i) => { 83 + const x = padding.left + (i / (chartData.length - 1)) * chartWidth; 84 + const y = padding.top + chartHeight - 85 + ((d.count - minCount) / range) * chartHeight; 86 + return `${x},${y}`; 87 + }), 88 + `${padding.left + chartWidth},${padding.top + chartHeight}`, 89 + ].join(" "); 90 + 91 + return ( 92 + <svg 93 + viewBox={`0 0 ${width} ${height}`} 94 + className="w-full h-full" 95 + preserveAspectRatio="none" 96 + > 97 + {/* Area fill */} 98 + <polygon 99 + points={areaPoints} 100 + fill="rgb(139 92 246 / 0.1)" 101 + stroke="none" 102 + /> 103 + 104 + {/* Line */} 105 + <polyline 106 + points={points} 107 + fill="none" 108 + stroke="rgb(139 92 246)" 109 + strokeWidth="1.5" 110 + strokeLinecap="round" 111 + strokeLinejoin="round" 112 + /> 113 + </svg> 114 + ); 115 + }
+13 -6
src/TopAlbums.tsx
··· 1 1 import { graphql, useLazyLoadQuery } from "react-relay"; 2 + import { useParams } from "react-router-dom"; 2 3 import type { TopAlbumsQuery } from "./__generated__/TopAlbumsQuery.graphql"; 3 4 import AlbumItem from "./AlbumItem"; 4 5 import Layout from "./Layout"; 6 + import { useDateRangeFilter } from "./useDateRangeFilter"; 5 7 6 8 export default function TopAlbums() { 9 + const { period } = useParams<{ period?: string }>(); 10 + const queryVariables = useDateRangeFilter(period); 11 + 7 12 const data = useLazyLoadQuery<TopAlbumsQuery>( 8 13 graphql` 9 - query TopAlbumsQuery { 10 - fmTealAlphaFeedPlaysAggregated( 11 - groupBy: ["releaseMbId", "releaseName", "artists"] 12 - orderBy: { count: desc } 14 + query TopAlbumsQuery($where: FmTealAlphaFeedPlayWhereInput) { 15 + fmTealAlphaFeedPlayAggregated( 16 + groupBy: [{ field: releaseMbId }, { field: releaseName }, { field: artists }] 17 + orderBy: { count: DESC } 13 18 limit: 100 19 + where: $where 14 20 ) { 15 21 releaseMbId 16 22 releaseName ··· 19 25 } 20 26 } 21 27 `, 22 - {} 28 + queryVariables, 29 + { fetchKey: period || "all", fetchPolicy: "store-or-network" }, 23 30 ); 24 31 25 - const albums = [...(data.fmTealAlphaFeedPlaysAggregated || [])]; 32 + const albums = [...(data.fmTealAlphaFeedPlayAggregated || [])]; 26 33 27 34 // Deduplicate by release name, keeping the one with highest count 28 35 // Prefer entries with artist data
+4 -1
src/TopTrackItem.tsx
··· 1 1 import AlbumArt from "./AlbumArt"; 2 + import MusicBrainzLink from "./MusicBrainzLink"; 2 3 3 4 interface Artist { 4 5 artistName: string; ··· 55 56 56 57 <div className="flex-1 min-w-0"> 57 58 <h3 className="text-sm font-medium text-zinc-100 truncate"> 58 - {trackName} 59 + <MusicBrainzLink releaseMbId={releaseMbId}> 60 + {trackName} 61 + </MusicBrainzLink> 59 62 </h3> 60 63 <p className="text-xs text-zinc-500 truncate">{artistNames}</p> 61 64 </div>
+13 -6
src/TopTracks.tsx
··· 1 1 import { graphql, useLazyLoadQuery } from "react-relay"; 2 + import { useParams } from "react-router-dom"; 2 3 import type { TopTracksQuery } from "./__generated__/TopTracksQuery.graphql"; 3 4 import TopTrackItem from "./TopTrackItem"; 4 5 import Layout from "./Layout"; 6 + import { useDateRangeFilter } from "./useDateRangeFilter"; 5 7 6 8 export default function TopTracks() { 9 + const { period } = useParams<{ period?: string }>(); 10 + const queryVariables = useDateRangeFilter(period); 11 + 7 12 const data = useLazyLoadQuery<TopTracksQuery>( 8 13 graphql` 9 - query TopTracksQuery { 10 - fmTealAlphaFeedPlaysAggregated( 11 - groupBy: ["trackName", "releaseMbId", "artists"] 12 - orderBy: { count: desc } 14 + query TopTracksQuery($where: FmTealAlphaFeedPlayWhereInput) { 15 + fmTealAlphaFeedPlayAggregated( 16 + groupBy: [{ field: trackName }, { field: releaseMbId }, { field: artists }] 17 + orderBy: { count: DESC } 13 18 limit: 50 19 + where: $where 14 20 ) { 15 21 trackName 16 22 releaseMbId ··· 19 25 } 20 26 } 21 27 `, 22 - {} 28 + queryVariables, 29 + { fetchKey: period || "all", fetchPolicy: "store-or-network" }, 23 30 ); 24 31 25 - const tracks = data.fmTealAlphaFeedPlaysAggregated || []; 32 + const tracks = data.fmTealAlphaFeedPlayAggregated || []; 26 33 const maxCount = tracks.length > 0 ? tracks[0].count : 0; 27 34 28 35 return (
+27 -8
src/TrackItem.tsx
··· 1 1 import { graphql, useFragment } from "react-relay"; 2 2 import type { TrackItem_play$key } from "./__generated__/TrackItem_play.graphql"; 3 3 import AlbumArt from "./AlbumArt"; 4 + import MusicBrainzLink from "./MusicBrainzLink"; 4 5 5 6 interface TrackItemProps { 6 7 play: TrackItem_play$key; ··· 12 13 fragment TrackItem_play on FmTealAlphaFeedPlay { 13 14 trackName 14 15 playedTime 15 - artists 16 + artists { 17 + artistName 18 + } 16 19 releaseName 17 20 releaseMbId 18 21 actorHandle 19 - appBskyActorProfile { 22 + musicServiceBaseDomain 23 + appBskyActorProfileByDid { 20 24 displayName 21 25 } 22 26 } 23 27 `, 24 - play 28 + play, 25 29 ); 26 30 27 31 return ( 28 32 <div className="group py-3 px-4 hover:bg-zinc-900/50 transition-colors"> 29 33 <div className="flex items-center gap-4"> 30 34 <div className="flex-shrink-0"> 31 - <AlbumArt releaseMbId={data.releaseMbId} alt={`${data.trackName} album art`} /> 35 + <AlbumArt 36 + releaseMbId={data.releaseMbId} 37 + alt={`${data.trackName} album art`} 38 + /> 32 39 </div> 33 40 34 41 <div className="flex-1 min-w-0 grid grid-cols-2 gap-4"> 35 42 <div className="min-w-0"> 36 - <h3 className="text-sm font-medium text-zinc-100 truncate"> 37 - {data.trackName} 43 + <h3 className="text-sm font-medium text-zinc-100 truncate flex items-center gap-2"> 44 + <span className="truncate">{data.trackName}</span> 45 + {data.musicServiceBaseDomain === "nts.live" && ( 46 + <a 47 + href={`https://${data.musicServiceBaseDomain}`} 48 + target="_blank" 49 + rel="noopener noreferrer" 50 + className="text-[10px] px-1.5 py-0.5 bg-violet-500/20 text-violet-400 rounded flex-shrink-0 hover:bg-violet-500/30 transition-colors" 51 + > 52 + NTS 53 + </a> 54 + )} 38 55 </h3> 39 56 <p className="text-xs text-zinc-500 truncate"> 40 57 {Array.isArray(data.artists) 41 58 ? data.artists.map((a) => a.artistName).join(", ") 42 - : data.artists} 59 + : "Unknown Artist"} 43 60 </p> 44 61 </div> 45 62 46 63 <div className="text-right min-w-0"> 47 64 <p className="text-xs text-zinc-400 truncate"> 48 - {data.releaseName} 65 + <MusicBrainzLink releaseMbId={data.releaseMbId}> 66 + {data.releaseName} 67 + </MusicBrainzLink> 49 68 </p> 50 69 <div className="flex items-center justify-end gap-2 mt-0.5 min-w-0 overflow-hidden"> 51 70 {data.playedTime && (
+28 -10
src/__generated__/AppPaginationQuery.graphql.ts
··· 1 1 /** 2 - * @generated SignedSource<<cef2df106afea24fa8527f2def8e9991>> 2 + * @generated SignedSource<<783e2832122c1d8f7bb41d1fe78f9ef1>> 3 3 * @lightSyntaxTransform 4 4 * @nogrep 5 5 */ ··· 51 51 "name": "sortBy", 52 52 "value": [ 53 53 { 54 - "direction": "desc", 54 + "direction": "DESC", 55 55 "field": "playedTime" 56 56 } 57 57 ] ··· 95 95 "args": (v1/*: any*/), 96 96 "concreteType": "FmTealAlphaFeedPlayConnection", 97 97 "kind": "LinkedField", 98 - "name": "fmTealAlphaFeedPlays", 98 + "name": "fmTealAlphaFeedPlay", 99 99 "plural": false, 100 100 "selections": [ 101 101 { ··· 138 138 { 139 139 "alias": null, 140 140 "args": null, 141 - "kind": "ScalarField", 141 + "concreteType": "FmTealAlphaFeedDefsArtist", 142 + "kind": "LinkedField", 142 143 "name": "artists", 144 + "plural": true, 145 + "selections": [ 146 + { 147 + "alias": null, 148 + "args": null, 149 + "kind": "ScalarField", 150 + "name": "artistName", 151 + "storageKey": null 152 + } 153 + ], 143 154 "storageKey": null 144 155 }, 145 156 { ··· 166 177 { 167 178 "alias": null, 168 179 "args": null, 180 + "kind": "ScalarField", 181 + "name": "musicServiceBaseDomain", 182 + "storageKey": null 183 + }, 184 + { 185 + "alias": null, 186 + "args": null, 169 187 "concreteType": "AppBskyActorProfile", 170 188 "kind": "LinkedField", 171 - "name": "appBskyActorProfile", 189 + "name": "appBskyActorProfileByDid", 172 190 "plural": false, 173 191 "selections": [ 174 192 { ··· 236 254 "sortBy" 237 255 ], 238 256 "handle": "connection", 239 - "key": "App_fmTealAlphaFeedPlays", 257 + "key": "App_fmTealAlphaFeedPlay", 240 258 "kind": "LinkedHandle", 241 - "name": "fmTealAlphaFeedPlays" 259 + "name": "fmTealAlphaFeedPlay" 242 260 } 243 261 ] 244 262 }, 245 263 "params": { 246 - "cacheID": "e115a73de49cf6f84a35f172a7910c5c", 264 + "cacheID": "dac482c1a09c5930d955d9805c6dfb8d", 247 265 "id": null, 248 266 "metadata": {}, 249 267 "name": "AppPaginationQuery", 250 268 "operationKind": "query", 251 - "text": "query AppPaginationQuery(\n $count: Int = 20\n $cursor: String\n) {\n ...App_plays_1G22uz\n}\n\nfragment App_plays_1G22uz on Query {\n fmTealAlphaFeedPlays(first: $count, after: $cursor, sortBy: [{field: \"playedTime\", direction: desc}]) {\n totalCount\n edges {\n node {\n playedTime\n ...TrackItem_play\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment TrackItem_play on FmTealAlphaFeedPlay {\n trackName\n playedTime\n artists\n releaseName\n releaseMbId\n actorHandle\n appBskyActorProfile {\n displayName\n }\n}\n" 269 + "text": "query AppPaginationQuery(\n $count: Int = 20\n $cursor: String\n) {\n ...App_plays_1G22uz\n}\n\nfragment App_plays_1G22uz on Query {\n fmTealAlphaFeedPlay(first: $count, after: $cursor, sortBy: [{field: playedTime, direction: DESC}]) {\n totalCount\n edges {\n node {\n playedTime\n ...TrackItem_play\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment TrackItem_play on FmTealAlphaFeedPlay {\n trackName\n playedTime\n artists {\n artistName\n }\n releaseName\n releaseMbId\n actorHandle\n musicServiceBaseDomain\n appBskyActorProfileByDid {\n displayName\n }\n}\n" 252 270 } 253 271 }; 254 272 })(); 255 273 256 - (node as any).hash = "0e4acf96fedae07af90ce6e9e3bf18d6"; 274 + (node as any).hash = "b793d066128b9e7d52d3209bd3e14afe"; 257 275 258 276 export default node;
+130 -27
src/__generated__/AppQuery.graphql.ts
··· 1 1 /** 2 - * @generated SignedSource<<541e6114682aef7988bd233592085337>> 2 + * @generated SignedSource<<fa406caeea45379cd74ae946fcf13cc3>> 3 3 * @lightSyntaxTransform 4 4 * @nogrep 5 5 */ ··· 10 10 11 11 import { ConcreteRequest } from 'relay-runtime'; 12 12 import { FragmentRefs } from "relay-runtime"; 13 - export type AppQuery$variables = Record<PropertyKey, never>; 13 + export type FmTealAlphaFeedPlayWhereInput = { 14 + actorHandle?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 15 + and?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput> | null | undefined; 16 + cid?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 17 + collection?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 18 + did?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 19 + duration?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 20 + indexedAt?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 21 + isrc?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 22 + musicServiceBaseDomain?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 23 + or?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput> | null | undefined; 24 + originUrl?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 25 + playedTime?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 26 + recordingMbId?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 27 + releaseMbId?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 28 + releaseName?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 29 + submissionClientAgent?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 30 + trackMbId?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 31 + trackName?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 32 + uri?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 33 + }; 34 + export type FmTealAlphaFeedPlayFieldCondition = { 35 + contains?: string | null | undefined; 36 + eq?: string | null | undefined; 37 + gt?: string | null | undefined; 38 + gte?: string | null | undefined; 39 + in?: ReadonlyArray<string> | null | undefined; 40 + lt?: string | null | undefined; 41 + lte?: string | null | undefined; 42 + }; 43 + export type AppQuery$variables = { 44 + chartWhere: FmTealAlphaFeedPlayWhereInput; 45 + }; 14 46 export type AppQuery$data = { 15 - readonly " $fragmentSpreads": FragmentRefs<"App_plays">; 47 + readonly " $fragmentSpreads": FragmentRefs<"App_plays" | "ScrobbleChart_data">; 16 48 }; 17 49 export type AppQuery = { 18 50 response: AppQuery$data; ··· 22 54 const node: ConcreteRequest = (function(){ 23 55 var v0 = [ 24 56 { 57 + "defaultValue": null, 58 + "kind": "LocalArgument", 59 + "name": "chartWhere" 60 + } 61 + ], 62 + v1 = [ 63 + { 25 64 "kind": "Literal", 26 65 "name": "first", 27 66 "value": 20 ··· 31 70 "name": "sortBy", 32 71 "value": [ 33 72 { 34 - "direction": "desc", 73 + "direction": "DESC", 35 74 "field": "playedTime" 36 75 } 37 76 ] 38 77 } 39 - ]; 78 + ], 79 + v2 = { 80 + "alias": null, 81 + "args": null, 82 + "kind": "ScalarField", 83 + "name": "playedTime", 84 + "storageKey": null 85 + }; 40 86 return { 41 87 "fragment": { 42 - "argumentDefinitions": [], 88 + "argumentDefinitions": (v0/*: any*/), 43 89 "kind": "Fragment", 44 90 "metadata": null, 45 91 "name": "AppQuery", ··· 48 94 "args": null, 49 95 "kind": "FragmentSpread", 50 96 "name": "App_plays" 97 + }, 98 + { 99 + "args": null, 100 + "kind": "FragmentSpread", 101 + "name": "ScrobbleChart_data" 51 102 } 52 103 ], 53 104 "type": "Query", ··· 55 106 }, 56 107 "kind": "Request", 57 108 "operation": { 58 - "argumentDefinitions": [], 109 + "argumentDefinitions": (v0/*: any*/), 59 110 "kind": "Operation", 60 111 "name": "AppQuery", 61 112 "selections": [ 62 113 { 63 114 "alias": null, 64 - "args": (v0/*: any*/), 115 + "args": (v1/*: any*/), 65 116 "concreteType": "FmTealAlphaFeedPlayConnection", 66 117 "kind": "LinkedField", 67 - "name": "fmTealAlphaFeedPlays", 118 + "name": "fmTealAlphaFeedPlay", 68 119 "plural": false, 69 120 "selections": [ 70 121 { ··· 90 141 "name": "node", 91 142 "plural": false, 92 143 "selections": [ 144 + (v2/*: any*/), 93 145 { 94 146 "alias": null, 95 147 "args": null, 96 148 "kind": "ScalarField", 97 - "name": "playedTime", 149 + "name": "trackName", 98 150 "storageKey": null 99 151 }, 100 152 { 101 153 "alias": null, 102 154 "args": null, 103 - "kind": "ScalarField", 104 - "name": "trackName", 105 - "storageKey": null 106 - }, 107 - { 108 - "alias": null, 109 - "args": null, 110 - "kind": "ScalarField", 111 - "name": "artists", 155 + "concreteType": "FmTealAlphaFeedDefsArtist", 156 + "kind": "LinkedField", 157 + "name": "artists", 158 + "plural": true, 159 + "selections": [ 160 + { 161 + "alias": null, 162 + "args": null, 163 + "kind": "ScalarField", 164 + "name": "artistName", 165 + "storageKey": null 166 + } 167 + ], 112 168 "storageKey": null 113 169 }, 114 170 { ··· 130 186 "args": null, 131 187 "kind": "ScalarField", 132 188 "name": "actorHandle", 189 + "storageKey": null 190 + }, 191 + { 192 + "alias": null, 193 + "args": null, 194 + "kind": "ScalarField", 195 + "name": "musicServiceBaseDomain", 133 196 "storageKey": null 134 197 }, 135 198 { ··· 137 200 "args": null, 138 201 "concreteType": "AppBskyActorProfile", 139 202 "kind": "LinkedField", 140 - "name": "appBskyActorProfile", 203 + "name": "appBskyActorProfileByDid", 141 204 "plural": false, 142 205 "selections": [ 143 206 { ··· 196 259 "storageKey": null 197 260 } 198 261 ], 199 - "storageKey": "fmTealAlphaFeedPlays(first:20,sortBy:[{\"direction\":\"desc\",\"field\":\"playedTime\"}])" 262 + "storageKey": "fmTealAlphaFeedPlay(first:20,sortBy:[{\"direction\":\"DESC\",\"field\":\"playedTime\"}])" 200 263 }, 201 264 { 202 265 "alias": null, 203 - "args": (v0/*: any*/), 266 + "args": (v1/*: any*/), 204 267 "filters": [ 205 268 "sortBy" 206 269 ], 207 270 "handle": "connection", 208 - "key": "App_fmTealAlphaFeedPlays", 271 + "key": "App_fmTealAlphaFeedPlay", 209 272 "kind": "LinkedHandle", 210 - "name": "fmTealAlphaFeedPlays" 273 + "name": "fmTealAlphaFeedPlay" 274 + }, 275 + { 276 + "alias": "chartData", 277 + "args": [ 278 + { 279 + "kind": "Literal", 280 + "name": "groupBy", 281 + "value": [ 282 + { 283 + "field": "playedTime", 284 + "interval": "DAY" 285 + } 286 + ] 287 + }, 288 + { 289 + "kind": "Literal", 290 + "name": "limit", 291 + "value": 90 292 + }, 293 + { 294 + "kind": "Variable", 295 + "name": "where", 296 + "variableName": "chartWhere" 297 + } 298 + ], 299 + "concreteType": "FmTealAlphaFeedPlayAggregated", 300 + "kind": "LinkedField", 301 + "name": "fmTealAlphaFeedPlayAggregated", 302 + "plural": true, 303 + "selections": [ 304 + (v2/*: any*/), 305 + { 306 + "alias": null, 307 + "args": null, 308 + "kind": "ScalarField", 309 + "name": "count", 310 + "storageKey": null 311 + } 312 + ], 313 + "storageKey": null 211 314 } 212 315 ] 213 316 }, 214 317 "params": { 215 - "cacheID": "1cacfb0aa5545cf84688b8396079b855", 318 + "cacheID": "3793b7efac382baa6447beb922658c23", 216 319 "id": null, 217 320 "metadata": {}, 218 321 "name": "AppQuery", 219 322 "operationKind": "query", 220 - "text": "query AppQuery {\n ...App_plays\n}\n\nfragment App_plays on Query {\n fmTealAlphaFeedPlays(first: 20, sortBy: [{field: \"playedTime\", direction: desc}]) {\n totalCount\n edges {\n node {\n playedTime\n ...TrackItem_play\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment TrackItem_play on FmTealAlphaFeedPlay {\n trackName\n playedTime\n artists\n releaseName\n releaseMbId\n actorHandle\n appBskyActorProfile {\n displayName\n }\n}\n" 323 + "text": "query AppQuery(\n $chartWhere: FmTealAlphaFeedPlayWhereInput!\n) {\n ...App_plays\n ...ScrobbleChart_data\n}\n\nfragment App_plays on Query {\n fmTealAlphaFeedPlay(first: 20, sortBy: [{field: playedTime, direction: DESC}]) {\n totalCount\n edges {\n node {\n playedTime\n ...TrackItem_play\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment ScrobbleChart_data on Query {\n chartData: fmTealAlphaFeedPlayAggregated(groupBy: [{field: playedTime, interval: DAY}], where: $chartWhere, limit: 90) {\n playedTime\n count\n }\n}\n\nfragment TrackItem_play on FmTealAlphaFeedPlay {\n trackName\n playedTime\n artists {\n artistName\n }\n releaseName\n releaseMbId\n actorHandle\n musicServiceBaseDomain\n appBskyActorProfileByDid {\n displayName\n }\n}\n" 221 324 } 222 325 }; 223 326 })(); 224 327 225 - (node as any).hash = "4b1837f6cd874e31461fbead77c1b012"; 328 + (node as any).hash = "7266612861cb55b740623549f1a03f26"; 226 329 227 330 export default node;
+24 -6
src/__generated__/AppSubscription.graphql.ts
··· 1 1 /** 2 - * @generated SignedSource<<401c6c4a1920db251447fa96aca8768a>> 2 + * @generated SignedSource<<896a9c63784bd14529042a1cb0adfd64>> 3 3 * @lightSyntaxTransform 4 4 * @nogrep 5 5 */ ··· 14 14 export type AppSubscription$data = { 15 15 readonly fmTealAlphaFeedPlayCreated: { 16 16 readonly playedTime: string | null | undefined; 17 - readonly uri: string; 17 + readonly uri: string | null | undefined; 18 18 readonly " $fragmentSpreads": FragmentRefs<"TrackItem_play">; 19 19 }; 20 20 }; ··· 93 93 { 94 94 "alias": null, 95 95 "args": null, 96 - "kind": "ScalarField", 96 + "concreteType": "FmTealAlphaFeedDefsArtist", 97 + "kind": "LinkedField", 97 98 "name": "artists", 99 + "plural": true, 100 + "selections": [ 101 + { 102 + "alias": null, 103 + "args": null, 104 + "kind": "ScalarField", 105 + "name": "artistName", 106 + "storageKey": null 107 + } 108 + ], 98 109 "storageKey": null 99 110 }, 100 111 { ··· 121 132 { 122 133 "alias": null, 123 134 "args": null, 135 + "kind": "ScalarField", 136 + "name": "musicServiceBaseDomain", 137 + "storageKey": null 138 + }, 139 + { 140 + "alias": null, 141 + "args": null, 124 142 "concreteType": "AppBskyActorProfile", 125 143 "kind": "LinkedField", 126 - "name": "appBskyActorProfile", 144 + "name": "appBskyActorProfileByDid", 127 145 "plural": false, 128 146 "selections": [ 129 147 { ··· 142 160 ] 143 161 }, 144 162 "params": { 145 - "cacheID": "c856872303e0f4904ea70ed5dc54cce2", 163 + "cacheID": "15e882ff59aeffce82a48611e02dbe63", 146 164 "id": null, 147 165 "metadata": {}, 148 166 "name": "AppSubscription", 149 167 "operationKind": "subscription", 150 - "text": "subscription AppSubscription {\n fmTealAlphaFeedPlayCreated {\n uri\n playedTime\n ...TrackItem_play\n }\n}\n\nfragment TrackItem_play on FmTealAlphaFeedPlay {\n trackName\n playedTime\n artists\n releaseName\n releaseMbId\n actorHandle\n appBskyActorProfile {\n displayName\n }\n}\n" 168 + "text": "subscription AppSubscription {\n fmTealAlphaFeedPlayCreated {\n uri\n playedTime\n ...TrackItem_play\n }\n}\n\nfragment TrackItem_play on FmTealAlphaFeedPlay {\n trackName\n playedTime\n artists {\n artistName\n }\n releaseName\n releaseMbId\n actorHandle\n musicServiceBaseDomain\n appBskyActorProfileByDid {\n displayName\n }\n}\n" 151 169 } 152 170 }; 153 171 })();
+10 -10
src/__generated__/App_plays.graphql.ts
··· 1 1 /** 2 - * @generated SignedSource<<a3ae5f31f618986fb12e6c57458c9853>> 2 + * @generated SignedSource<<3266d35506946a9879921e682d9a0b8a>> 3 3 * @lightSyntaxTransform 4 4 * @nogrep 5 5 */ ··· 11 11 import { ReaderFragment } from 'relay-runtime'; 12 12 import { FragmentRefs } from "relay-runtime"; 13 13 export type App_plays$data = { 14 - readonly fmTealAlphaFeedPlays: { 14 + readonly fmTealAlphaFeedPlay: { 15 15 readonly edges: ReadonlyArray<{ 16 16 readonly node: { 17 17 readonly playedTime: string | null | undefined; 18 18 readonly " $fragmentSpreads": FragmentRefs<"TrackItem_play">; 19 19 }; 20 20 }>; 21 - readonly totalCount: number; 22 - }; 21 + readonly totalCount: number | null | undefined; 22 + } | null | undefined; 23 23 readonly " $fragmentType": "App_plays"; 24 24 }; 25 25 export type App_plays$key = { ··· 31 31 32 32 const node: ReaderFragment = (function(){ 33 33 var v0 = [ 34 - "fmTealAlphaFeedPlays" 34 + "fmTealAlphaFeedPlay" 35 35 ]; 36 36 return { 37 37 "argumentDefinitions": [ ··· 72 72 "name": "App_plays", 73 73 "selections": [ 74 74 { 75 - "alias": "fmTealAlphaFeedPlays", 75 + "alias": "fmTealAlphaFeedPlay", 76 76 "args": [ 77 77 { 78 78 "kind": "Literal", 79 79 "name": "sortBy", 80 80 "value": [ 81 81 { 82 - "direction": "desc", 82 + "direction": "DESC", 83 83 "field": "playedTime" 84 84 } 85 85 ] ··· 87 87 ], 88 88 "concreteType": "FmTealAlphaFeedPlayConnection", 89 89 "kind": "LinkedField", 90 - "name": "__App_fmTealAlphaFeedPlays_connection", 90 + "name": "__App_fmTealAlphaFeedPlay_connection", 91 91 "plural": false, 92 92 "selections": [ 93 93 { ··· 171 171 "storageKey": null 172 172 } 173 173 ], 174 - "storageKey": "__App_fmTealAlphaFeedPlays_connection(sortBy:[{\"direction\":\"desc\",\"field\":\"playedTime\"}])" 174 + "storageKey": "__App_fmTealAlphaFeedPlay_connection(sortBy:[{\"direction\":\"DESC\",\"field\":\"playedTime\"}])" 175 175 } 176 176 ], 177 177 "type": "Query", ··· 179 179 }; 180 180 })(); 181 181 182 - (node as any).hash = "0e4acf96fedae07af90ce6e9e3bf18d6"; 182 + (node as any).hash = "b793d066128b9e7d52d3209bd3e14afe"; 183 183 184 184 export default node;
+59 -11
src/__generated__/ProfilePaginationQuery.graphql.ts
··· 1 1 /** 2 - * @generated SignedSource<<97625934b32c4079cc58877234aeac04>> 2 + * @generated SignedSource<<39639d8abd772effa21efdf069699bf6>> 3 3 * @lightSyntaxTransform 4 4 * @nogrep 5 5 */ ··· 10 10 11 11 import { ConcreteRequest } from 'relay-runtime'; 12 12 import { FragmentRefs } from "relay-runtime"; 13 + export type FmTealAlphaFeedPlayWhereInput = { 14 + actorHandle?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 15 + and?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput> | null | undefined; 16 + cid?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 17 + collection?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 18 + did?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 19 + duration?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 20 + indexedAt?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 21 + isrc?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 22 + musicServiceBaseDomain?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 23 + or?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput> | null | undefined; 24 + originUrl?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 25 + playedTime?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 26 + recordingMbId?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 27 + releaseMbId?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 28 + releaseName?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 29 + submissionClientAgent?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 30 + trackMbId?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 31 + trackName?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 32 + uri?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 33 + }; 34 + export type FmTealAlphaFeedPlayFieldCondition = { 35 + contains?: string | null | undefined; 36 + eq?: string | null | undefined; 37 + gt?: string | null | undefined; 38 + gte?: string | null | undefined; 39 + in?: ReadonlyArray<string> | null | undefined; 40 + lt?: string | null | undefined; 41 + lte?: string | null | undefined; 42 + }; 13 43 export type ProfilePaginationQuery$variables = { 14 44 count?: number | null | undefined; 15 45 cursor?: string | null | undefined; 16 - where: any; 46 + where: FmTealAlphaFeedPlayWhereInput; 17 47 }; 18 48 export type ProfilePaginationQuery$data = { 19 49 readonly " $fragmentSpreads": FragmentRefs<"Profile_plays">; ··· 62 92 "name": "sortBy", 63 93 "value": [ 64 94 { 65 - "direction": "desc", 95 + "direction": "DESC", 66 96 "field": "playedTime" 67 97 } 68 98 ] ··· 108 138 "args": (v2/*: any*/), 109 139 "concreteType": "FmTealAlphaFeedPlayConnection", 110 140 "kind": "LinkedField", 111 - "name": "fmTealAlphaFeedPlays", 141 + "name": "fmTealAlphaFeedPlay", 112 142 "plural": false, 113 143 "selections": [ 114 144 { ··· 151 181 { 152 182 "alias": null, 153 183 "args": null, 154 - "kind": "ScalarField", 184 + "concreteType": "FmTealAlphaFeedDefsArtist", 185 + "kind": "LinkedField", 155 186 "name": "artists", 187 + "plural": true, 188 + "selections": [ 189 + { 190 + "alias": null, 191 + "args": null, 192 + "kind": "ScalarField", 193 + "name": "artistName", 194 + "storageKey": null 195 + } 196 + ], 156 197 "storageKey": null 157 198 }, 158 199 { ··· 179 220 { 180 221 "alias": null, 181 222 "args": null, 223 + "kind": "ScalarField", 224 + "name": "musicServiceBaseDomain", 225 + "storageKey": null 226 + }, 227 + { 228 + "alias": null, 229 + "args": null, 182 230 "concreteType": "AppBskyActorProfile", 183 231 "kind": "LinkedField", 184 - "name": "appBskyActorProfile", 232 + "name": "appBskyActorProfileByDid", 185 233 "plural": false, 186 234 "selections": [ 187 235 { ··· 281 329 "sortBy" 282 330 ], 283 331 "handle": "connection", 284 - "key": "Profile_fmTealAlphaFeedPlays", 332 + "key": "Profile_fmTealAlphaFeedPlay", 285 333 "kind": "LinkedHandle", 286 - "name": "fmTealAlphaFeedPlays" 334 + "name": "fmTealAlphaFeedPlay" 287 335 } 288 336 ] 289 337 }, 290 338 "params": { 291 - "cacheID": "08e603fb4052c3556739bda428413453", 339 + "cacheID": "4da0ee226512aceadce3210332ed4766", 292 340 "id": null, 293 341 "metadata": {}, 294 342 "name": "ProfilePaginationQuery", 295 343 "operationKind": "query", 296 - "text": "query ProfilePaginationQuery(\n $count: Int = 20\n $cursor: String\n $where: JSON!\n) {\n ...Profile_plays_mjR8k\n}\n\nfragment Profile_plays_mjR8k on Query {\n fmTealAlphaFeedPlays(first: $count, after: $cursor, sortBy: [{field: \"playedTime\", direction: desc}], where: $where) {\n totalCount\n edges {\n node {\n ...TrackItem_play\n actorHandle\n appBskyActorProfile {\n displayName\n description\n avatar {\n url(preset: \"avatar\")\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment TrackItem_play on FmTealAlphaFeedPlay {\n trackName\n playedTime\n artists\n releaseName\n releaseMbId\n actorHandle\n appBskyActorProfile {\n displayName\n }\n}\n" 344 + "text": "query ProfilePaginationQuery(\n $count: Int = 20\n $cursor: String\n $where: FmTealAlphaFeedPlayWhereInput!\n) {\n ...Profile_plays_mjR8k\n}\n\nfragment Profile_plays_mjR8k on Query {\n fmTealAlphaFeedPlay(first: $count, after: $cursor, sortBy: [{field: playedTime, direction: DESC}], where: $where) {\n totalCount\n edges {\n node {\n ...TrackItem_play\n actorHandle\n appBskyActorProfileByDid {\n displayName\n description\n avatar {\n url(preset: \"avatar\")\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment TrackItem_play on FmTealAlphaFeedPlay {\n trackName\n playedTime\n artists {\n artistName\n }\n releaseName\n releaseMbId\n actorHandle\n musicServiceBaseDomain\n appBskyActorProfileByDid {\n displayName\n }\n}\n" 297 345 } 298 346 }; 299 347 })(); 300 348 301 - (node as any).hash = "474168bb0d13417c1b7067c09a82f7a2"; 349 + (node as any).hash = "06ba557474df22684f61a32da8aec20a"; 302 350 303 351 export default node;
+139 -35
src/__generated__/ProfileQuery.graphql.ts
··· 1 1 /** 2 - * @generated SignedSource<<c47dafb8d21963c2a9dcbcd54d7bd8d8>> 2 + * @generated SignedSource<<586d7e40425eb1e823fb98f47d69cefd>> 3 3 * @lightSyntaxTransform 4 4 * @nogrep 5 5 */ ··· 10 10 11 11 import { ConcreteRequest } from 'relay-runtime'; 12 12 import { FragmentRefs } from "relay-runtime"; 13 + export type FmTealAlphaFeedPlayWhereInput = { 14 + actorHandle?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 15 + and?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput> | null | undefined; 16 + cid?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 17 + collection?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 18 + did?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 19 + duration?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 20 + indexedAt?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 21 + isrc?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 22 + musicServiceBaseDomain?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 23 + or?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput> | null | undefined; 24 + originUrl?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 25 + playedTime?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 26 + recordingMbId?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 27 + releaseMbId?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 28 + releaseName?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 29 + submissionClientAgent?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 30 + trackMbId?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 31 + trackName?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 32 + uri?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 33 + }; 34 + export type FmTealAlphaFeedPlayFieldCondition = { 35 + contains?: string | null | undefined; 36 + eq?: string | null | undefined; 37 + gt?: string | null | undefined; 38 + gte?: string | null | undefined; 39 + in?: ReadonlyArray<string> | null | undefined; 40 + lt?: string | null | undefined; 41 + lte?: string | null | undefined; 42 + }; 13 43 export type ProfileQuery$variables = { 14 - where: any; 44 + chartWhere: FmTealAlphaFeedPlayWhereInput; 45 + where: FmTealAlphaFeedPlayWhereInput; 15 46 }; 16 47 export type ProfileQuery$data = { 17 - readonly " $fragmentSpreads": FragmentRefs<"Profile_plays">; 48 + readonly " $fragmentSpreads": FragmentRefs<"Profile_plays" | "ScrobbleChart_data">; 18 49 }; 19 50 export type ProfileQuery = { 20 51 response: ProfileQuery$data; ··· 22 53 }; 23 54 24 55 const node: ConcreteRequest = (function(){ 25 - var v0 = [ 26 - { 27 - "defaultValue": null, 28 - "kind": "LocalArgument", 29 - "name": "where" 30 - } 31 - ], 56 + var v0 = { 57 + "defaultValue": null, 58 + "kind": "LocalArgument", 59 + "name": "chartWhere" 60 + }, 32 61 v1 = { 62 + "defaultValue": null, 63 + "kind": "LocalArgument", 64 + "name": "where" 65 + }, 66 + v2 = { 33 67 "kind": "Variable", 34 68 "name": "where", 35 69 "variableName": "where" 36 70 }, 37 - v2 = [ 71 + v3 = [ 38 72 { 39 73 "kind": "Literal", 40 74 "name": "first", ··· 45 79 "name": "sortBy", 46 80 "value": [ 47 81 { 48 - "direction": "desc", 82 + "direction": "DESC", 49 83 "field": "playedTime" 50 84 } 51 85 ] 52 86 }, 53 - (v1/*: any*/) 54 - ]; 87 + (v2/*: any*/) 88 + ], 89 + v4 = { 90 + "alias": null, 91 + "args": null, 92 + "kind": "ScalarField", 93 + "name": "playedTime", 94 + "storageKey": null 95 + }; 55 96 return { 56 97 "fragment": { 57 - "argumentDefinitions": (v0/*: any*/), 98 + "argumentDefinitions": [ 99 + (v0/*: any*/), 100 + (v1/*: any*/) 101 + ], 58 102 "kind": "Fragment", 59 103 "metadata": null, 60 104 "name": "ProfileQuery", 61 105 "selections": [ 62 106 { 63 107 "args": [ 64 - (v1/*: any*/) 108 + (v2/*: any*/) 65 109 ], 66 110 "kind": "FragmentSpread", 67 111 "name": "Profile_plays" 112 + }, 113 + { 114 + "args": null, 115 + "kind": "FragmentSpread", 116 + "name": "ScrobbleChart_data" 68 117 } 69 118 ], 70 119 "type": "Query", ··· 72 121 }, 73 122 "kind": "Request", 74 123 "operation": { 75 - "argumentDefinitions": (v0/*: any*/), 124 + "argumentDefinitions": [ 125 + (v1/*: any*/), 126 + (v0/*: any*/) 127 + ], 76 128 "kind": "Operation", 77 129 "name": "ProfileQuery", 78 130 "selections": [ 79 131 { 80 132 "alias": null, 81 - "args": (v2/*: any*/), 133 + "args": (v3/*: any*/), 82 134 "concreteType": "FmTealAlphaFeedPlayConnection", 83 135 "kind": "LinkedField", 84 - "name": "fmTealAlphaFeedPlays", 136 + "name": "fmTealAlphaFeedPlay", 85 137 "plural": false, 86 138 "selections": [ 87 139 { ··· 114 166 "name": "trackName", 115 167 "storageKey": null 116 168 }, 169 + (v4/*: any*/), 117 170 { 118 171 "alias": null, 119 172 "args": null, 120 - "kind": "ScalarField", 121 - "name": "playedTime", 122 - "storageKey": null 123 - }, 124 - { 125 - "alias": null, 126 - "args": null, 127 - "kind": "ScalarField", 128 - "name": "artists", 173 + "concreteType": "FmTealAlphaFeedDefsArtist", 174 + "kind": "LinkedField", 175 + "name": "artists", 176 + "plural": true, 177 + "selections": [ 178 + { 179 + "alias": null, 180 + "args": null, 181 + "kind": "ScalarField", 182 + "name": "artistName", 183 + "storageKey": null 184 + } 185 + ], 129 186 "storageKey": null 130 187 }, 131 188 { ··· 152 209 { 153 210 "alias": null, 154 211 "args": null, 212 + "kind": "ScalarField", 213 + "name": "musicServiceBaseDomain", 214 + "storageKey": null 215 + }, 216 + { 217 + "alias": null, 218 + "args": null, 155 219 "concreteType": "AppBskyActorProfile", 156 220 "kind": "LinkedField", 157 - "name": "appBskyActorProfile", 221 + "name": "appBskyActorProfileByDid", 158 222 "plural": false, 159 223 "selections": [ 160 224 { ··· 248 312 }, 249 313 { 250 314 "alias": null, 251 - "args": (v2/*: any*/), 315 + "args": (v3/*: any*/), 252 316 "filters": [ 253 317 "where", 254 318 "sortBy" 255 319 ], 256 320 "handle": "connection", 257 - "key": "Profile_fmTealAlphaFeedPlays", 321 + "key": "Profile_fmTealAlphaFeedPlay", 258 322 "kind": "LinkedHandle", 259 - "name": "fmTealAlphaFeedPlays" 323 + "name": "fmTealAlphaFeedPlay" 324 + }, 325 + { 326 + "alias": "chartData", 327 + "args": [ 328 + { 329 + "kind": "Literal", 330 + "name": "groupBy", 331 + "value": [ 332 + { 333 + "field": "playedTime", 334 + "interval": "DAY" 335 + } 336 + ] 337 + }, 338 + { 339 + "kind": "Literal", 340 + "name": "limit", 341 + "value": 90 342 + }, 343 + { 344 + "kind": "Variable", 345 + "name": "where", 346 + "variableName": "chartWhere" 347 + } 348 + ], 349 + "concreteType": "FmTealAlphaFeedPlayAggregated", 350 + "kind": "LinkedField", 351 + "name": "fmTealAlphaFeedPlayAggregated", 352 + "plural": true, 353 + "selections": [ 354 + (v4/*: any*/), 355 + { 356 + "alias": null, 357 + "args": null, 358 + "kind": "ScalarField", 359 + "name": "count", 360 + "storageKey": null 361 + } 362 + ], 363 + "storageKey": null 260 364 } 261 365 ] 262 366 }, 263 367 "params": { 264 - "cacheID": "3137e7b6ec5148299e7a7c7f4edf07b2", 368 + "cacheID": "06807a18670ad67256b497dd0ef8f208", 265 369 "id": null, 266 370 "metadata": {}, 267 371 "name": "ProfileQuery", 268 372 "operationKind": "query", 269 - "text": "query ProfileQuery(\n $where: JSON!\n) {\n ...Profile_plays_3FC4Qo\n}\n\nfragment Profile_plays_3FC4Qo on Query {\n fmTealAlphaFeedPlays(first: 20, sortBy: [{field: \"playedTime\", direction: desc}], where: $where) {\n totalCount\n edges {\n node {\n ...TrackItem_play\n actorHandle\n appBskyActorProfile {\n displayName\n description\n avatar {\n url(preset: \"avatar\")\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment TrackItem_play on FmTealAlphaFeedPlay {\n trackName\n playedTime\n artists\n releaseName\n releaseMbId\n actorHandle\n appBskyActorProfile {\n displayName\n }\n}\n" 373 + "text": "query ProfileQuery(\n $where: FmTealAlphaFeedPlayWhereInput!\n $chartWhere: FmTealAlphaFeedPlayWhereInput!\n) {\n ...Profile_plays_3FC4Qo\n ...ScrobbleChart_data\n}\n\nfragment Profile_plays_3FC4Qo on Query {\n fmTealAlphaFeedPlay(first: 20, sortBy: [{field: playedTime, direction: DESC}], where: $where) {\n totalCount\n edges {\n node {\n ...TrackItem_play\n actorHandle\n appBskyActorProfileByDid {\n displayName\n description\n avatar {\n url(preset: \"avatar\")\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment ScrobbleChart_data on Query {\n chartData: fmTealAlphaFeedPlayAggregated(groupBy: [{field: playedTime, interval: DAY}], where: $chartWhere, limit: 90) {\n playedTime\n count\n }\n}\n\nfragment TrackItem_play on FmTealAlphaFeedPlay {\n trackName\n playedTime\n artists {\n artistName\n }\n releaseName\n releaseMbId\n actorHandle\n musicServiceBaseDomain\n appBskyActorProfileByDid {\n displayName\n }\n}\n" 270 374 } 271 375 }; 272 376 })(); 273 377 274 - (node as any).hash = "267039e382b3b95a739ff3cdced3211e"; 378 + (node as any).hash = "e000bb0fb9935e8e853d847c4362ffe6"; 275 379 276 380 export default node;
+11 -11
src/__generated__/Profile_plays.graphql.ts
··· 1 1 /** 2 - * @generated SignedSource<<bd63c9e74b05810076b60ad0e51cb230>> 2 + * @generated SignedSource<<debe921c118f11f1685c3407b690bcf8>> 3 3 * @lightSyntaxTransform 4 4 * @nogrep 5 5 */ ··· 11 11 import { ReaderFragment } from 'relay-runtime'; 12 12 import { FragmentRefs } from "relay-runtime"; 13 13 export type Profile_plays$data = { 14 - readonly fmTealAlphaFeedPlays: { 14 + readonly fmTealAlphaFeedPlay: { 15 15 readonly edges: ReadonlyArray<{ 16 16 readonly node: { 17 17 readonly actorHandle: string | null | undefined; 18 - readonly appBskyActorProfile: { 18 + readonly appBskyActorProfileByDid: { 19 19 readonly avatar: { 20 20 readonly url: string; 21 21 } | null | undefined; ··· 25 25 readonly " $fragmentSpreads": FragmentRefs<"TrackItem_play">; 26 26 }; 27 27 }>; 28 - readonly totalCount: number; 29 - }; 28 + readonly totalCount: number | null | undefined; 29 + } | null | undefined; 30 30 readonly " $fragmentType": "Profile_plays"; 31 31 }; 32 32 export type Profile_plays$key = { ··· 38 38 39 39 const node: ReaderFragment = (function(){ 40 40 var v0 = [ 41 - "fmTealAlphaFeedPlays" 41 + "fmTealAlphaFeedPlay" 42 42 ]; 43 43 return { 44 44 "argumentDefinitions": [ ··· 84 84 "name": "Profile_plays", 85 85 "selections": [ 86 86 { 87 - "alias": "fmTealAlphaFeedPlays", 87 + "alias": "fmTealAlphaFeedPlay", 88 88 "args": [ 89 89 { 90 90 "kind": "Literal", 91 91 "name": "sortBy", 92 92 "value": [ 93 93 { 94 - "direction": "desc", 94 + "direction": "DESC", 95 95 "field": "playedTime" 96 96 } 97 97 ] ··· 104 104 ], 105 105 "concreteType": "FmTealAlphaFeedPlayConnection", 106 106 "kind": "LinkedField", 107 - "name": "__Profile_fmTealAlphaFeedPlays_connection", 107 + "name": "__Profile_fmTealAlphaFeedPlay_connection", 108 108 "plural": false, 109 109 "selections": [ 110 110 { ··· 147 147 "args": null, 148 148 "concreteType": "AppBskyActorProfile", 149 149 "kind": "LinkedField", 150 - "name": "appBskyActorProfile", 150 + "name": "appBskyActorProfileByDid", 151 151 "plural": false, 152 152 "selections": [ 153 153 { ··· 245 245 }; 246 246 })(); 247 247 248 - (node as any).hash = "474168bb0d13417c1b7067c09a82f7a2"; 248 + (node as any).hash = "06ba557474df22684f61a32da8aec20a"; 249 249 250 250 export default node;
+89
src/__generated__/ScrobbleChart_data.graphql.ts
··· 1 + /** 2 + * @generated SignedSource<<7e2392afa490a7b1da46656aa250f70b>> 3 + * @lightSyntaxTransform 4 + * @nogrep 5 + */ 6 + 7 + /* tslint:disable */ 8 + /* eslint-disable */ 9 + // @ts-nocheck 10 + 11 + import { ReaderFragment } from 'relay-runtime'; 12 + import { FragmentRefs } from "relay-runtime"; 13 + export type ScrobbleChart_data$data = { 14 + readonly chartData: ReadonlyArray<{ 15 + readonly count: number; 16 + readonly playedTime: string | null | undefined; 17 + }> | null | undefined; 18 + readonly " $fragmentType": "ScrobbleChart_data"; 19 + }; 20 + export type ScrobbleChart_data$key = { 21 + readonly " $data"?: ScrobbleChart_data$data; 22 + readonly " $fragmentSpreads": FragmentRefs<"ScrobbleChart_data">; 23 + }; 24 + 25 + const node: ReaderFragment = { 26 + "argumentDefinitions": [ 27 + { 28 + "kind": "RootArgument", 29 + "name": "chartWhere" 30 + } 31 + ], 32 + "kind": "Fragment", 33 + "metadata": null, 34 + "name": "ScrobbleChart_data", 35 + "selections": [ 36 + { 37 + "alias": "chartData", 38 + "args": [ 39 + { 40 + "kind": "Literal", 41 + "name": "groupBy", 42 + "value": [ 43 + { 44 + "field": "playedTime", 45 + "interval": "DAY" 46 + } 47 + ] 48 + }, 49 + { 50 + "kind": "Literal", 51 + "name": "limit", 52 + "value": 90 53 + }, 54 + { 55 + "kind": "Variable", 56 + "name": "where", 57 + "variableName": "chartWhere" 58 + } 59 + ], 60 + "concreteType": "FmTealAlphaFeedPlayAggregated", 61 + "kind": "LinkedField", 62 + "name": "fmTealAlphaFeedPlayAggregated", 63 + "plural": true, 64 + "selections": [ 65 + { 66 + "alias": null, 67 + "args": null, 68 + "kind": "ScalarField", 69 + "name": "playedTime", 70 + "storageKey": null 71 + }, 72 + { 73 + "alias": null, 74 + "args": null, 75 + "kind": "ScalarField", 76 + "name": "count", 77 + "storageKey": null 78 + } 79 + ], 80 + "storageKey": null 81 + } 82 + ], 83 + "type": "Query", 84 + "abstractKey": null 85 + }; 86 + 87 + (node as any).hash = "acb96f5268c9520f77c672a7ea3a7454"; 88 + 89 + export default node;
+70 -20
src/__generated__/TopAlbumsQuery.graphql.ts
··· 1 1 /** 2 - * @generated SignedSource<<e2bf0d16ddd996a8b44b47387dd220b3>> 2 + * @generated SignedSource<<8d07fc631e364271a41ea2bd1ab069bb>> 3 3 * @lightSyntaxTransform 4 4 * @nogrep 5 5 */ ··· 9 9 // @ts-nocheck 10 10 11 11 import { ConcreteRequest } from 'relay-runtime'; 12 - export type TopAlbumsQuery$variables = Record<PropertyKey, never>; 12 + export type FmTealAlphaFeedPlayWhereInput = { 13 + actorHandle?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 14 + and?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput> | null | undefined; 15 + cid?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 16 + collection?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 17 + did?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 18 + duration?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 19 + indexedAt?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 20 + isrc?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 21 + musicServiceBaseDomain?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 22 + or?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput> | null | undefined; 23 + originUrl?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 24 + playedTime?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 25 + recordingMbId?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 26 + releaseMbId?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 27 + releaseName?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 28 + submissionClientAgent?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 29 + trackMbId?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 30 + trackName?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 31 + uri?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 32 + }; 33 + export type FmTealAlphaFeedPlayFieldCondition = { 34 + contains?: string | null | undefined; 35 + eq?: string | null | undefined; 36 + gt?: string | null | undefined; 37 + gte?: string | null | undefined; 38 + in?: ReadonlyArray<string> | null | undefined; 39 + lt?: string | null | undefined; 40 + lte?: string | null | undefined; 41 + }; 42 + export type TopAlbumsQuery$variables = { 43 + where?: FmTealAlphaFeedPlayWhereInput | null | undefined; 44 + }; 13 45 export type TopAlbumsQuery$data = { 14 - readonly fmTealAlphaFeedPlaysAggregated: ReadonlyArray<{ 15 - readonly artists: any | null | undefined; 46 + readonly fmTealAlphaFeedPlayAggregated: ReadonlyArray<{ 47 + readonly artists: string | null | undefined; 16 48 readonly count: number; 17 - readonly releaseMbId: any | null | undefined; 18 - readonly releaseName: any | null | undefined; 19 - }>; 49 + readonly releaseMbId: string | null | undefined; 50 + readonly releaseName: string | null | undefined; 51 + }> | null | undefined; 20 52 }; 21 53 export type TopAlbumsQuery = { 22 54 response: TopAlbumsQuery$data; ··· 26 58 const node: ConcreteRequest = (function(){ 27 59 var v0 = [ 28 60 { 61 + "defaultValue": null, 62 + "kind": "LocalArgument", 63 + "name": "where" 64 + } 65 + ], 66 + v1 = [ 67 + { 29 68 "alias": null, 30 69 "args": [ 31 70 { 32 71 "kind": "Literal", 33 72 "name": "groupBy", 34 73 "value": [ 35 - "releaseMbId", 36 - "releaseName", 37 - "artists" 74 + { 75 + "field": "releaseMbId" 76 + }, 77 + { 78 + "field": "releaseName" 79 + }, 80 + { 81 + "field": "artists" 82 + } 38 83 ] 39 84 }, 40 85 { ··· 46 91 "kind": "Literal", 47 92 "name": "orderBy", 48 93 "value": { 49 - "count": "desc" 94 + "count": "DESC" 50 95 } 96 + }, 97 + { 98 + "kind": "Variable", 99 + "name": "where", 100 + "variableName": "where" 51 101 } 52 102 ], 53 103 "concreteType": "FmTealAlphaFeedPlayAggregated", 54 104 "kind": "LinkedField", 55 - "name": "fmTealAlphaFeedPlaysAggregated", 105 + "name": "fmTealAlphaFeedPlayAggregated", 56 106 "plural": true, 57 107 "selections": [ 58 108 { ··· 84 134 "storageKey": null 85 135 } 86 136 ], 87 - "storageKey": "fmTealAlphaFeedPlaysAggregated(groupBy:[\"releaseMbId\",\"releaseName\",\"artists\"],limit:100,orderBy:{\"count\":\"desc\"})" 137 + "storageKey": null 88 138 } 89 139 ]; 90 140 return { 91 141 "fragment": { 92 - "argumentDefinitions": [], 142 + "argumentDefinitions": (v0/*: any*/), 93 143 "kind": "Fragment", 94 144 "metadata": null, 95 145 "name": "TopAlbumsQuery", 96 - "selections": (v0/*: any*/), 146 + "selections": (v1/*: any*/), 97 147 "type": "Query", 98 148 "abstractKey": null 99 149 }, 100 150 "kind": "Request", 101 151 "operation": { 102 - "argumentDefinitions": [], 152 + "argumentDefinitions": (v0/*: any*/), 103 153 "kind": "Operation", 104 154 "name": "TopAlbumsQuery", 105 - "selections": (v0/*: any*/) 155 + "selections": (v1/*: any*/) 106 156 }, 107 157 "params": { 108 - "cacheID": "65b42ff33b8a5de6eb4785e764ae70fc", 158 + "cacheID": "6b742b6a57c908748af1780da995e31c", 109 159 "id": null, 110 160 "metadata": {}, 111 161 "name": "TopAlbumsQuery", 112 162 "operationKind": "query", 113 - "text": "query TopAlbumsQuery {\n fmTealAlphaFeedPlaysAggregated(groupBy: [\"releaseMbId\", \"releaseName\", \"artists\"], orderBy: {count: desc}, limit: 100) {\n releaseMbId\n releaseName\n artists\n count\n }\n}\n" 163 + "text": "query TopAlbumsQuery(\n $where: FmTealAlphaFeedPlayWhereInput\n) {\n fmTealAlphaFeedPlayAggregated(groupBy: [{field: releaseMbId}, {field: releaseName}, {field: artists}], orderBy: {count: DESC}, limit: 100, where: $where) {\n releaseMbId\n releaseName\n artists\n count\n }\n}\n" 114 164 } 115 165 }; 116 166 })(); 117 167 118 - (node as any).hash = "b5748a3a4af3140d3cff228e7462f73d"; 168 + (node as any).hash = "13fd8a47c19eeb4f327f0d6d869b73cd"; 119 169 120 170 export default node;
+70 -20
src/__generated__/TopTracksQuery.graphql.ts
··· 1 1 /** 2 - * @generated SignedSource<<58e8aa653524405ace1405d28bd8f19e>> 2 + * @generated SignedSource<<3d375bb2f6549eb84b9399717743b845>> 3 3 * @lightSyntaxTransform 4 4 * @nogrep 5 5 */ ··· 9 9 // @ts-nocheck 10 10 11 11 import { ConcreteRequest } from 'relay-runtime'; 12 - export type TopTracksQuery$variables = Record<PropertyKey, never>; 12 + export type FmTealAlphaFeedPlayWhereInput = { 13 + actorHandle?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 14 + and?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput> | null | undefined; 15 + cid?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 16 + collection?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 17 + did?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 18 + duration?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 19 + indexedAt?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 20 + isrc?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 21 + musicServiceBaseDomain?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 22 + or?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput> | null | undefined; 23 + originUrl?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 24 + playedTime?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 25 + recordingMbId?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 26 + releaseMbId?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 27 + releaseName?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 28 + submissionClientAgent?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 29 + trackMbId?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 30 + trackName?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 31 + uri?: FmTealAlphaFeedPlayFieldCondition | null | undefined; 32 + }; 33 + export type FmTealAlphaFeedPlayFieldCondition = { 34 + contains?: string | null | undefined; 35 + eq?: string | null | undefined; 36 + gt?: string | null | undefined; 37 + gte?: string | null | undefined; 38 + in?: ReadonlyArray<string> | null | undefined; 39 + lt?: string | null | undefined; 40 + lte?: string | null | undefined; 41 + }; 42 + export type TopTracksQuery$variables = { 43 + where?: FmTealAlphaFeedPlayWhereInput | null | undefined; 44 + }; 13 45 export type TopTracksQuery$data = { 14 - readonly fmTealAlphaFeedPlaysAggregated: ReadonlyArray<{ 15 - readonly artists: any | null | undefined; 46 + readonly fmTealAlphaFeedPlayAggregated: ReadonlyArray<{ 47 + readonly artists: string | null | undefined; 16 48 readonly count: number; 17 - readonly releaseMbId: any | null | undefined; 18 - readonly trackName: any | null | undefined; 19 - }>; 49 + readonly releaseMbId: string | null | undefined; 50 + readonly trackName: string | null | undefined; 51 + }> | null | undefined; 20 52 }; 21 53 export type TopTracksQuery = { 22 54 response: TopTracksQuery$data; ··· 26 58 const node: ConcreteRequest = (function(){ 27 59 var v0 = [ 28 60 { 61 + "defaultValue": null, 62 + "kind": "LocalArgument", 63 + "name": "where" 64 + } 65 + ], 66 + v1 = [ 67 + { 29 68 "alias": null, 30 69 "args": [ 31 70 { 32 71 "kind": "Literal", 33 72 "name": "groupBy", 34 73 "value": [ 35 - "trackName", 36 - "releaseMbId", 37 - "artists" 74 + { 75 + "field": "trackName" 76 + }, 77 + { 78 + "field": "releaseMbId" 79 + }, 80 + { 81 + "field": "artists" 82 + } 38 83 ] 39 84 }, 40 85 { ··· 46 91 "kind": "Literal", 47 92 "name": "orderBy", 48 93 "value": { 49 - "count": "desc" 94 + "count": "DESC" 50 95 } 96 + }, 97 + { 98 + "kind": "Variable", 99 + "name": "where", 100 + "variableName": "where" 51 101 } 52 102 ], 53 103 "concreteType": "FmTealAlphaFeedPlayAggregated", 54 104 "kind": "LinkedField", 55 - "name": "fmTealAlphaFeedPlaysAggregated", 105 + "name": "fmTealAlphaFeedPlayAggregated", 56 106 "plural": true, 57 107 "selections": [ 58 108 { ··· 84 134 "storageKey": null 85 135 } 86 136 ], 87 - "storageKey": "fmTealAlphaFeedPlaysAggregated(groupBy:[\"trackName\",\"releaseMbId\",\"artists\"],limit:50,orderBy:{\"count\":\"desc\"})" 137 + "storageKey": null 88 138 } 89 139 ]; 90 140 return { 91 141 "fragment": { 92 - "argumentDefinitions": [], 142 + "argumentDefinitions": (v0/*: any*/), 93 143 "kind": "Fragment", 94 144 "metadata": null, 95 145 "name": "TopTracksQuery", 96 - "selections": (v0/*: any*/), 146 + "selections": (v1/*: any*/), 97 147 "type": "Query", 98 148 "abstractKey": null 99 149 }, 100 150 "kind": "Request", 101 151 "operation": { 102 - "argumentDefinitions": [], 152 + "argumentDefinitions": (v0/*: any*/), 103 153 "kind": "Operation", 104 154 "name": "TopTracksQuery", 105 - "selections": (v0/*: any*/) 155 + "selections": (v1/*: any*/) 106 156 }, 107 157 "params": { 108 - "cacheID": "61e9f7886dfe9eaeb599b939f2d636e5", 158 + "cacheID": "bc9ed2b6c355b3a8fedb84cb713fa8de", 109 159 "id": null, 110 160 "metadata": {}, 111 161 "name": "TopTracksQuery", 112 162 "operationKind": "query", 113 - "text": "query TopTracksQuery {\n fmTealAlphaFeedPlaysAggregated(groupBy: [\"trackName\", \"releaseMbId\", \"artists\"], orderBy: {count: desc}, limit: 50) {\n trackName\n releaseMbId\n artists\n count\n }\n}\n" 163 + "text": "query TopTracksQuery(\n $where: FmTealAlphaFeedPlayWhereInput\n) {\n fmTealAlphaFeedPlayAggregated(groupBy: [{field: trackName}, {field: releaseMbId}, {field: artists}], orderBy: {count: DESC}, limit: 50, where: $where) {\n trackName\n releaseMbId\n artists\n count\n }\n}\n" 114 164 } 115 165 }; 116 166 })(); 117 167 118 - (node as any).hash = "536f8ddb64daa09017abff121d7ea8ce"; 168 + (node as any).hash = "d2aadd883f60f6f31f8d466dc9653cd8"; 119 169 120 170 export default node;
+28 -7
src/__generated__/TrackItem_play.graphql.ts
··· 1 1 /** 2 - * @generated SignedSource<<1403d6e3403844ad955c2fe48c8a66c9>> 2 + * @generated SignedSource<<d110bc257911ffb384b30e7e973211c9>> 3 3 * @lightSyntaxTransform 4 4 * @nogrep 5 5 */ ··· 12 12 import { FragmentRefs } from "relay-runtime"; 13 13 export type TrackItem_play$data = { 14 14 readonly actorHandle: string | null | undefined; 15 - readonly appBskyActorProfile: { 15 + readonly appBskyActorProfileByDid: { 16 16 readonly displayName: string | null | undefined; 17 17 } | null | undefined; 18 - readonly artists: any | null | undefined; 18 + readonly artists: ReadonlyArray<{ 19 + readonly artistName: string; 20 + }> | null | undefined; 21 + readonly musicServiceBaseDomain: string | null | undefined; 19 22 readonly playedTime: string | null | undefined; 20 23 readonly releaseMbId: string | null | undefined; 21 24 readonly releaseName: string | null | undefined; 22 - readonly trackName: string; 25 + readonly trackName: string | null | undefined; 23 26 readonly " $fragmentType": "TrackItem_play"; 24 27 }; 25 28 export type TrackItem_play$key = { ··· 50 53 { 51 54 "alias": null, 52 55 "args": null, 53 - "kind": "ScalarField", 56 + "concreteType": "FmTealAlphaFeedDefsArtist", 57 + "kind": "LinkedField", 54 58 "name": "artists", 59 + "plural": true, 60 + "selections": [ 61 + { 62 + "alias": null, 63 + "args": null, 64 + "kind": "ScalarField", 65 + "name": "artistName", 66 + "storageKey": null 67 + } 68 + ], 55 69 "storageKey": null 56 70 }, 57 71 { ··· 78 92 { 79 93 "alias": null, 80 94 "args": null, 95 + "kind": "ScalarField", 96 + "name": "musicServiceBaseDomain", 97 + "storageKey": null 98 + }, 99 + { 100 + "alias": null, 101 + "args": null, 81 102 "concreteType": "AppBskyActorProfile", 82 103 "kind": "LinkedField", 83 - "name": "appBskyActorProfile", 104 + "name": "appBskyActorProfileByDid", 84 105 "plural": false, 85 106 "selections": [ 86 107 { ··· 98 119 "abstractKey": null 99 120 }; 100 121 101 - (node as any).hash = "08e8e2c14a894471e9a3153f8918e02e"; 122 + (node as any).hash = "93f45db972efd335604fbc28995328de"; 102 123 103 124 export default node;
+37
src/generateChartData.ts
··· 1 + export interface DataPoint { 2 + date: string; 3 + count: number; 4 + } 5 + 6 + export function generateChartData( 7 + plays: readonly { readonly playedTime?: string | null; readonly [key: string]: any }[], 8 + days = 90 9 + ): DataPoint[] { 10 + const counts = new Map<string, number>(); 11 + const now = new Date(); 12 + 13 + // Initialize last N days with 0 counts 14 + for (let i = days - 1; i >= 0; i--) { 15 + const date = new Date(now); 16 + date.setDate(date.getDate() - i); 17 + date.setHours(0, 0, 0, 0); 18 + const dateStr = date.toISOString().split("T")[0]; 19 + counts.set(dateStr, 0); 20 + } 21 + 22 + // Count plays per day 23 + plays.forEach((play) => { 24 + if (play?.playedTime) { 25 + const date = new Date(play.playedTime); 26 + date.setHours(0, 0, 0, 0); 27 + const dateStr = date.toISOString().split("T")[0]; 28 + if (counts.has(dateStr)) { 29 + counts.set(dateStr, (counts.get(dateStr) || 0) + 1); 30 + } 31 + } 32 + }); 33 + 34 + return Array.from(counts.entries()) 35 + .map(([date, count]) => ({ date, count })) 36 + .sort((a, b) => a.date.localeCompare(b.date)); 37 + }
+10 -9
src/main.tsx
··· 1 1 import { StrictMode, Suspense } from "react"; 2 2 import { createRoot } from "react-dom/client"; 3 - import { BrowserRouter, Routes, Route } from "react-router-dom"; 3 + import { BrowserRouter, Route, Routes } from "react-router-dom"; 4 4 import "./index.css"; 5 5 import App from "./App.tsx"; 6 6 import Profile from "./Profile.tsx"; ··· 10 10 import { RelayEnvironmentProvider } from "react-relay"; 11 11 import { 12 12 Environment, 13 - Network, 14 13 type FetchFunction, 14 + type GraphQLResponse, 15 + Network, 15 16 Observable, 16 17 type SubscribeFunction, 17 - type GraphQLResponse, 18 18 } from "relay-runtime"; 19 19 import { createClient } from "graphql-ws"; 20 20 21 21 const HTTP_ENDPOINT = 22 - "https://api.slices.network/graphql?slice=at://did:plc:fpruhuo22xkm5o7ttr2ktxdo/network.slices.slice/3m257yljpbg2a"; 22 + "https://quickslice-production-d668.up.railway.app/graphql"; 23 23 24 - const WS_ENDPOINT = 25 - "wss://api.slices.network/graphql/ws?slice=at://did:plc:fpruhuo22xkm5o7ttr2ktxdo/network.slices.slice/3m257yljpbg2a"; 24 + const WS_ENDPOINT = "wss://quickslice-production-d668.up.railway.app/graphql"; 26 25 27 26 const fetchGraphQL: FetchFunction = async (request, variables) => { 28 27 const resp = await fetch(HTTP_ENDPOINT, { ··· 78 77 sink.error(error); 79 78 } else if (error instanceof CloseEvent) { 80 79 sink.error( 81 - new Error(`WebSocket closed: ${error.code} ${error.reason}`) 80 + new Error(`WebSocket closed: ${error.code} ${error.reason}`), 82 81 ); 83 82 } else { 84 83 sink.error(new Error(JSON.stringify(error))); 85 84 } 86 85 }, 87 86 complete: () => sink.complete(), 88 - } 87 + }, 89 88 ); 90 89 }); 91 90 }; ··· 102 101 <Routes> 103 102 <Route path="/" element={<App />} /> 104 103 <Route path="/tracks" element={<TopTracks />} /> 104 + <Route path="/tracks/:period" element={<TopTracks />} /> 105 105 <Route path="/albums" element={<TopAlbums />} /> 106 + <Route path="/albums/:period" element={<TopAlbums />} /> 106 107 <Route path="/profile/:handle" element={<Profile />} /> 107 108 </Routes> 108 109 </Suspense> 109 110 </RelayEnvironmentProvider> 110 111 </BrowserRouter> 111 - </StrictMode> 112 + </StrictMode>, 112 113 );
+31
src/useDateRangeFilter.ts
··· 1 + import { useMemo } from "react"; 2 + 3 + export function useDateRangeFilter(period: string | undefined) { 4 + return useMemo(() => { 5 + if (!period || period === "all") { 6 + return { where: undefined }; 7 + } 8 + 9 + // Round to start of current day to keep the timestamp stable 10 + const now = new Date(); 11 + now.setHours(0, 0, 0, 0); 12 + 13 + let daysAgo = 0; 14 + switch (period) { 15 + case "daily": 16 + daysAgo = 1; 17 + break; 18 + case "weekly": 19 + daysAgo = 7; 20 + break; 21 + case "monthly": 22 + daysAgo = 30; 23 + break; 24 + default: 25 + return { where: undefined }; 26 + } 27 + 28 + const startDate = new Date(now.getTime() - daysAgo * 24 * 60 * 60 * 1000); 29 + return { where: { playedTime: { gte: startDate.toISOString() } } }; 30 + }, [period]); 31 + }