diff --git a/web/src/pages/About.tsx b/web/src/pages/About.tsx index da87b6b9..854fa995 100644 --- a/web/src/pages/About.tsx +++ b/web/src/pages/About.tsx @@ -1,82 +1,100 @@ import { ExternalLinkIcon } from "lucide-react"; -import TileSpriteStrip from "@/components/Placeholder/TileSpriteStrip"; -import { TILE_SPRITES, type TileSprite } from "@/components/Placeholder/tileSprites"; -import SettingGroup from "@/components/Settings/SettingGroup"; -import SettingSection from "@/components/Settings/SettingSection"; -import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { useInstance } from "@/contexts/InstanceContext"; +import { useTranslate } from "@/utils/i18n"; -const SPRITE_SCALE = 2; +const GITHUB_COMMIT_URL_PREFIX = "https://github.com/usememos/memos/commit/"; +const GITHUB_RELEASE_URL_PREFIX = "https://github.com/usememos/memos/releases/tag/v"; -const PRODUCT_LINKS = [ - { label: "Website", href: "https://usememos.com/" }, - { label: "GitHub", href: "https://github.com/usememos/memos" }, - { label: "Docs", href: "https://usememos.com/docs" }, -]; +const DEFAULT_TITLE = "Memos"; +const DEFAULT_TAGLINE = "Capture first. Keep it yours."; +const DEFAULT_LOGO = "/logo.webp"; -const PRODUCT_POINTS = ["Open. Write. Done.", "Markdown-native.", "Fully yours."]; +const isCommitSha = (commit: string) => /^[0-9a-f]{7,40}$/i.test(commit); +const isSemver = (version: string) => /^\d+\.\d+\.\d+/.test(version); -const BirdSprite = ({ sprite }: { sprite: TileSprite }) => { - return ( -
- -
-

{sprite.name}

-
-
- ); +const Chip = ({ href, children }: { href?: string; children: React.ReactNode }) => { + const className = "inline-flex items-center rounded-full bg-muted px-2.5 py-0.5 font-mono text-xs text-muted-foreground"; + if (href) { + return ( + + {children} + + ); + } + return {children}; }; const About = () => { + const t = useTranslate(); + const { profile, generalSetting } = useInstance(); + + // Instance identity: custom branding when the admin has set it, Memos defaults otherwise. + const customProfile = generalSetting.customProfile; + const instanceTitle = customProfile?.title || DEFAULT_TITLE; + const instanceTagline = customProfile?.description || DEFAULT_TAGLINE; + const instanceLogo = customProfile?.logoUrl || DEFAULT_LOGO; + const isCustomBranded = instanceTitle !== DEFAULT_TITLE; + + // Dev builds report version "dev" and commit "unknown"; show the raw version and skip the commit chip. + const hasSemver = isSemver(profile.version); + const releaseUrl = hasSemver ? `${GITHUB_RELEASE_URL_PREFIX}${profile.version}` : ""; + const versionLabel = hasSemver ? `v${profile.version}` : profile.version; + const hasCommitSha = isCommitSha(profile.commit); + const commitUrl = hasCommitSha ? `${GITHUB_COMMIT_URL_PREFIX}${profile.commit}` : ""; + const shortCommit = hasCommitSha ? profile.commit.slice(0, 7) : ""; + + const projectLinks = [ + { label: t("about.official-website"), note: "the project homepage", href: "https://usememos.com/" }, + { label: t("about.documents"), note: "deploy, configure, use", href: "https://usememos.com/docs" }, + { label: "API Docs", note: "REST + gRPC reference", href: "https://usememos.com/docs/api" }, + { label: t("about.github-repository"), note: "source, issues, releases", href: "https://github.com/usememos/memos" }, + ]; + return (
- - -
-
- -
-

Memos

-

Capture first. Keep it yours.

-
-
-
- {PRODUCT_LINKS.map((link) => ( - - ))} -
+
+ +
+
+

{instanceTitle}

+ {profile.demo && Demo}
- - - -
- {PRODUCT_POINTS.map((item) => ( -
- {item} -
- ))} +

{instanceTagline}

+
+ {profile.version && {versionLabel}} + {shortCommit && {shortCommit}} + {isCustomBranded && Powered by Memos}
- +
+
- -
- {TILE_SPRITES.map((sprite) => ( - - ))} -
-
- + + +

Free and open source under the MIT license.

diff --git a/web/src/pages/Setting.tsx b/web/src/pages/Setting.tsx index 622f3ea3..4e8d3328 100644 --- a/web/src/pages/Setting.tsx +++ b/web/src/pages/Setting.tsx @@ -16,19 +16,14 @@ import useMediaQuery from "@/hooks/useMediaQuery"; import { User_Role } from "@/types/proto/api/v1/user_service_pb"; import { useTranslate } from "@/utils/i18n"; -const GITHUB_COMMIT_URL_PREFIX = "https://github.com/usememos/memos/commit/"; - -const isCommitSha = (commit: string) => /^[0-9a-f]{7,40}$/i.test(commit); - const Setting = () => { const t = useTranslate(); const sm = useMediaQuery("sm"); const location = useLocation(); const user = useCurrentUser(); - const { profile, fetchSettings } = useInstance(); + const { fetchSettings } = useInstance(); const [selectedSection, setSelectedSection] = useState(DEFAULT_SETTING_SECTION); const isHost = user?.role === User_Role.ADMIN; - const commitUrl = isCommitSha(profile.commit) ? `${GITHUB_COMMIT_URL_PREFIX}${profile.commit}` : ""; const sectionGroups = useMemo(() => { const visibleSections = SETTINGS_SECTIONS.filter((section) => section.scope === "basic" || isHost); @@ -93,24 +88,7 @@ const Setting = () => { {isHost && ( <> {t("common.admin")} -
- {renderSectionMenuItems(sectionGroups.admin)} -
- {t("setting.version")}: {profile.version} - {profile.commit && ( - - Commit:{" "} - {commitUrl ? ( - - {profile.commit} - - ) : ( - profile.commit - )} - - )} -
-
+
{renderSectionMenuItems(sectionGroups.admin)}
)} diff --git a/web/tests/about-page.test.tsx b/web/tests/about-page.test.tsx index fb79c600..51e3cbe9 100644 --- a/web/tests/about-page.test.tsx +++ b/web/tests/about-page.test.tsx @@ -1,26 +1,103 @@ -import { render, screen, within } from "@testing-library/react"; -import { afterEach, describe, expect, it } from "vitest"; -import { TILE_SPRITES } from "@/components/Placeholder/tileSprites"; +import { render, screen } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import About from "@/pages/About"; +const mockInstance = { + profile: { + version: "0.25.0", + commit: "0123456789abcdef0123456789abcdef01234567", + instanceUrl: "", + demo: false, + admin: undefined as { username: string; displayName: string } | undefined, + }, + generalSetting: {} as { customProfile?: { title: string; description: string; logoUrl: string } }, +}; + +vi.mock("@/contexts/InstanceContext", () => ({ + useInstance: () => mockInstance, +})); + +vi.mock("@/utils/i18n", () => ({ + useTranslate: () => (key: string) => (key === "common.version" ? "Version" : key), +})); + describe("", () => { + beforeEach(() => { + mockInstance.profile = { + version: "0.25.0", + commit: "0123456789abcdef0123456789abcdef01234567", + instanceUrl: "https://notes.example.com", + demo: false, + admin: { username: "steven", displayName: "Steven" }, + }; + mockInstance.generalSetting = {}; + }); + afterEach(() => { document.documentElement.removeAttribute("data-theme"); }); - it("renders the product story and current bird sprites", () => { + it("renders the identity hero with linked version and commit chips", () => { render(); expect(screen.getByRole("heading", { name: "Memos" })).toBeInTheDocument(); expect(screen.getByText(/Capture first/i)).toBeInTheDocument(); - expect(screen.getByText(/quick capture/i)).toBeInTheDocument(); + expect(screen.getByRole("link", { name: "v0.25.0" })).toHaveAttribute("href", "https://github.com/usememos/memos/releases/tag/v0.25.0"); + expect(screen.getByRole("link", { name: "0123456" })).toHaveAttribute( + "href", + "https://github.com/usememos/memos/commit/0123456789abcdef0123456789abcdef01234567", + ); + expect(screen.getByText(/MIT license/i)).toBeInTheDocument(); + }); - const birds = screen.getByRole("region", { name: "Birds" }); - expect(within(birds).getAllByTestId("about-bird-sprite")).toHaveLength(TILE_SPRITES.length); + it("links to the usememos.com homepage, docs, API docs, and GitHub repo", () => { + render(); - for (const sprite of TILE_SPRITES) { - expect(within(birds).getByText(sprite.name)).toBeInTheDocument(); - } + expect(screen.getByRole("link", { name: /about\.official-website/ })).toHaveAttribute("href", "https://usememos.com/"); + expect(screen.getByRole("link", { name: /about\.documents/ })).toHaveAttribute("href", "https://usememos.com/docs"); + expect(screen.getByRole("link", { name: /API Docs/ })).toHaveAttribute("href", "https://usememos.com/docs/api"); + expect(screen.getByRole("link", { name: /about\.github-repository/ })).toHaveAttribute("href", "https://github.com/usememos/memos"); + }); + + it("does not surface the instance URL, administrator, or birds", () => { + render(); + + expect(screen.queryByText("https://notes.example.com")).not.toBeInTheDocument(); + expect(screen.queryByText("Administrator")).not.toBeInTheDocument(); + expect(screen.queryByText("Steven")).not.toBeInTheDocument(); + expect(screen.queryByText("Birds")).not.toBeInTheDocument(); + expect(screen.queryByTestId("about-bird-sprite")).not.toBeInTheDocument(); + }); + + it("shows a plain version chip and no commit chip on dev builds", () => { + mockInstance.profile.version = "dev"; + mockInstance.profile.commit = "unknown"; + + render(); + + expect(screen.getByText("dev")).toBeInTheDocument(); + expect(screen.queryByText("vdev")).not.toBeInTheDocument(); + expect(screen.queryByText(/unknown/)).not.toBeInTheDocument(); + }); + + it("shows the demo badge on demo instances", () => { + mockInstance.profile.demo = true; + + render(); + + expect(screen.getByText("Demo")).toBeInTheDocument(); + }); + + it("uses custom branding for the identity hero and credits Memos", () => { + mockInstance.generalSetting = { + customProfile: { title: "Team Notes", description: "Our shared scratchpad.", logoUrl: "/custom-logo.png" }, + }; + + render(); + + expect(screen.getByRole("heading", { name: "Team Notes" })).toBeInTheDocument(); + expect(screen.getByText("Our shared scratchpad.")).toBeInTheDocument(); + expect(screen.getByText("Powered by Memos")).toBeInTheDocument(); }); it("does not add nested horizontal page padding on mobile", () => {