···1212//
1313// to start, we'll have a static AES-GCM friend key that never rotates
14141515-// we first need to merge ui with auth tho
1515+import Alpine from "alpinejs";
1616+import { goto } from "../utils/tools.ts";
1717+import { Store } from "../utils/store.ts";
1818+import * as api from "../utils/api.ts";
1919+2020+// temp until we have qr codes and actual crng
2121+function generateKey(len = 5): string {
2222+ return Math.random()
2323+ .toString(36)
2424+ .slice(2, 2 + len)
2525+ .toUpperCase();
2626+}
2727+2828+Alpine.data("addFriendPageState", () => ({
2929+ my_user_id: "",
3030+ my_rand_key: generateKey(5),
3131+3232+ friend_id: "",
3333+ friend_rand_key: "",
3434+ friend_name: "",
3535+ is_doing_stuff: false,
3636+3737+ async init() {
3838+ this.my_user_id = await Store.get("user_id");
3939+ },
4040+4141+ goBack() {
4242+ goto("home");
4343+ },
4444+4545+ async sendFriendRequest() {
4646+ this.is_doing_stuff = true;
4747+ try {
4848+ api.requestFriendRequest(this.friend_id);
4949+ const timeoutMs = 60_000;
5050+ const intervalMs = 1_000;
5151+ const start = Date.now();
5252+ let friend_request_accepted = false;
5353+5454+ while (Date.now() - start < timeoutMs && friend_request_accepted === false) {
5555+ friend_request_accepted = await api.isFriendRequestAccepted(this.friend_id);
5656+ await new Promise((r) => setTimeout(r, intervalMs));
5757+ }
5858+5959+ if (friend_request_accepted) {
6060+ const friends = await Store.get("friends");
6161+ friends.push({ name: this.friend_name, id: this.friend_id });
6262+ await Store.set("friends", friends);
6363+ }
6464+6565+ // even if it is false, we should have a longer thing that runs well after
6666+ // we are sure that the request was deleted to be absolutely sure they did not accept it
6767+ // without us knowing
6868+ // TODO: to fix this, later, add an api like IsMyFriendRequestStillActive
6969+ // that checks if a friend request with a single person is still stored. If not,
7070+ // we can forget about it, if it is still there, we keep in storage the fact that it
7171+ // is possible we get an accepted friend request after. We can check like on each app startup or whatnot
7272+ } catch (e) {
7373+ const err = e instanceof Error ? e.message : e;
7474+ alert(`Sign-up failed: ${err}`);
7575+ }
7676+ this.is_doing_stuff = false;
7777+ },
7878+}));
7979+8080+Alpine.start();
+8-30
app/src/home-page/home.html
···3737 <div class="content">
3838 <div class="friends-header">
3939 <h2 style="font-size: 1rem; margin: 0">Friends</h2>
4040- <span style="color: #6b7280; font-size: 0.9rem"
4141- >(<span x-text="friends.length"></span>)</span
4242- >
4040+ <span style="color: #6b7280; font-size: 0.9rem">(<span x-text="friends.length"></span>)</span>
4341 </div>
4444-4242+ <!--TODO idk why but sometimes when i launch the app I see the stuff when you have no friends for a split second before I see them appear, even tho we get them in init -->
4543 <template x-if="friends.length > 0">
4644 <div>
4745 <template x-for="friend in friends" :key="friend.id">
4846 <div class="friend-card">
4947 <strong x-text="friend.name"></strong>
5048 <div class="friend-actions">
5151- <button
5252- class="view-btn"
5353- @click="viewLocation(friend.id)"
5454- >
5555- <img
5656- src="/src/assets/pin.svg"
5757- alt="Pin Icon"
5858- />View
5959- </button>
6060- <span
6161- class="menu-icon"
6262- @click="friendOptions(friend.id)"
6363- ></span>
6464- <img
6565- class="menu-icon"
6666- src="/src/assets/three-dots.svg"
6767- alt="Pin Icon"
6868- />
4949+ <button class="view-btn" @click="viewLocation(friend.id)"><img src="/src/assets/pin.svg" alt="Pin Icon" />View</button>
5050+ <span class="menu-icon" @click="friendOptions(friend.id)"></span>
5151+ <img class="menu-icon" src="/src/assets/three-dots.svg" alt="Pin Icon" />
6952 </div>
7053 </div>
7154 </template>
···8366 </div>
84678568 <!-- Admin -->
8686- <div x-if="isAdmin()">
6969+ <div x-show="is_admin">
8770 <div class="content" style="text-align: center">
8871 <h4>Admin Controls:</h4>
8972 <div style="display: flex; justify-content: center">
9090- <button @click="generateSignupKey()">
9191- Generate signup key
9292- </button>
7373+ <button @click="generateSignupKey()">Generate signup key</button>
9374 </div>
9494- <div
9595- style="display: flex; justify-content: center"
9696- x-show="newSignupKey != ''"
9797- >
7575+ <div style="display: flex; justify-content: center" x-show="newSignupKey != ''">
9876 <p>New signup key: <a x-text="newSignupKey"></a></p>
9977 </div>
10078 </div>
+13-11
app/src/home-page/home.ts
···44import * as api from "../utils/api.ts";
5566Alpine.data("homePageState", () => ({
77- friends: [
88- { id: "123", name: "Alice Johnson" },
99- { id: "456", name: "Bob Smith" },
1010- { id: "789", name: "Carol Davis" },
1111- ],
77+ is_admin: false,
88+ friends: [] as { name: string; id: string }[],
99+1010+ async init() {
1111+ this.is_admin = await Store.get("is_admin");
1212+ this.friends = await Store.get("friends");
1313+ },
1414+1215 newSignupKey: "",
13161417 timeAgo() {
···2427 },
25282629 async updateServer() {
2727- await api.sendPings("123", "3.14159N 3.14159W");
3030+ // temporarly, just use the friend id of the first in the list
3131+ const friends = await Store.get("friends");
3232+3333+ await api.sendPings(friends[0].id, "3.14159N 3.14159W");
2834 },
29353036 addFriend() {
3131- alert("Add friend functionality would open here");
3737+ goto("add-friend");
3238 },
33393440 openSettings() {
···37433844 async generateSignupKey() {
3945 this.newSignupKey = await api.generateSignupKey();
4040- },
4141-4242- isAdmin() {
4343- return Store.get("is_admin");
4446 },
4547}));
4648
···11+set shell := ["bash", "-cu"]
22+33+APP_DIR := "app"
44+SERVER_DIR := "server"
55+66+alias srv := server
77+alias s := server
88+alias a := app
99+1010+default:
1111+ @just --list
1212+1313+server:
1414+ cd "{{SERVER_DIR}}" && cargo run
1515+1616+app:
1717+ cd "{{APP_DIR}}" && WEBKIT_DISABLE_DMABUF_RENDERER=1 bun run tauri dev
1818+1919+vite:
2020+ cd "{{APP_DIR}}" && bun run vite
2121+2222+duo:
2323+ #!/usr/bin/env bash
2424+ set -euo pipefail
2525+2626+ VITE_HOST="127.0.0.1"
2727+ VITE_PORT="5173"
2828+2929+ mkdir -p /tmp/ppA /tmp/ppB
3030+3131+ VITE_PID=""
3232+ TAURI_A_PID=""
3333+ TAURI_B_PID=""
3434+3535+ cleanup() {
3636+ for pid in "$TAURI_A_PID" "$TAURI_B_PID" "$VITE_PID"; do
3737+ [[ -n "$pid" ]] && kill "$pid" 2>/dev/null || true
3838+ done
3939+ }
4040+ trap cleanup INT TERM EXIT
4141+4242+ # Start Vite
4343+ (cd app && bun run vite) &
4444+ VITE_PID=$!
4545+4646+ # Start Tauri instance A
4747+ (cd app/src-tauri && \
4848+ WEBKIT_DISABLE_DMABUF_RENDERER=1 \
4949+ XDG_DATA_HOME=/tmp/ppA \
5050+ cargo run) &
5151+ TAURI_A_PID=$!
5252+5353+ # Start Tauri instance B
5454+ (cd app/src-tauri && \
5555+ WEBKIT_DISABLE_DMABUF_RENDERER=1 \
5656+ XDG_DATA_HOME=/tmp/ppB \
5757+ cargo run) &
5858+ TAURI_B_PID=$!
5959+6060+ wait
+14-8
server/src/handlers.rs
···1212 let key_used = { state.signup_keys.lock().await.remove(&payload.signup_key) };
13131414 if !key_used {
1515- ReqBail!("Signup key was not there");
1515+ ReqBail!("Invalid signup key");
1616 }
17171818 // todo check
···6666 }
67676868 let mut friend_requests = state.friend_requests.lock().await;
6969- let link = Link::new(friend_id, user_id);
6969+ let mut friend_request_link = DirectedFriendRequestLink {
7070+ sender_id: friend_id,
7171+ accepter_id: user_id,
7272+ };
70737174 // if we remove sucessfully the link, it means a request already existed
7275 // so we are making the friendship official
7373- let friend_request_accepted = friend_requests.remove(&link);
7676+ let friend_request_accepted = friend_requests.remove(&friend_request_link);
7777+7478 if friend_request_accepted {
7579 drop(friend_requests);
76807781 let mut pings_state = state.positions.lock().await;
8282+ let link = UndirectedLink::from(friend_request_link);
7883 pings_state.insert(link.clone(), RingBuffer::new(state.ring_buffer_cap));
7984 drop(pings_state);
8085···8287 links.insert(link);
8388 drop(links);
8489 } else {
8585- friend_requests.insert(link);
9090+ friend_request_link.swap_direction();
9191+ friend_requests.insert(friend_request_link);
8692 drop(friend_requests);
8793 }
8894···94100 Extension(user_id): Extension<String>,
95101 friend_id: String,
96102) -> Result<PlainBool, SrvErr> {
9797- let link = Link::new(friend_id, user_id);
103103+ let link = UndirectedLink::new(friend_id, user_id);
98104 let links = state.links.lock().await;
99105 let accepted = links.contains(&link);
100106 return Ok(PlainBool(accepted));
···107113) -> Result<(), SrvErr> {
108114 let links = state.links.lock().await;
109115 for ping in &pings {
110110- let link = Link::new(user_id.clone(), ping.receiver_id.clone());
116116+ let link = UndirectedLink::new(user_id.clone(), ping.receiver_id.clone());
111117 if !links.contains(&link) {
112118 ReqBail!("Ping receiver is not linked to sender");
113119 }
···117123 let mut pings_state = state.positions.lock().await;
118124119125 for ping in pings {
120120- let link = Link::new(user_id.clone(), ping.receiver_id.clone());
126126+ let link = UndirectedLink::new(user_id.clone(), ping.receiver_id.clone());
121127 pings_state
122128 .get_mut(&link)
123129 .unwrap()
···132138 Extension(user_id): Extension<String>,
133139 sender_id: String,
134140) -> Result<EncryptedPingVec, SrvErr> {
135135- let link = Link::new(user_id, sender_id);
141141+ let link = UndirectedLink::new(user_id, sender_id);
136142 let links = state.links.lock().await;
137143138144 if !links.contains(&link) {