Skip to content

Commit 52ff84d

Browse files
committed
Make scroll sessions more dynamic
1 parent fbe3a00 commit 52ff84d

File tree

9 files changed

+380
-84
lines changed

9 files changed

+380
-84
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"use server";
2+
3+
import { knex } from "../../../db";
4+
5+
export async function logUserEvent(sessionId: string, event: UserEvent) {
6+
const timestamp = Date.now();
7+
8+
await knex("user_log_events").insert({
9+
session_id: sessionId,
10+
timestamp: timestamp,
11+
metadata: JSON.stringify(event),
12+
});
13+
}
14+
15+
type UserEvent =
16+
| {
17+
type: "session_start";
18+
}
19+
| {
20+
type: "session_end";
21+
reason: "unmount" | "before_unload";
22+
}
23+
| {
24+
type: "skin_view_start";
25+
skinMd5: string;
26+
}
27+
| {
28+
type: "skin_view_end";
29+
skinMd5: string;
30+
durationMs: number;
31+
}
32+
| {
33+
type: "skins_fetch_start";
34+
offset: number;
35+
}
36+
| {
37+
type: "skins_fetch_success";
38+
offset: number;
39+
}
40+
| {
41+
type: "skins_fetch_failure";
42+
offset: number;
43+
errorMessage: string;
44+
}
45+
| {
46+
type: "readme_expand";
47+
skinMd5: string;
48+
}
49+
| {
50+
type: "skin_download";
51+
skinMd5: string;
52+
}
53+
| {
54+
type: "share_open";
55+
skinMd5: string;
56+
}
57+
| {
58+
type: "share_success";
59+
skinMd5: string;
60+
}
61+
| {
62+
type: "share_failure";
63+
skinMd5: string;
64+
errorMessage: string;
65+
};
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"use client";
2+
3+
import { ClientSkin } from "./SkinScroller";
4+
5+
type Props = {
6+
skin: ClientSkin;
7+
index: number;
8+
sessionId: string;
9+
};
10+
11+
export default function SkinPage({ skin, index }: Props) {
12+
return (
13+
<div
14+
key={skin.md5}
15+
skin-md5={skin.md5}
16+
skin-index={index}
17+
className="scroller"
18+
style={{
19+
display: "flex",
20+
flexDirection: "column",
21+
width: "100%",
22+
height: "100vh",
23+
scrollSnapAlign: "start",
24+
scrollSnapStop: "always",
25+
}}
26+
>
27+
<img
28+
src={skin.screenshotUrl}
29+
alt={skin.fileName}
30+
style={{
31+
paddingTop: "4rem",
32+
boxSizing: "border-box",
33+
width: "100%",
34+
imageRendering: "pixelated",
35+
}}
36+
/>
37+
<div
38+
style={{
39+
color: "white",
40+
flexGrow: 1,
41+
paddingLeft: "0.5rem",
42+
paddingTop: "0.5rem",
43+
}}
44+
>
45+
<h2
46+
style={{
47+
marginBottom: 0,
48+
fontSize: "0.9rem",
49+
paddingBottom: "0",
50+
fontFamily: 'Arial, "Helvetica Neue", Helvetica, sans-serif',
51+
color: "#ccc",
52+
wordBreak: "break-all",
53+
}}
54+
>
55+
{skin.fileName}
56+
</h2>
57+
<p
58+
style={{
59+
marginTop: "0.5rem",
60+
fontSize: "0.75rem",
61+
paddingTop: "0",
62+
color: "#999",
63+
fontFamily: 'monospace, "Courier New", Courier, monospace',
64+
overflow: "hidden",
65+
}}
66+
>
67+
{skin.readmeStart}
68+
</p>
69+
</div>
70+
</div>
71+
);
72+
}

packages/skin-database/app/(modern)/scroll/SkinScroller.tsx

Lines changed: 74 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"use client";
22

3-
import { useState, useLayoutEffect } from "react";
3+
import { useState, useLayoutEffect, useEffect } from "react";
4+
import SkinPage from "./SkinPage";
5+
import { logUserEvent } from "./Events";
46

57
export type ClientSkin = {
68
screenshotUrl: string;
@@ -11,10 +13,15 @@ export type ClientSkin = {
1113

1214
type Props = {
1315
initialSkins: ClientSkin[];
14-
getSkins: (offset: number) => Promise<ClientSkin[]>;
16+
getSkins: (sessionId: string, offset: number) => Promise<ClientSkin[]>;
17+
sessionId: string;
1518
};
1619

17-
export default function SkinScroller({ initialSkins, getSkins }: Props) {
20+
export default function SkinScroller({
21+
initialSkins,
22+
getSkins,
23+
sessionId,
24+
}: Props) {
1825
const [skins, setSkins] = useState<ClientSkin[]>(initialSkins);
1926
const [visibleSkinIndex, setVisibleSkinIndex] = useState(0);
2027
const [fetching, setFetching] = useState(false);
@@ -38,16 +45,72 @@ export default function SkinScroller({ initialSkins, getSkins }: Props) {
3845
};
3946
}, [containerRef]);
4047

48+
useEffect(() => {
49+
logUserEvent(sessionId, {
50+
type: "session_start",
51+
});
52+
53+
function beforeUnload() {
54+
logUserEvent(sessionId, {
55+
type: "session_end",
56+
reason: "before_unload",
57+
});
58+
}
59+
60+
addEventListener("beforeunload", beforeUnload);
61+
return () => {
62+
removeEventListener("beforeunload", beforeUnload);
63+
logUserEvent(sessionId, {
64+
type: "session_end",
65+
reason: "unmount",
66+
});
67+
};
68+
}, []);
69+
70+
useEffect(() => {
71+
logUserEvent(sessionId, {
72+
type: "skin_view_start",
73+
skinMd5: skins[visibleSkinIndex].md5,
74+
});
75+
const startTime = Date.now();
76+
return () => {
77+
const durationMs = Date.now() - startTime;
78+
logUserEvent(sessionId, {
79+
type: "skin_view_end",
80+
skinMd5: skins[visibleSkinIndex].md5,
81+
durationMs,
82+
});
83+
};
84+
}, [visibleSkinIndex, skins, fetching]);
85+
4186
useLayoutEffect(() => {
4287
if (fetching) {
4388
return;
4489
}
4590
if (visibleSkinIndex + 5 >= skins.length) {
4691
setFetching(true);
47-
getSkins(skins.length).then((newSkins) => {
48-
setSkins([...skins, ...newSkins]);
49-
setFetching(false);
92+
console.log("Fetching more skins...");
93+
logUserEvent(sessionId, {
94+
type: "skins_fetch_start",
95+
offset: skins.length,
5096
});
97+
getSkins(sessionId, skins.length)
98+
.then((newSkins) => {
99+
logUserEvent(sessionId, {
100+
type: "skins_fetch_success",
101+
offset: skins.length,
102+
});
103+
setSkins([...skins, ...newSkins]);
104+
setFetching(false);
105+
})
106+
.catch((error) => {
107+
logUserEvent(sessionId, {
108+
type: "skins_fetch_failure",
109+
offset: skins.length,
110+
errorMessage: error.message,
111+
});
112+
setFetching(false);
113+
});
51114
}
52115
}, [visibleSkinIndex, skins, fetching]);
53116

@@ -62,64 +125,12 @@ export default function SkinScroller({ initialSkins, getSkins }: Props) {
62125
>
63126
{skins.map((skin, i) => {
64127
return (
65-
<div
128+
<SkinPage
66129
key={skin.md5}
67-
skin-md5={skin.md5}
68-
skin-index={i}
69-
className="scroller"
70-
style={{
71-
display: "flex",
72-
flexDirection: "column",
73-
width: "100%",
74-
height: "100vh",
75-
scrollSnapAlign: "start",
76-
scrollSnapStop: "always",
77-
}}
78-
>
79-
<img
80-
src={skin.screenshotUrl}
81-
alt={skin.fileName}
82-
style={{
83-
paddingTop: "4rem",
84-
boxSizing: "border-box",
85-
width: "100%",
86-
imageRendering: "pixelated",
87-
}}
88-
/>
89-
<div
90-
style={{
91-
color: "white",
92-
flexGrow: 1,
93-
paddingLeft: "0.5rem",
94-
paddingTop: "0.5rem",
95-
}}
96-
>
97-
<h2
98-
style={{
99-
marginBottom: 0,
100-
fontSize: "0.9rem",
101-
paddingBottom: "0",
102-
fontFamily: 'Arial, "Helvetica Neue", Helvetica, sans-serif',
103-
color: "#ccc",
104-
wordBreak: "break-all",
105-
}}
106-
>
107-
{skin.fileName}
108-
</h2>
109-
<p
110-
style={{
111-
marginTop: "0.5rem",
112-
fontSize: "0.75rem",
113-
paddingTop: "0",
114-
color: "#999",
115-
fontFamily: 'monospace, "Courier New", Courier, monospace',
116-
overflow: "hidden",
117-
}}
118-
>
119-
{skin.readmeStart}
120-
</p>
121-
</div>
122-
</div>
130+
skin={skin}
131+
index={i}
132+
sessionId={sessionId}
133+
/>
123134
);
124135
})}
125136
</div>
Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,49 @@
1-
import ClassicSkinResolver from "../../../api/graphql/resolvers/ClassicSkinResolver";
2-
import SkinsConnection from "../../../api/graphql/SkinsConnection";
31
import UserContext from "../../../data/UserContext";
2+
import SessionModel from "../../../data/SessionModel";
43
import "./scroll.css";
54
import SkinScroller, { ClientSkin } from "./SkinScroller";
5+
import { getScrollPage } from "../../../data/skins";
6+
import SkinModel from "../../../data/SkinModel";
67

7-
async function getClientSkins(offset: number): Promise<ClientSkin[]> {
8+
async function getClientSkins(sessionId: string): Promise<ClientSkin[]> {
89
"use server";
910
const ctx = new UserContext();
10-
const connection = new SkinsConnection(10, offset, "MUSEUM", null);
11-
const skins = await connection.nodes(ctx);
12-
if (skins == null) {
13-
return [];
14-
}
15-
const classicSkins = skins.filter(
16-
(skin): skin is ClassicSkinResolver => skin instanceof ClassicSkinResolver
17-
);
18-
const clientSkins: ClientSkin[] = await Promise.all(
19-
classicSkins.map(async (skin) => {
20-
const url = await skin.screenshot_url();
21-
const readmeText = await skin.readme_text();
11+
12+
const page = await getScrollPage(sessionId);
13+
14+
const skins = await Promise.all(
15+
page.map(async (item) => {
16+
const model = await SkinModel.fromMd5Assert(ctx, item.md5);
17+
const readmeText = await model.getReadme();
2218
return {
23-
screenshotUrl: url,
24-
md5: skin.md5(),
25-
fileName: await skin.filename(true),
19+
screenshotUrl: model.getScreenshotUrl(),
20+
md5: item.md5,
21+
// TODO: Normalize to .wsz
22+
fileName: await model.getFileName(),
2623
readmeStart: readmeText ? readmeText.slice(0, 200) : "",
2724
};
2825
})
2926
);
30-
return clientSkins;
27+
for (const skin of skins) {
28+
SessionModel.addSkin(sessionId, skin.md5);
29+
}
30+
return skins;
3131
}
3232

3333
/**
3434
* A tik-tok style scroll page where we display one skin at a time in full screen
3535
*/
3636
export default async function ScrollPage() {
37-
const initialSkins = await getClientSkins(0);
37+
// Create the session in the database
38+
const sessionId = await SessionModel.create();
39+
40+
const initialSkins = await getClientSkins(sessionId);
3841

39-
return <SkinScroller initialSkins={initialSkins} getSkins={getClientSkins} />;
42+
return (
43+
<SkinScroller
44+
initialSkins={initialSkins}
45+
getSkins={getClientSkins}
46+
sessionId={sessionId}
47+
/>
48+
);
4049
}

0 commit comments

Comments
 (0)