feat(activity): per-viewer archive and hide
Two new per-viewer flags on activities, mirroring the heart/done shape: - ARCHIVE: any viewer (incl. the owner) can archive a row. Out of sight by default but the permalink still resolves. For owners, archive also filters the row out of their /<bruker>/liste public page — "I'm done sharing this." - HIDE: only non-owners. "This doesn't appeal to me." Endpoint returns 400 cannot_hide_own when the owner tries it (they should delete or archive instead). Both default-filtered from GET /api/activities. Opt-in via query params with three modes each: exclude (default), include (1), only. ?archived=1&hidden=1 includes both; ?archived=only shows just the archive. The list endpoint composes the SQL filters per-viewer (anonymous viewers have no rows in either table → no-ops). Schema: two new tables user_archived_activities and user_hidden_activities, both composite-PK on (user_id, activity_id), cascade on both refs. Same shape as the existing engagement tables. Types: viewer_archived + viewer_hidden on every Activity variant. viewer_hidden on private is always false (the endpoint refuses); docstring on the type explains why. Bulk lookups extended so the list endpoint stays at constant queries. Single-row paths get per-row viewerArchived/viewerHidden helpers. fetchRowForViewer keeps preserving the viewer's custom sort_position on toggles. UI: two new buttons in ActivityRow's action row — "📦 Arkiver" / "📦 Arkivert" (everyone) and "🙈 Gjem" / "🙈 Skjult" (non-owners only). Two new checkboxes near the search field on /hjem: "📦 Vis arkivert" and "🙈 Vis skjult". Toggling either re-fetches from the server because the filtering is server-side. When an archive/hide flips the row out of the current view, Home.svelte triggers a refetch so the row disappears in real time. Public-list endpoint (/api/users/:bruker/list) also filters out the owner's own archived rows — consistent with "archive means filed away from active view." Tests: 3 new in engagement.test.ts — viewer archives + per-viewer isolation, owner's archive filters their public list, hide refuses on own row + ?hidden=only path. Suite goes 102 → 105.
This commit is contained in:
parent
ef02b3f585
commit
6e005fc2d7
8 changed files with 418 additions and 7 deletions
|
|
@ -188,6 +188,99 @@ describe('"gjort" (done) marks', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('archive + hide (per-viewer)', () => {
|
||||
ttest('viewer can archive any visibility they see; default list excludes archived', async () => {
|
||||
const [owner, viewer] = await Promise.all([
|
||||
signupAndGetCookie(ctx, 'arch-owner@test.invalid'),
|
||||
signupAndGetCookie(ctx, 'arch-viewer@test.invalid'),
|
||||
]);
|
||||
const pub = await createActivity(ctx, owner.cookie, {
|
||||
visibility: 'public', title: 'arch-pub', tags: [],
|
||||
});
|
||||
|
||||
// Default list includes it.
|
||||
let list = await listActivities(ctx, viewer.cookie);
|
||||
expect(list.find((a) => a.id === pub.id)).toBeTruthy();
|
||||
|
||||
// Archive it.
|
||||
const archived = await reqJson<Activity>(ctx, 'POST', `/api/activities/${pub.id}/archive`, {
|
||||
cookie: viewer.cookie,
|
||||
});
|
||||
expect(archived.viewer_archived).toBe(true);
|
||||
|
||||
// Disappears from the default list.
|
||||
list = await listActivities(ctx, viewer.cookie);
|
||||
expect(list.find((a) => a.id === pub.id)).toBeUndefined();
|
||||
|
||||
// ?archived=1 brings it back.
|
||||
const withArch = await reqJson<Activity[]>(ctx, 'GET', '/api/activities?archived=1', {
|
||||
cookie: viewer.cookie,
|
||||
});
|
||||
expect(withArch.find((a) => a.id === pub.id)).toBeTruthy();
|
||||
|
||||
// Other viewer is unaffected — archive is per-viewer.
|
||||
const otherList = await listActivities(ctx, owner.cookie);
|
||||
expect(otherList.find((a) => a.id === pub.id)?.viewer_archived).toBe(false);
|
||||
});
|
||||
|
||||
ttest('owner can archive their own row (own public list filters it too)', async () => {
|
||||
const owner = await signupAndGetCookie(ctx, 'arch-self@test.invalid');
|
||||
// Owner needs a username + public_list_enabled to test the public-list filter.
|
||||
await reqJson(ctx, 'PATCH', '/api/auth/profile', {
|
||||
cookie: owner.cookie,
|
||||
body: { username: 'archself', public_list_enabled: true, display_name: 'Arch Self' },
|
||||
});
|
||||
const pub = await createActivity(ctx, owner.cookie, {
|
||||
visibility: 'public', title: 'arch-own-pub', tags: [],
|
||||
});
|
||||
|
||||
// Visible on the owner's public list initially.
|
||||
const before = await reqJson<{ activities: Activity[] }>(ctx, 'GET', '/api/users/archself/list');
|
||||
expect(before.activities.find((a) => a.id === pub.id)).toBeTruthy();
|
||||
|
||||
// Owner archives → drops off their public list.
|
||||
await req(ctx, 'POST', `/api/activities/${pub.id}/archive`, { cookie: owner.cookie });
|
||||
const after = await reqJson<{ activities: Activity[] }>(ctx, 'GET', '/api/users/archself/list');
|
||||
expect(after.activities.find((a) => a.id === pub.id)).toBeUndefined();
|
||||
});
|
||||
|
||||
ttest('hide refuses on owner\'s own row; works on others\'; per-viewer', async () => {
|
||||
const [owner, viewer] = await Promise.all([
|
||||
signupAndGetCookie(ctx, 'hide-owner@test.invalid'),
|
||||
signupAndGetCookie(ctx, 'hide-viewer@test.invalid'),
|
||||
]);
|
||||
const pub = await createActivity(ctx, owner.cookie, {
|
||||
visibility: 'public', title: 'hide-test', tags: [],
|
||||
});
|
||||
|
||||
// Owner trying to hide their own row → 400.
|
||||
const ownerHide = await req(ctx, 'POST', `/api/activities/${pub.id}/hide`, {
|
||||
cookie: owner.cookie,
|
||||
});
|
||||
expect(ownerHide.status).toBe(400);
|
||||
|
||||
// Non-owner viewer can hide it.
|
||||
const hidden = await reqJson<Activity>(ctx, 'POST', `/api/activities/${pub.id}/hide`, {
|
||||
cookie: viewer.cookie,
|
||||
});
|
||||
expect(hidden.viewer_hidden).toBe(true);
|
||||
|
||||
// Disappears from the viewer's default list…
|
||||
const list = await listActivities(ctx, viewer.cookie);
|
||||
expect(list.find((a) => a.id === pub.id)).toBeUndefined();
|
||||
|
||||
// …but ?hidden=only surfaces just the hidden ones.
|
||||
const only = await reqJson<Activity[]>(ctx, 'GET', '/api/activities?hidden=only', {
|
||||
cookie: viewer.cookie,
|
||||
});
|
||||
expect(only.find((a) => a.id === pub.id)).toBeTruthy();
|
||||
|
||||
// Owner's view is unaffected.
|
||||
const ownerList = await listActivities(ctx, owner.cookie);
|
||||
expect(ownerList.find((a) => a.id === pub.id)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('bookmarks', () => {
|
||||
ttest('toggle, idempotent, refused on private', async () => {
|
||||
const [owner, viewer, otherViewer] = await Promise.all([
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue