mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
## Summary - align the multi-community rail with the content surface and balance its visible 10px side gutters - center Mobile pairing, start sessions on demand, and keep retry states inside the QR area - reveal the QR code and copy action with 250ms motion and use the standard loading spinner The rail was centered within its own box, but the adjacent sidebar added another 11px to the visible right gap. Mobile pairing also started before user intent, which could leave an idle session waiting for EOSE. ## Validation - `pnpm -C desktop build:e2e` - `pnpm -C desktop test` — 3,516 passed - `pnpm -C desktop exec playwright test tests/e2e/community-rail.spec.ts --project=smoke` — 19 passed - `pnpm -C desktop exec playwright test tests/e2e/mobile-pairing-qr.spec.ts --project=smoke` — 1 passed `pnpm -C desktop check` is currently blocked by the existing `src-tauri/src/managed_agents/runtime.rs` file-size baseline (2,220 lines; limit 2,216).
190 lines
5.1 KiB
TypeScript
190 lines
5.1 KiB
TypeScript
import { relayClient } from "@/shared/api/relayClient";
|
|
import { invokeTauri, signRelayEvent } from "@/shared/api/tauri";
|
|
import { getIdentity } from "@/shared/api/tauriIdentity";
|
|
import type {
|
|
RelayEvent,
|
|
RelayMember,
|
|
RelayMemberRole,
|
|
} from "@/shared/api/types";
|
|
|
|
const KIND_NIP43_MEMBERSHIP_LIST = 13534;
|
|
const KIND_RELAY_ADMIN_ADD_MEMBER = 9030;
|
|
const KIND_RELAY_ADMIN_REMOVE_MEMBER = 9031;
|
|
const KIND_RELAY_ADMIN_CHANGE_ROLE = 9032;
|
|
|
|
function isRelayMemberRole(
|
|
value: string | undefined,
|
|
): value is RelayMemberRole {
|
|
return value === "owner" || value === "admin" || value === "member";
|
|
}
|
|
|
|
function normalizePubkey(pubkey: string): string {
|
|
return pubkey.trim().toLowerCase();
|
|
}
|
|
|
|
function eventCreatedAtIso(event: RelayEvent): string {
|
|
return new Date(event.created_at * 1_000).toISOString();
|
|
}
|
|
|
|
export type RelayMembershipLookup = {
|
|
/**
|
|
* True when the relay returned a NIP-43 membership snapshot.
|
|
*
|
|
* Open relays do not publish kind:13534, so absence of this snapshot must not
|
|
* be treated as a denial by onboarding.
|
|
*/
|
|
snapshotFound: boolean;
|
|
membershipRequired: boolean;
|
|
membership: RelayMember | null;
|
|
};
|
|
|
|
export function canManageCommunityMembers(
|
|
lookup: RelayMembershipLookup | undefined,
|
|
): boolean {
|
|
const role = lookup?.membership?.role;
|
|
return role === "owner" || role === "admin";
|
|
}
|
|
|
|
export function shouldWarnMissingMembershipSnapshot(
|
|
lookup: RelayMembershipLookup | undefined,
|
|
): boolean {
|
|
return lookup?.membershipRequired === true && !lookup.snapshotFound;
|
|
}
|
|
|
|
export function relayMembersFromEvent(event: RelayEvent): RelayMember[] {
|
|
const seen = new Set<string>();
|
|
const members: RelayMember[] = [];
|
|
const createdAt = eventCreatedAtIso(event);
|
|
|
|
for (const tag of event.tags) {
|
|
const [name, rawPubkey, maybeRoleOrRelay, maybePTagRole] = tag;
|
|
if (name !== "member" && name !== "p") continue;
|
|
if (!rawPubkey) continue;
|
|
|
|
const pubkey = normalizePubkey(rawPubkey);
|
|
if (!/^[0-9a-f]{64}$/.test(pubkey) || seen.has(pubkey)) continue;
|
|
seen.add(pubkey);
|
|
|
|
const rawRole = name === "member" ? maybeRoleOrRelay : maybePTagRole;
|
|
const role = isRelayMemberRole(rawRole) ? rawRole : "member";
|
|
|
|
members.push({
|
|
pubkey,
|
|
role,
|
|
addedBy: null,
|
|
createdAt,
|
|
});
|
|
}
|
|
|
|
return members;
|
|
}
|
|
|
|
export function relayMembershipLookupFromEvent(
|
|
event: RelayEvent | null,
|
|
pubkey: string,
|
|
membershipRequired = event !== null,
|
|
): RelayMembershipLookup {
|
|
if (!event) {
|
|
return {
|
|
snapshotFound: false,
|
|
membershipRequired,
|
|
membership: null,
|
|
};
|
|
}
|
|
|
|
const normalizedPubkey = normalizePubkey(pubkey);
|
|
return {
|
|
snapshotFound: true,
|
|
membershipRequired,
|
|
membership:
|
|
relayMembersFromEvent(event).find(
|
|
(member) => normalizePubkey(member.pubkey) === normalizedPubkey,
|
|
) ?? null,
|
|
};
|
|
}
|
|
|
|
async function fetchMembershipListEvent(): Promise<RelayEvent | null> {
|
|
return relayClient.fetchFirstEvent({
|
|
kinds: [KIND_NIP43_MEMBERSHIP_LIST],
|
|
limit: 1,
|
|
});
|
|
}
|
|
|
|
/** Loads the NIP-43 snapshot only when the relay advertises membership support. */
|
|
export async function loadRelayMembershipLookup(
|
|
pubkey: string,
|
|
membershipRequired: boolean,
|
|
fetchSnapshot: () => Promise<RelayEvent | null> = fetchMembershipListEvent,
|
|
): Promise<RelayMembershipLookup> {
|
|
if (!membershipRequired) {
|
|
return relayMembershipLookupFromEvent(null, pubkey, false);
|
|
}
|
|
return relayMembershipLookupFromEvent(await fetchSnapshot(), pubkey, true);
|
|
}
|
|
|
|
export async function listRelayMembers(): Promise<RelayMember[]> {
|
|
const event = await fetchMembershipListEvent();
|
|
return event ? relayMembersFromEvent(event) : [];
|
|
}
|
|
|
|
async function relayRequiresMembership(): Promise<boolean> {
|
|
return invokeTauri<boolean>("relay_requires_membership");
|
|
}
|
|
|
|
export async function getMyRelayMembershipLookup(): Promise<RelayMembershipLookup> {
|
|
const [{ pubkey }, membershipRequired] = await Promise.all([
|
|
getIdentity(),
|
|
relayRequiresMembership(),
|
|
]);
|
|
return loadRelayMembershipLookup(pubkey, membershipRequired);
|
|
}
|
|
|
|
export async function getMyRelayMembership(): Promise<RelayMember | null> {
|
|
return (await getMyRelayMembershipLookup()).membership;
|
|
}
|
|
|
|
async function publishRelayAdminEvent(
|
|
kind: number,
|
|
targetPubkey: string,
|
|
role?: string,
|
|
): Promise<void> {
|
|
const tags = [["p", normalizePubkey(targetPubkey)]];
|
|
if (role) {
|
|
tags.push(["role", role]);
|
|
}
|
|
|
|
const event = await signRelayEvent({
|
|
kind,
|
|
content: "",
|
|
tags,
|
|
});
|
|
|
|
await relayClient.publishEvent(
|
|
event,
|
|
"Timed out while updating relay access.",
|
|
"Failed to update relay access.",
|
|
);
|
|
}
|
|
|
|
export async function addRelayMember(
|
|
targetPubkey: string,
|
|
role: string,
|
|
): Promise<void> {
|
|
await publishRelayAdminEvent(KIND_RELAY_ADMIN_ADD_MEMBER, targetPubkey, role);
|
|
}
|
|
|
|
export async function removeRelayMember(targetPubkey: string): Promise<void> {
|
|
await publishRelayAdminEvent(KIND_RELAY_ADMIN_REMOVE_MEMBER, targetPubkey);
|
|
}
|
|
|
|
export async function changeRelayMemberRole(
|
|
targetPubkey: string,
|
|
newRole: string,
|
|
): Promise<void> {
|
|
await publishRelayAdminEvent(
|
|
KIND_RELAY_ADMIN_CHANGE_ROLE,
|
|
targetPubkey,
|
|
newRole,
|
|
);
|
|
}
|