diff --git a/AGENTS.md b/AGENTS.md index f1aa7e3b..0ff74656 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -170,6 +170,15 @@ src/ - Do not define svelte event handlers inline, instead name them and put them in the script section of templates - Avoid using `as`, except where necessary. Instead, annotate function parameters, and ensure upstream values are typed correctly. +**Human-First Simplicity (Jon Staab Style):** + +- Prefer direct, readable code over layered abstractions. +- Do not add indirection (extra helpers, wrappers, stores, or derived state) unless it removes real repeated complexity. +- Reuse existing Welshman and Flotilla primitives before introducing new utilities or dependencies. +- Favor linear control flow and explicit naming over clever patterns. +- Remove defensive checks that do not apply in this runtime model. +- When two approaches work, pick the one that feels more human and easier to maintain. + ## Common Tasks ### Adding a New Component diff --git a/src/app/util/title.ts b/src/app/util/title.ts new file mode 100644 index 00000000..b39864eb --- /dev/null +++ b/src/app/util/title.ts @@ -0,0 +1,142 @@ +import {append, identity, uniq} from "@welshman/lib" +import {repository} from "@welshman/app" +import {displayPubkey, getTagValue} from "@welshman/util" +import {PLATFORM_NAME, decodeRelay, getRoom, makeRoomId, splitChatId} from "@app/core/state" + +const FALLBACK_APP_NAME = "Flotilla" + +const staticTitles = new Map([ + ["/", "Redirecting"], + ["/home", "Home"], + ["/discover", "Discover Spaces"], + ["/spaces", "Your Spaces"], + ["/spaces/create", "Create a Space"], + ["/spaces/[relay]", "Space"], + ["/spaces/[relay]/chat", "Space Chat"], + ["/spaces/[relay]/recent", "Recent Activity"], + ["/spaces/[relay]/threads", "Threads"], + ["/spaces/[relay]/classifieds", "Classifieds"], + ["/spaces/[relay]/calendar", "Calendar"], + ["/spaces/[relay]/goals", "Goals"], + ["/chat", "Messages"], + ["/join", "Join Space"], + ["/people", "Find People"], + ["/settings/about", "About"], + ["/settings/profile", "Profile Settings"], + ["/settings/content", "Content Settings"], + ["/settings/privacy", "Privacy Settings"], + ["/settings/relays", "Relay Settings"], + ["/settings/alerts", "Alert Settings"], + ["/settings/wallet", "Wallet Settings"], + ["/[bech32]", "Opening Link"], +]) + +const eventRoutes = new Set([ + "/spaces/[relay]/threads/[id]", + "/spaces/[relay]/goals/[id]", + "/spaces/[relay]/calendar/[address]", + "/spaces/[relay]/classifieds/[address]", +]) + +type RouteParams = Record + +type TitlePage = { + route: {id: string | null} + params: RouteParams +} + +type PageTitleContext = { + page: TitlePage + pubkey: string | undefined +} + +const getRoomTitle = (params: RouteParams) => { + const relay = params.relay + const h = params.h + + if (!relay || !h) { + return "Room" + } + + const url = decodeRelay(relay) + + return getRoom(makeRoomId(url, h))?.name || "Room" +} + +const getEventForTitle = (routeId: string, params: RouteParams) => { + if (!eventRoutes.has(routeId)) { + return + } + + const eventId = params.id || params.address + + if (!eventId) { + return + } + + return repository.getEvent(eventId) +} + +const getChatTitle = (chatId: string | undefined, pubkey: string | undefined) => { + if (!chatId) { + return "Chat" + } + + const chatPeers = pubkey ? uniq(append(pubkey, splitChatId(chatId))) : splitChatId(chatId) + const others = pubkey ? chatPeers.filter(pk => pk !== pubkey) : chatPeers + + if (others.length === 1) { + return `Chat with ${displayPubkey(others[0])}` + } + + if (others.length > 1) { + return `Group chat (${others.length})` + } + + return "Chat" +} + +export const makeTitle = (...parts: Array) => + parts + .map(part => part?.trim() || "") + .filter(identity) + .join(" ยท ") || + PLATFORM_NAME || + FALLBACK_APP_NAME + +export const getPageTitle = ({page, pubkey}: PageTitleContext) => { + const routeId = page.route.id || "" + const staticTitle = staticTitles.get(routeId) + + if (staticTitle) { + return makeTitle(staticTitle) + } + + if (routeId === "/chat/[chat]") { + return makeTitle(getChatTitle(page.params.chat, pubkey)) + } + + if (routeId === "/spaces/[relay]/[h]") { + return makeTitle(getRoomTitle(page.params)) + } + + const event = getEventForTitle(routeId, page.params) + + if (routeId === "/spaces/[relay]/threads/[id]") { + return makeTitle(getTagValue("title", event?.tags || []) || "Thread") + } + + if (routeId === "/spaces/[relay]/calendar/[address]") { + return makeTitle(getTagValue("title", event?.tags || []) || "Event") + } + + if (routeId === "/spaces/[relay]/classifieds/[address]") { + return makeTitle(getTagValue("title", event?.tags || []) || "Listing") + } + + if (routeId === "/spaces/[relay]/goals/[id]") { + return makeTitle(event?.content || getTagValue("summary", event?.tags || []) || "Goal") + } + + return makeTitle() +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index e23f0015..9cd3be65 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -7,6 +7,7 @@ import {App, type URLOpenListenerEvent} from "@capacitor/app" import {dev} from "$app/environment" import {goto} from "$app/navigation" + import {page} from "$app/stores" import {sync, throttled} from "@welshman/store" import {call} from "@welshman/lib" import {defaultSocketPolicies} from "@welshman/net" @@ -42,6 +43,7 @@ import * as notifications from "@app/util/notifications" import * as storage from "@app/util/storage" import {syncKeyboard} from "@app/util/keyboard" + import {getPageTitle} from "@app/util/title" import NewNotificationSound from "@src/app/components/NewNotificationSound.svelte" const {children} = $props() @@ -199,6 +201,10 @@ App.removeAllListeners() unsubscribe.then(call) }) + + $effect(() => { + document.title = getPageTitle({page: $page, pubkey: $pubkey}) + })