+30
-2
__generated__/lexicons.ts
+30
-2
__generated__/lexicons.ts
···
2684
2684
description: {
2685
2685
type: 'string',
2686
2686
},
2687
+
cameras: {
2688
+
type: 'array',
2689
+
description:
2690
+
'List of camera make and models used in this gallery derived from EXIF data.',
2691
+
items: {
2692
+
type: 'string',
2693
+
},
2694
+
},
2687
2695
facets: {
2688
2696
type: 'array',
2689
2697
description:
···
2719
2727
type: 'ref',
2720
2728
ref: 'lex:com.atproto.label.defs#label',
2721
2729
},
2730
+
},
2731
+
createdAt: {
2732
+
type: 'string',
2733
+
format: 'datetime',
2722
2734
},
2723
2735
indexedAt: {
2724
2736
type: 'string',
···
3214
3226
defs: {
3215
3227
profileView: {
3216
3228
type: 'object',
3217
-
required: ['did', 'handle'],
3229
+
required: ['cid', 'did', 'handle'],
3218
3230
properties: {
3231
+
cid: {
3232
+
type: 'string',
3233
+
format: 'cid',
3234
+
},
3219
3235
did: {
3220
3236
type: 'string',
3221
3237
format: 'did',
···
3253
3269
},
3254
3270
profileViewDetailed: {
3255
3271
type: 'object',
3256
-
required: ['did', 'handle'],
3272
+
required: ['cid', 'did', 'handle'],
3257
3273
properties: {
3274
+
cid: {
3275
+
type: 'string',
3276
+
format: 'cid',
3277
+
},
3258
3278
did: {
3259
3279
type: 'string',
3260
3280
format: 'did',
···
3276
3296
avatar: {
3277
3297
type: 'string',
3278
3298
format: 'uri',
3299
+
},
3300
+
cameras: {
3301
+
type: 'array',
3302
+
items: {
3303
+
type: 'string',
3304
+
},
3305
+
description:
3306
+
'List of camera make and models used by this actor derived from EXIF data of photos linked to galleries.',
3279
3307
},
3280
3308
followersCount: {
3281
3309
type: 'integer',
+4
-7
src/components/GalleryInfo.tsx
+4
-7
src/components/GalleryInfo.tsx
···
1
-
import { Record as Gallery } from "$lexicon/types/social/grain/gallery.ts";
2
1
import { GalleryView } from "$lexicon/types/social/grain/gallery/defs.ts";
3
-
import { getGalleryCameras } from "../lib/gallery.ts";
4
2
import { ActorInfo } from "./ActorInfo.tsx";
5
3
import { CameraBadges } from "./CameraBadges.tsx";
6
4
import { RenderFacetedText } from "./RenderFacetedText.tsx";
···
8
6
export function GalleryInfo(
9
7
{ gallery }: Readonly<{ gallery: GalleryView }>,
10
8
) {
11
-
const description = (gallery.record as Gallery).description;
12
-
const facets = (gallery.record as Gallery).facets;
13
-
const cameras = getGalleryCameras(gallery);
9
+
const description = gallery.description;
10
+
const facets = gallery.facets;
14
11
return (
15
12
<div
16
13
class="flex flex-col space-y-2 mb-4 max-w-[500px]"
17
14
id="gallery-info"
18
15
>
19
16
<h1 class="font-bold text-2xl">
20
-
{(gallery.record as Gallery).title}
17
+
{gallery.title}
21
18
</h1>
22
19
<ActorInfo profile={gallery.creator} />
23
20
{description
···
27
24
</p>
28
25
)
29
26
: null}
30
-
<CameraBadges class="my-1" cameras={cameras} />
27
+
<CameraBadges class="my-1" cameras={gallery.cameras ?? []} />
31
28
</div>
32
29
);
33
30
}
+4
-5
src/components/GallerySelectDialog.tsx
+4
-5
src/components/GallerySelectDialog.tsx
···
1
-
import { Record as Gallery } from "$lexicon/types/social/grain/gallery.ts";
2
1
import { GalleryView } from "$lexicon/types/social/grain/gallery/defs.ts";
3
2
import { AtUri } from "@atproto/syntax";
4
3
import { Button } from "./Button.tsx";
···
72
71
hx-swap="none"
73
72
class="block text-left w-full px-2 py-4"
74
73
>
75
-
{(gallery.record as Gallery).title}
74
+
{gallery.title}
76
75
<div class="text-sm text-zinc-600 dark:text-zinc-500">
77
-
{(gallery.record as Gallery).description}
76
+
{gallery.description}
78
77
</div>
79
78
</button>
80
79
)
···
83
82
href={uploadPageLink(gallery.uri)}
84
83
class="block w-full px-2 py-4"
85
84
>
86
-
{(gallery.record as Gallery).title}
85
+
{gallery.title}
87
86
<div class="text-sm text-zinc-600 dark:text-zinc-500">
88
-
{(gallery.record as Gallery).description}
87
+
{gallery.description}
89
88
</div>
90
89
</a>
91
90
)}
+1
-1
src/components/NotificationsPage.tsx
+1
-1
src/components/NotificationsPage.tsx
···
300
300
return (
301
301
<div class="text-sm border-l-2 border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-500 pl-2">
302
302
<RenderFacetedText
303
-
text={(gallery.record as Gallery).description ?? ""}
303
+
text={gallery.description ?? ""}
304
304
facets={galleryRecord.facets}
305
305
/>
306
306
<div class="mt-2 max-w-[200px]">
+8
-20
src/components/ProfilePage.tsx
+8
-20
src/components/ProfilePage.tsx
···
1
1
import { LabelValueDefinition } from "$lexicon/types/com/atproto/label/defs.ts";
2
-
import { ProfileView } from "$lexicon/types/social/grain/actor/defs.ts";
3
-
import { Record as Gallery } from "$lexicon/types/social/grain/gallery.ts";
2
+
import { ProfileViewDetailed } from "$lexicon/types/social/grain/actor/defs.ts";
4
3
import { GalleryView } from "$lexicon/types/social/grain/gallery/defs.ts";
5
4
import { isPhotoView } from "$lexicon/types/social/grain/photo/defs.ts";
6
5
import { Un$Typed } from "$lexicon/util.ts";
7
6
import { Facet } from "@atproto/api";
8
7
import { AtUri } from "@atproto/syntax";
9
8
import { LabelerPolicies } from "@bigmoves/bff";
10
-
import { getGalleryCameras } from "../lib/gallery.ts";
11
9
import {
12
10
atprotoLabelValueDefinitions,
13
11
ModerationDecsion,
···
32
30
export type ProfileTabs = "favs" | "galleries" | "labels";
33
31
34
32
export function ProfilePage({
35
-
followUri,
36
-
followersCount,
37
-
followingCount,
38
33
userProfiles,
39
34
loggedInUserDid,
40
35
profile,
···
46
41
isLabeler,
47
42
labelerDefinitions,
48
43
}: Readonly<{
49
-
followUri?: string;
50
-
followersCount?: number;
51
-
followingCount?: number;
52
44
userProfiles: SocialNetwork[];
53
45
actorProfiles: SocialNetwork[];
54
46
loggedInUserDid?: string;
55
-
profile: Un$Typed<ProfileView>;
47
+
profile: Un$Typed<ProfileViewDetailed>;
56
48
descriptionFacets?: Facet[];
57
49
selectedTab?: ProfileTabs;
58
50
galleries?: GalleryView[];
···
63
55
}>) {
64
56
const isCreator = loggedInUserDid === profile.did;
65
57
const displayName = profile.displayName || profile.handle;
66
-
const cameras = Array.from(
67
-
new Set(galleries?.flatMap(getGalleryCameras) ?? []),
68
-
);
69
58
return (
70
59
<div class="px-4 mb-4" id="profile-page">
71
60
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between my-4">
···
80
69
<p class="space-x-1">
81
70
<a href={followersLink(profile.handle)}>
82
71
<span class="font-semibold" id="followers-count">
83
-
{followersCount ?? 0}
72
+
{profile.followersCount ?? 0}
84
73
</span>{" "}
85
74
<span class="text-zinc-600 dark:text-zinc-500">
86
75
followers
···
88
77
</a>{" "}
89
78
<a href={followingLink(profile.handle)}>
90
79
<span class="font-semibold" id="following-count">
91
-
{followingCount ?? 0}
80
+
{profile.followsCount ?? 0}
92
81
</span>{" "}
93
82
<span class="text-zinc-600 dark:text-zinc-500">
94
83
following
···
97
86
<span class="font-semibold">{galleries?.length ?? 0}</span>
98
87
<span class="text-zinc-600 dark:text-zinc-500">galleries</span>
99
88
</p>
100
-
<CameraBadges cameras={cameras} class="mt-2" />
89
+
<CameraBadges cameras={profile.cameras ?? []} class="mt-2" />
101
90
</>
102
91
)}
103
92
{profile.description
···
135
124
<div class="flex self-start gap-2 w-full sm:w-fit flex-col sm:flex-row">
136
125
<FollowButton
137
126
followeeDid={profile.did}
138
-
followUri={followUri}
127
+
followUri={profile.viewer?.following}
139
128
/>
140
129
</div>
141
130
)
···
356
345
)
357
346
: <div class="w-full h-full bg-zinc-200 dark:bg-zinc-900" />}
358
347
<div class="absolute sm:flex hidden bottom-0 left-0 bg-black/80 text-white p-2 items-center gap-2">
359
-
{(gallery.record as Gallery).title}
348
+
{gallery.title}
360
349
</div>
361
350
</a>
362
351
);
···
387
376
)
388
377
: <div class="w-full h-full bg-zinc-200 dark:bg-zinc-900" />}
389
378
<div class="absolute bottom-0 left-0 bg-black/80 text-white p-2 hidden sm:flex items-center gap-2">
390
-
<ActorAvatar profile={gallery.creator} size={20} />{" "}
391
-
{(gallery.record as Gallery).title}
379
+
<ActorAvatar profile={gallery.creator} size={20} /> {gallery.title}
392
380
</div>
393
381
</a>
394
382
);
+3
-4
src/components/TimelineItem.tsx
+3
-4
src/components/TimelineItem.tsx
···
1
-
import { Record as Gallery } from "$lexicon/types/social/grain/gallery.ts";
2
1
import { isPhotoView } from "$lexicon/types/social/grain/photo/defs.ts";
3
2
import { type TimelineItem } from "../lib/timeline.ts";
4
3
import { CommentsButton } from "../modules/comments.tsx";
···
10
9
import { RenderFacetedText } from "./RenderFacetedText.tsx";
11
10
12
11
export function TimelineItem({ item }: Readonly<{ item: TimelineItem }>) {
13
-
const title = (item.gallery.record as Gallery).title;
14
-
const description = (item.gallery.record as Gallery).description;
15
-
const facets = (item.gallery.record as Gallery).facets;
12
+
const title = item.gallery.title;
13
+
const description = item.gallery.description;
14
+
const facets = item.gallery.facets;
16
15
return (
17
16
<li>
18
17
<div class="flex flex-col pb-4 max-w-md">
+38
-16
src/lib/actor.ts
+38
-16
src/lib/actor.ts
···
10
10
import { Record as Favorite } from "$lexicon/types/social/grain/favorite.ts";
11
11
import { Record as Gallery } from "$lexicon/types/social/grain/gallery.ts";
12
12
import { Record as Photo } from "$lexicon/types/social/grain/photo.ts";
13
+
import { isPhotoView } from "$lexicon/types/social/grain/photo/defs.ts";
13
14
import { Record as PhotoExif } from "$lexicon/types/social/grain/photo/exif.ts";
14
15
import { Un$Typed } from "$lexicon/util.ts";
15
16
import { BffContext, WithBffMeta } from "@bigmoves/bff";
16
17
import { getFollow, getFollowersCount, getFollowsCount } from "./follow.ts";
17
18
import {
18
19
galleryToView,
19
-
getGalleryCount,
20
+
getGalleryCameras,
20
21
getGalleryItemsAndPhotos,
21
22
} from "./gallery.ts";
22
23
import { photoToView, photoUrl } from "./photo.ts";
···
39
40
);
40
41
const followersCount = getFollowersCount(did, ctx);
41
42
const followsCount = getFollowsCount(did, ctx);
42
-
const galleryCount = getGalleryCount(did, ctx);
43
+
const galleries = getActorGalleries(did, ctx);
44
+
const cameras = Array.from(
45
+
new Set(
46
+
galleries.flatMap((g) =>
47
+
getGalleryCameras(g.items?.filter(isPhotoView) ?? [])
48
+
),
49
+
),
50
+
).sort((a, b) => a.localeCompare(b));
43
51
44
52
let followedBy: string | undefined = "";
45
53
let following: string | undefined = "";
···
49
57
}
50
58
51
59
return profileRecord
52
-
? profileDetailedToView(
53
-
profileRecord,
54
-
actor.handle,
60
+
? profileDetailedToView({
61
+
record: profileRecord,
62
+
handle: actor.handle,
63
+
cameras,
55
64
followersCount,
56
65
followsCount,
57
-
galleryCount,
58
-
{
66
+
galleryCount: galleries.length,
67
+
viewer: {
59
68
followedBy,
60
69
following,
61
70
},
62
-
)
71
+
})
63
72
: null;
64
73
}
65
74
···
68
77
handle: string,
69
78
): Un$Typed<ProfileView> {
70
79
return {
80
+
cid: record.cid,
71
81
did: record.did,
72
82
handle,
73
83
displayName: record.displayName,
···
78
88
};
79
89
}
80
90
81
-
export function profileDetailedToView(
82
-
record: WithBffMeta<GrainProfile>,
83
-
handle: string,
84
-
followersCount: number,
85
-
followsCount: number,
86
-
galleryCount: number,
87
-
viewer: ViewerState,
88
-
): Un$Typed<ProfileViewDetailed> {
91
+
export function profileDetailedToView(params: {
92
+
record: WithBffMeta<GrainProfile>;
93
+
handle: string;
94
+
followersCount: number;
95
+
followsCount: number;
96
+
galleryCount: number;
97
+
viewer: ViewerState;
98
+
cameras?: string[];
99
+
}): Un$Typed<ProfileViewDetailed> {
100
+
const {
101
+
record,
102
+
handle,
103
+
followersCount,
104
+
followsCount,
105
+
galleryCount,
106
+
viewer,
107
+
cameras,
108
+
} = params;
89
109
return {
110
+
cid: record.cid,
90
111
did: record.did,
91
112
handle,
92
113
displayName: record.displayName,
···
98
119
followsCount,
99
120
galleryCount,
100
121
viewer,
122
+
cameras,
101
123
};
102
124
}
103
125
+15
-9
src/lib/gallery.ts
+15
-9
src/lib/gallery.ts
···
212
212
favCount?: number;
213
213
commentCount?: number;
214
214
viewerState?: ViewerState;
215
+
cameras?: string[];
215
216
}): $Typed<GalleryView> {
217
+
const viewItems = items
218
+
?.map((item) => itemToView(record.did, item))
219
+
.filter(isPhotoView);
220
+
const cameras = getGalleryCameras(viewItems);
216
221
return {
217
222
$type: "social.grain.gallery.defs#galleryView",
218
223
uri: record.uri,
219
224
cid: record.cid,
220
225
title: record.title,
221
226
description: record.description,
227
+
cameras,
222
228
facets: record.facets,
223
229
creator,
224
230
record,
225
-
items: items
226
-
?.map((item) => itemToView(record.did, item))
227
-
.filter(isPhotoView),
231
+
items: viewItems,
228
232
labels,
229
-
indexedAt: record.indexedAt,
230
233
favCount,
231
234
commentCount,
232
235
viewer: viewerState,
236
+
createdAt: record.createdAt,
237
+
indexedAt: record.indexedAt,
233
238
};
234
239
}
235
240
···
248
253
}
249
254
250
255
export function getGalleryCameras(
251
-
gallery: GalleryView,
256
+
items: Array<PhotoWithExif | PhotoView>,
252
257
): string[] {
253
-
const photos = gallery.items?.filter(isPhotoView) ?? [];
254
258
const cameras = new Set<string>();
255
-
for (const photo of photos) {
256
-
if (photo.exif?.make) {
257
-
cameras.add(`${photo.exif.make} ${photo.exif.model}`.trim());
259
+
if (!Array.isArray(items)) return [];
260
+
for (const item of items) {
261
+
const exif = "exif" in item ? item.exif : (item as PhotoView).exif;
262
+
if (exif?.make && exif?.model) {
263
+
cameras.add(`${exif.make} ${exif.model}`.trim());
258
264
}
259
265
}
260
266
return Array.from(cameras);
+2
-3
src/meta.ts
+2
-3
src/meta.ts
···
1
-
import { Record as Gallery } from "$lexicon/types/social/grain/gallery.ts";
2
1
import { GalleryView } from "$lexicon/types/social/grain/gallery/defs.ts";
3
2
import { isPhotoView } from "$lexicon/types/social/grain/photo/defs.ts";
4
3
import { AtUri } from "@atproto/syntax";
···
29
28
)
30
29
}`,
31
30
},
32
-
{ property: "og:title", content: (gallery.record as Gallery).title },
31
+
{ property: "og:title", content: gallery.title },
33
32
{
34
33
property: "og:description",
35
-
content: (gallery.record as Gallery).description,
34
+
content: gallery.description,
36
35
},
37
36
{
38
37
property: "og:image",
+2
-3
src/modules/comments.tsx
+2
-3
src/modules/comments.tsx
···
1
1
import { ProfileView } from "$lexicon/types/social/grain/actor/defs.ts";
2
2
import { Record as Comment } from "$lexicon/types/social/grain/comment.ts";
3
3
import { CommentView } from "$lexicon/types/social/grain/comment/defs.ts";
4
-
import { Record as Gallery } from "$lexicon/types/social/grain/gallery.ts";
5
4
import {
6
5
GalleryView,
7
6
isGalleryView,
···
64
63
/>
65
64
)}
66
65
{!comment && !photo && gallery &&
67
-
(gallery.record as Gallery).title}
66
+
gallery.title}
68
67
{!comment && !photo && gallery
69
68
? (
70
69
<div class="w-[200px] pointer-events-none">
···
145
144
{gallery.creator
146
145
? <div class="font-semibold">{gallery.creator.displayName}</div>
147
146
: null}
148
-
{(gallery.record as Gallery).title}
147
+
{gallery.title}
149
148
<div class="w-[200px] pointer-events-none">
150
149
<GalleryPreviewLink
151
150
gallery={gallery}
+1
-2
src/routes/gallery.tsx
+1
-2
src/routes/gallery.tsx
···
1
-
import { Record as Gallery } from "$lexicon/types/social/grain/gallery.ts";
2
1
import { BffContext, RouteHandler } from "@bigmoves/bff";
3
2
import { GalleryPage } from "../components/GalleryPage.tsx";
4
3
import { getGallery } from "../lib/gallery.ts";
···
20
19
if (!gallery) return ctx.next();
21
20
22
21
ctx.state.meta = [
23
-
{ title: `${(gallery.record as Gallery).title} — Grain` },
22
+
{ title: `${gallery.title} — Grain` },
24
23
...getPageMeta(galleryLink(handle, rkey)),
25
24
...getGalleryMeta(gallery),
26
25
];
+3
-4
src/routes/hashtag.tsx
+3
-4
src/routes/hashtag.tsx
···
1
-
import { Record as Gallery } from "$lexicon/types/social/grain/gallery.ts";
2
1
import { GalleryView } from "$lexicon/types/social/grain/gallery/defs.ts";
3
2
import { BffContext, RouteHandler } from "@bigmoves/bff";
4
3
import { ActorInfo } from "../components/ActorInfo.tsx";
···
48
47
gallery: GalleryView;
49
48
}>,
50
49
) {
51
-
const title = (gallery.record as Gallery).title;
52
-
const description = (gallery.record as Gallery).description;
53
-
const facets = (gallery.record as Gallery).facets || [];
50
+
const title = gallery.title;
51
+
const description = gallery.description;
52
+
const facets = gallery.facets || [];
54
53
return (
55
54
<div class="flex flex-col gap-2" key={gallery.uri}>
56
55
<ActorInfo profile={gallery.creator} />
+2
-13
src/routes/profile.tsx
+2
-13
src/routes/profile.tsx
···
9
9
import {
10
10
getActorGalleries,
11
11
getActorGalleryFavs,
12
-
getActorProfile,
12
+
getActorProfileDetailed,
13
13
getActorProfiles,
14
14
} from "../lib/actor.ts";
15
-
import { getFollow, getFollowers, getFollowing } from "../lib/follow.ts";
16
15
import {
17
16
isLabeler as isLabelerFn,
18
17
moderateGallery,
···
45
44
const isHxRequest = req.headers.get("hx-request") !== null;
46
45
const render = isHxRequest ? ctx.html : ctx.render;
47
46
48
-
const profile = getActorProfile(actor.did, ctx);
47
+
const profile = getActorProfileDetailed(actor.did, ctx);
49
48
const galleries = getActorGalleries(actor.did, ctx);
50
-
const followers = getFollowers(actor.did, ctx);
51
-
const following = getFollowing(actor.did, ctx);
52
49
53
50
let descriptionFacets: RichText["facets"] = undefined;
54
51
if (profile?.description) {
···
80
77
81
78
if (!profile) return ctx.next();
82
79
83
-
let followUri: string | undefined;
84
80
let actorProfiles: SocialNetwork[] = [];
85
81
let userProfiles: SocialNetwork[] = [];
86
82
87
83
if (ctx.currentUser) {
88
-
followUri = getFollow(profile.did, ctx.currentUser.did, ctx)?.uri;
89
84
actorProfiles = getActorProfiles(ctx.currentUser.did, ctx);
90
85
}
91
86
···
104
99
const galleryFavs = getActorGalleryFavs(actor.did, ctx);
105
100
return render(
106
101
<ProfilePage
107
-
followersCount={followers.length}
108
-
followingCount={following.length}
109
102
userProfiles={userProfiles}
110
103
actorProfiles={actorProfiles}
111
-
followUri={followUri}
112
104
loggedInUserDid={ctx.currentUser?.did}
113
105
profile={profile}
114
106
selectedTab="favs"
···
120
112
}
121
113
return render(
122
114
<ProfilePage
123
-
followersCount={followers.length}
124
-
followingCount={following.length}
125
115
userProfiles={userProfiles}
126
116
actorProfiles={actorProfiles}
127
-
followUri={followUri}
128
117
loggedInUserDid={ctx.currentUser?.did}
129
118
profile={profile}
130
119
descriptionFacets={descriptionFacets}