Merge pull request #201 from Coffey-Labs/feat/base-path

Serve ihasmail from a subpath
This commit is contained in:
Coffey Labs
2026-09-01 22:47:53 -07:00
committed by GitHub
29 changed files with 579 additions and 55 deletions
+2 -1
View File
@@ -2,6 +2,7 @@ import { useEffect, useState, type ReactNode } from "react";
import { Link, useLocation } from "wouter";
import { BookOpen, Calendar, ChevronsUpDown, FolderOpen, Globe, HelpCircle, LogOut, Mail, Menu as MenuIcon, Moon, PenSquare, Plus, RefreshCw, Settings, Sun, Upload, Users, X } from "lucide-react";
import { useSession } from "@/store/session";
import { withBase } from "@/lib/basePath";
import { toggleTarget, useEffectiveTheme, useSettings } from "@/store/settings";
import { useMail } from "@/store/mail";
import { draftFromMailto, useCompose } from "@/store/compose";
@@ -80,7 +81,7 @@ export function AppShell({ children }: { children: ReactNode }) {
<MenuIcon size={22} />
</button>
<Link href="/mail" className="brand">
<img src="/img/logo.png" alt="" />
<img src={withBase("/img/logo.png")} alt="" />
{/* A product name, not a word. "ihasmail" translated is a different
product, and the one on the tab beside it is still called this. */}
<span className="brand-name notranslate" translate="no">
+3 -2
View File
@@ -2,6 +2,7 @@ import { useEffect, useState, type FormEvent } from "react";
import { Eye, EyeOff, LogIn } from "lucide-react";
import { useSession } from "@/store/session";
import { ApiError } from "@/jmap/client";
import { withBase } from "@/lib/basePath";
import { DEFAULT_SOURCE_URL } from "@/lib/source";
import { APP_VERSION } from "@/lib/version";
import { t } from "@/lib/i18n";
@@ -14,7 +15,7 @@ export function LoginPage() {
const [sourceUrl, setSourceUrl] = useState(DEFAULT_SOURCE_URL);
useEffect(() => {
let live = true;
fetch("/api/config")
fetch(withBase("/api/config"))
.then((r) => (r.ok ? r.json() : null))
.then((c) => { if (live && c?.sourceUrl) setSourceUrl(c.sourceUrl as string); })
.catch(() => { /* the default stands */ });
@@ -53,7 +54,7 @@ export function LoginPage() {
<div className="login-page">
<form className="login-card" onSubmit={submit}>
<div className="logo">
<img src="/img/logo.png" alt="" width={120} height={143} />
<img src={withBase("/img/logo.png")} alt="" width={120} height={143} />
<h1 className="notranslate" translate="no">ihasmail</h1>
<p className="tagline">{t("Fast, friendly webmail. Your mailbox, your way.")}</p>
</div>
+2 -1
View File
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useLocation, useSearch } from "wouter";
import { DEFAULT_SORT, useMail, type ListQuery } from "@/store/mail";
import { useSettings } from "@/store/settings";
import { withBase } from "@/lib/basePath";
import { useCompose } from "@/store/compose";
import { buildFilter, describeFilter, parseQuery } from "@/lib/search";
import { keyboard } from "@/lib/keyboard";
@@ -353,7 +354,7 @@ export function MailView({ mailboxId, threadId, search }: { mailboxId?: string;
<ThreadView key={threadId} threadId={threadId} mailboxId={mailboxId ?? null} onBack={() => openThread(null)} actions={actions} onNavigate={(delta) => { const idx = currentRowIndex; const next = ids[idx + delta]; const t = next ? rowThreadId(next) : undefined; if (t) { setFocusId(next!); openThread(t); } }} hasPrev={currentRowIndex > 0} hasNext={currentRowIndex >= 0 && currentRowIndex < ids.length - 1} />
) : (
<div className="no-thread">
<img src="/img/logo.png" alt="" />
<img src={withBase("/img/logo.png")} alt="" />
<div>{list?.total ? plural(list.total, { one: "{n} conversation", other: "{n} conversations" }) : translate("No conversation selected")}</div>
<div className="hint">{tNode("Select a conversation to read it here · Press {key} for shortcuts", { key: <kbd className="kbd">?</kbd> })}</div>
</div>
+2 -1
View File
@@ -2,6 +2,7 @@ import { useSession } from "@/store/session";
import { client } from "@/jmap/client";
import { DEFAULT_SOURCE_URL } from "@/lib/source";
import { APP_VERSION } from "@/lib/version";
import { withBase } from "@/lib/basePath";
import { t, tNode } from "@/lib/i18n";
export function AboutSettings() {
@@ -14,7 +15,7 @@ export function AboutSettings() {
<h1>{t("About ihasmail")}</h1>
<p className="lead">{tNode("A fast, friendly, open-source webmail for {server}, built on JMAP.", { server: <a href="https://stalw.art" target="_blank" rel="noreferrer">{t("Stalwart Mail Server")}</a> })}</p>
<div className="row" style={{ gap: 16, alignItems: "center", marginBottom: 16 }}>
<img src="/img/logo.png" alt={t("ihasmail")} width={96} />
<img src={withBase("/img/logo.png")} alt={t("ihasmail")} width={96} />
<div>
{/* A product name and a version string: neither is a word to translate. */}
<div style={{ fontWeight: 700, fontSize: "1.2em" }} className="notranslate" translate="no">ihasmail v{APP_VERSION}</div>