feedback-free.openapps.xyz

Source

This app’s code and revision history are public. Download a copy or explore the files below.

Public
The app’s code and revision history are public.
Owned app · rev 4a4c54cd95c6
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 54b9c9d7fdc2b68029ce2e57
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
← All files
src/lib/votes.ts
// Votes: one record per person per post (plus the same record listed under the person), written in the
// same batch that moves the post's count by exactly one. The policy refuses anything else.
import { get, set, setMany, increment, serverTimestamp } from "@bounded-sh/client";
import { me, signIn } from "./bounded";
import { visitedBoards } from "./mine";

export function castVote(boardId: string, postId: string, uid: string) {
  return setMany([
    { path: `boards/${boardId}/posts/${postId}`, document: { votes: increment(1) } },
    { path: `boards/${boardId}/posts/${postId}/votes/${uid}`, document: { at: serverTimestamp() } },
    { path: `boards/${boardId}/people/${uid}/votes/${postId}`, document: { at: serverTimestamp() } },
  ]);
}

export function removeVote(boardId: string, postId: string, uid: string) {
  return setMany([
    { path: `boards/${boardId}/posts/${postId}`, document: { votes: increment(-1) } },
    { path: `boards/${boardId}/posts/${postId}/votes/${uid}`, document: null },
    { path: `boards/${boardId}/people/${uid}/votes/${postId}`, document: null },
  ]);
}

const rows = (res: any): any[] => (Array.isArray(res) ? res : res?.data ?? []);
const idOf = (r: any) => String(r.id ?? String(r._id ?? "").split("/").pop());

/** Every vote this person has on a board, as post ids. */
export async function votesOn(boardId: string, uid: string): Promise<string[]> {
  return rows(await get(`boards/${boardId}/people/${uid}/votes`, { limit: 1000 })).map(idOf);
}

// Signing in makes a new account, so a guest's votes would stay behind (and count twice if cast again).
// Before leaving for sign-in the guest takes its votes off and notes them here; whoever comes back
// (the account, or the same guest if sign-in was cancelled) puts them back.
const CARRY = "feedback:carry";
interface Carry { from: string; votes: { b: string; p: string }[] }

export async function signInKeepingVotes(): Promise<void> {
  const m = await me();
  if (!m.guest) return;
  const carry: Carry = { from: m.id, votes: [] };
  for (const b of visitedBoards()) {
    let posts: string[] = [];
    try { posts = await votesOn(b, m.id); } catch { continue; /* a board we cannot read anymore */ }
    for (const p of posts) {
      try {
        await removeVote(b, p, m.id);
        carry.votes.push({ b, p });
        localStorage.setItem(CARRY, JSON.stringify(carry));
      } catch { /* this vote stays with the guest */ }
    }
  }
  await signIn();
}

let carried: Promise<number> | null = null;

/** Puts carried votes back under whoever is signed in now. Resolves to how many came back. */
export function restoreCarriedVotes(): Promise<number> {
  carried ??= (async () => {
    let carry: Carry | null = null;
    try { carry = JSON.parse(localStorage.getItem(CARRY) || "null"); } catch { /* ignore */ }
    if (!carry?.votes?.length) { localStorage.removeItem(CARRY); return 0; }
    const m = await me();
    let back = 0;
    for (const { b, p } of carry.votes) {
      try {
        if (!(await get(`boards/${b}/joins/${m.id}`))) await set(`boards/${b}/joins/${m.id}`, { at: serverTimestamp() });
        if (await get(`boards/${b}/people/${m.id}/votes/${p}`)) continue;
        const post = await get(`boards/${b}/posts/${p}`);
        if (!post || post.merged) continue;
        await castVote(b, p, m.id);
        back++;
      } catch { /* the post or board is gone */ }
    }
    localStorage.removeItem(CARRY);
    return back;
  })();
  return carried;
}

Change history

Point store links to openapps.free
4a4c54cd95 · 1 day ago
Release with preview
1d0728818d · 1 day ago
Retry release
1abc843b3f · 1 day ago
Bind app agent address in policy
00eb15f50d · 1 day ago
Use OpenApps service URLs and add contribution strip
347a7b8ca0 · 1 day ago
Bounded source sync
a0aaceae63 · 2 days ago
Bounded source sync
3125b8113f · 2 days ago
Bounded source sync
882a76ba95 · 2 days ago
Bounded source sync
852d9782ee · 2 days ago
Bounded source sync
a8ee705109 · 2 days ago