+3
-3
README.md
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+
}