Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/OysList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export function OysList(props: OysListProps) {
oy.from_user_id === props.currentUserId ? oy.to_user_id : oy.from_user_id;
try {
const response = await props.api<LoHistoryResponse>(
`/api/lo/history?friendId=${friendId}&direction=${direction}`,
`/api/lo/history?friendId=${friendId}&direction=${direction}&before=${oy.created_at}`,
);
setHistoryCache((prev) => {
const next = new Map(prev);
Expand Down
8 changes: 6 additions & 2 deletions src/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ L.Icon.Default.imagePath = "";
type MapContainer = HTMLDivElement & {
_leafletMap?: L.Map;
_leafletTileLayer?: L.TileLayer;
_historyAdded?: boolean;
};

export type LoHistoryPoint = {
Expand Down Expand Up @@ -73,8 +74,9 @@ export function initLocationMap(
history?: LoHistoryPoint[],
) {
if (container.dataset.mapInit === "true") {
if (history && container._leafletMap) {
if (history && container._leafletMap && !container._historyAdded) {
addHistoryMarkers(container._leafletMap, history);
container._historyAdded = true;
}
return;
}
Expand All @@ -96,6 +98,7 @@ export function initLocationMap(

if (history && history.length > 0) {
addHistoryMarkers(map, history);
container._historyAdded = true;
}

L.marker([lat, lon]).addTo(map);
Expand All @@ -121,7 +124,8 @@ function addHistoryMarkers(map: L.Map, history: LoHistoryPoint[]) {
const accent = getCssVar("--accent", "#f59e0b");

for (const point of history) {
const t = point.intensity;
// Cubic curve compresses old points toward purple, spreading recent ones across the color range.
const t = Math.pow(point.intensity, 3);
const color = interpolateColor(primary, accent, t);
L.circleMarker([point.lat, point.lon], {
radius: lerp(3, 7, t),
Expand Down
25 changes: 25 additions & 0 deletions tests/worker/oys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,31 @@ describe("oys and los", () => {
assert.ok(multi.locations[1].intensity > 0 && multi.locations[1].intensity < 1);
});

it("filters to locations at or before the given before timestamp, renormalizing intensity", async () => {
const { env, db } = createTestEnv();
const me = seedUser(db, { username: "Me" });
const friend = seedUser(db, { username: "Friend" });
seedSession(db, me.id, "before-token");

seedOy(db, { fromUserId: friend.id, toUserId: me.id, type: "lo", payload: '{"lat":1.0,"lon":1.0}', createdAt: 100 });
seedOy(db, { fromUserId: friend.id, toUserId: me.id, type: "lo", payload: '{"lat":2.0,"lon":2.0}', createdAt: 200 });
seedOy(db, { fromUserId: friend.id, toUserId: me.id, type: "lo", payload: '{"lat":3.0,"lon":3.0}', createdAt: 300 });

// before=200 should return only createdAt 100 and 200, with intensity re-normalized
const { res, json } = await jsonRequest(
env,
`/api/lo/history?friendId=${friend.id}&direction=inbound&before=200`,
{ headers: { "x-session-token": "before-token" } },
);
const body = json as { locations: Array<{ lat: number; intensity: number }> };
assert.equal(res.status, 200);
assert.equal(body.locations.length, 2);
assert.equal(body.locations[0].lat, 1.0);
assert.equal(body.locations[0].intensity, 0); // oldest
assert.equal(body.locations[1].lat, 2.0);
assert.equal(body.locations[1].intensity, 1); // newest = the current lo, always 1
});

it("cannot fetch another user's location history by spoofing friendId", async () => {
const { env, db } = createTestEnv();
const me = seedUser(db, { username: "Me" });
Expand Down
7 changes: 4 additions & 3 deletions tests/worker/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1559,17 +1559,18 @@ class FakeD1PreparedStatement implements D1PreparedStatement {
"SELECT payload, created_at FROM oys WHERE from_user_id = ? AND to_user_id = ? AND type = 'lo' AND payload IS NOT NULL",
)
) {
const [fromUserId, toUserId, limit] = this.params as [number, number, number];
const [fromUserId, toUserId, before] = this.params as [number, number, number | undefined];
const results = this.db.oys
.filter(
(row) =>
row.from_user_id === fromUserId &&
row.to_user_id === toUserId &&
row.type === "lo" &&
row.payload !== null,
row.payload !== null &&
(before === undefined || row.created_at <= before),
)
.sort((a, b) => b.created_at - a.created_at)
.slice(0, limit)
.slice(0, 50)
.map((row) => ({ payload: row.payload, created_at: row.created_at }));
return { results };
}
Expand Down
8 changes: 7 additions & 1 deletion worker/routes/oys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,9 @@ export function registerOyRoutes(app: App) {

const friendIdRaw = c.req.query("friendId");
const direction = c.req.query("direction");
const beforeRaw = c.req.query("before");
const friendId = friendIdRaw ? Number(friendIdRaw) : Number.NaN;
const before = beforeRaw ? Number(beforeRaw) : undefined;

if (!Number.isFinite(friendId)) {
return c.json({ error: "Missing friendId" }, 400);
Expand All @@ -313,15 +315,19 @@ export function registerOyRoutes(app: App) {
const fromId = direction === "inbound" ? friendId : user.id;
const toId = direction === "inbound" ? user.id : friendId;

const params: (number | undefined)[] = [fromId, toId];
const beforeClause =
before !== undefined ? `AND created_at <= $${params.push(before)}` : "";
const result = await c.get("db").query<{
payload: string | null;
created_at: number;
}>(
`SELECT payload, created_at FROM oys
WHERE from_user_id = $1 AND to_user_id = $2 AND type = 'lo' AND payload IS NOT NULL
${beforeClause}
ORDER BY created_at DESC
LIMIT 50`,
[fromId, toId],
params,
);

// Reverse so oldest is first (index 0), newest is last
Expand Down
Loading