Skip to content
Merged
231 changes: 198 additions & 33 deletions apps/desktop/src/components/main/body/changelog/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,73 @@
import { openUrl } from "@tauri-apps/plugin-opener";
import { ExternalLinkIcon, SparklesIcon } from "lucide-react";
import {
ExternalLinkIcon,
GitCompareArrowsIcon,
SparklesIcon,
} from "lucide-react";
type RefObject,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import { useResizeObserver } from "usehooks-ts";

import NoteEditor from "@hypr/tiptap/editor";
import { md2json } from "@hypr/tiptap/shared";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@hypr/ui/components/ui/breadcrumb";
import { Button } from "@hypr/ui/components/ui/button";
import { cn } from "@hypr/utils";

import { type Tab } from "../../../../store/zustand/tabs";
import { StandardTabWrapper } from "../index";
import { type TabItem, TabItemBase } from "../shared";

export const changelogFiles = import.meta.glob(
"../../../../../../web/content/changelog/*.mdx",
{ query: "?raw", import: "default" },
);

export function getLatestVersion(): string | null {
const versions = Object.keys(changelogFiles)
.map((k) => {
const match = k.match(/\/([^/]+)\.mdx$/);
return match ? match[1] : null;
})
.filter((v): v is string => v !== null)
.filter((v) => !v.includes("nightly"))
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));

return versions[0] || null;
}

function stripFrontmatter(content: string): string {
return content.trim();
}

function stripImageLine(content: string): string {
return content.replace(/^!\[.*?\]\(.*?\)\s*\n*/m, "");
}

function addEmptyParagraphsBeforeHeaders(
json: ReturnType<typeof md2json>,
): ReturnType<typeof md2json> {
if (!json.content) return json;

const newContent: typeof json.content = [];
for (let i = 0; i < json.content.length; i++) {
const node = json.content[i];
if (node.type === "heading" && i > 0) {
newContent.push({ type: "paragraph" });
}
newContent.push(node);
}

return { ...json, content: newContent };
}

export const TabItemChangelog: TabItem<Extract<Tab, { type: "changelog" }>> = ({
tab,
tabIndex,
Expand Down Expand Up @@ -39,43 +98,149 @@ export function TabContentChangelog({
}: {
tab: Extract<Tab, { type: "changelog" }>;
}) {
const { previous, current } = tab.state;
const { current } = tab.state;

const { content, loading } = useChangelogContent(current);
const { scrollRef, atStart, atEnd } = useScrollFade<HTMLDivElement>();

return (
<StandardTabWrapper>
<div className="flex flex-1 flex-col items-center justify-center gap-6">
<div className="text-center">
<h1 className="text-2xl font-semibold text-neutral-900">
Updated to v{current}
<div className="flex flex-col h-full">
<div className="pl-2 pr-1 shrink-0">
<ChangelogHeader version={current} />
</div>

<div className="mt-2 px-3 shrink-0">
<h1 className="text-xl font-semibold text-neutral-900">
What's new in {current}?
</h1>
{previous && (
<p className="mt-1 text-sm text-neutral-500">from v{previous}</p>
)}
</div>

<div className="flex flex-col gap-2">
<button
onClick={() => openUrl(`https://hyprnote.com/changelog/${current}`)}
className="flex items-center gap-3 px-4 py-2.5 text-sm text-neutral-600 hover:text-neutral-900 hover:bg-neutral-100 rounded-lg transition-colors"
>
<ExternalLinkIcon className="w-4 h-4" />
View Changelog
</button>
{previous && (
<button
onClick={() =>
openUrl(
`https://github.com/fastrepl/hyprnote/compare/desktop_${previous}...desktop_v${current}`,
)
}
className="flex items-center gap-3 px-4 py-2.5 text-sm text-neutral-600 hover:text-neutral-900 hover:bg-neutral-100 rounded-lg transition-colors"
>
<GitCompareArrowsIcon className="w-4 h-4" />
View GitHub Diff
</button>
)}
<div className="mt-4 flex-1 min-h-0 relative overflow-hidden">
{!atStart && <ScrollFadeOverlay position="top" />}
{!atEnd && <ScrollFadeOverlay position="bottom" />}
<div ref={scrollRef} className="h-full overflow-y-auto px-3">
{loading ? (
<p className="text-neutral-500">Loading...</p>
) : content ? (
<NoteEditor initialContent={content} editable={false} />
) : (
<p className="text-neutral-500">
No changelog available for this version.
</p>
)}
</div>
</div>
</div>
</StandardTabWrapper>
);
}

function ChangelogHeader({ version }: { version: string }) {
return (
<div className="w-full pt-1">
<div className="flex items-center gap-2">
<div className="min-w-0 flex-1">
<Breadcrumb className="ml-1.5 min-w-0">
<BreadcrumbList className="text-neutral-700 text-xs flex-nowrap overflow-hidden gap-0.5">
<BreadcrumbItem className="shrink-0">
<span className="text-neutral-500">Changelog</span>
</BreadcrumbItem>
<BreadcrumbSeparator className="shrink-0" />
<BreadcrumbItem className="overflow-hidden">
<BreadcrumbPage className="truncate">{version}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>

<div className="flex items-center shrink-0">
<Button
size="sm"
variant="ghost"
className="gap-1.5"
onClick={() => openUrl("https://hyprnote.com/changelog")}
>
<ExternalLinkIcon size={14} className="-mt-0.5" />
<span>See all</span>
</Button>
</div>
</div>
</div>
);
}

function useChangelogContent(version: string) {
const [content, setContent] = useState<ReturnType<typeof md2json> | null>(
null,
);
const [loading, setLoading] = useState(true);

useEffect(() => {
const key = Object.keys(changelogFiles).find((k) =>
k.endsWith(`/${version}.mdx`),
);

if (!key) {
setLoading(false);
return;
}

changelogFiles[key]()
.then((raw) => {
const markdown = stripImageLine(stripFrontmatter(raw as string));
const json = md2json(markdown);
setContent(addEmptyParagraphsBeforeHeaders(json));
setLoading(false);
})
.catch(() => {
setContent(null);
setLoading(false);
});
}, [version]);

return { content, loading };
}

function useScrollFade<T extends HTMLElement>() {
const scrollRef = useRef<T>(null);
const [state, setState] = useState({ atStart: true, atEnd: true });

const update = useCallback(() => {
const el = scrollRef.current;
if (!el) return;

const { scrollTop, scrollHeight, clientHeight } = el;
setState({
atStart: scrollTop <= 1,
atEnd: scrollTop + clientHeight >= scrollHeight - 1,
});
}, []);

useResizeObserver({ ref: scrollRef as RefObject<T>, onResize: update });

useEffect(() => {
const el = scrollRef.current;
if (!el) return;

update();
el.addEventListener("scroll", update);
return () => el.removeEventListener("scroll", update);
}, [update]);

return { scrollRef, ...state };
}

function ScrollFadeOverlay({ position }: { position: "top" | "bottom" }) {
return (
<div
className={cn([
"absolute left-0 w-full h-8 z-20 pointer-events-none",
position === "top" &&
"top-0 bg-gradient-to-b from-white to-transparent",
position === "bottom" &&
"bottom-0 bg-gradient-to-t from-white to-transparent",
])}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const TriggerInner = forwardRef<
size="sm"
className={cn([open && "bg-neutral-100"])}
>
<CalendarIcon size={16} className="-mt-0.5" />
<CalendarIcon size={14} className="-mt-0.5" />
{formatRelativeOrAbsolute(createdAt ? new Date(createdAt) : new Date())}
</Button>
);
Expand Down
39 changes: 37 additions & 2 deletions apps/desktop/src/components/main/sidebar/devtool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import {
type Store as MainStore,
STORE_ID as STORE_ID_PERSISTED,
} from "../../../store/tinybase/store/main";
import { useTabs } from "../../../store/zustand/tabs";
import { type SeedDefinition, seeds } from "../../devtool/seed/index";
import { useTrialExpiredModal } from "../../devtool/trial-expired-modal";
import { getLatestVersion } from "../body/changelog";

declare global {
interface Window {
Expand Down Expand Up @@ -87,24 +89,32 @@ export function DevtoolView() {
function DevtoolCard({
title,
children,
maxHeight,
}: {
title: string;
children: React.ReactNode;
maxHeight?: string;
}) {
return (
<div
className={cn([
"rounded-lg border border-neutral-200 bg-white",
"shadow-sm",
"overflow-hidden",
"shrink-0",
])}
>
<div className="px-2 py-1.5 border-b border-neutral-100 bg-neutral-50">
<h2 className="text-xs font-semibold text-neutral-600 uppercase tracking-wide">
{title}
</h2>
</div>
<div className="p-2">{children}</div>
<div
className="p-2 overflow-y-auto"
style={maxHeight ? { maxHeight } : undefined}
>
{children}
</div>
</div>
);
}
Expand Down Expand Up @@ -189,7 +199,7 @@ function CalendarMockCard() {

function SeedCard({ onSeed }: { onSeed: (seed: SeedDefinition) => void }) {
return (
<DevtoolCard title="Seeds">
<DevtoolCard title="Seeds" maxHeight="200px">
<div className="flex flex-col gap-1.5">
{seeds.map((seed) => (
<button
Expand All @@ -213,6 +223,8 @@ function SeedCard({ onSeed }: { onSeed: (seed: SeedDefinition) => void }) {
}

function NavigationCard() {
const openNew = useTabs((s) => s.openNew);

const handleShowMain = useCallback(() => {
void windowsCommands.windowShow({ type: "main" });
}, []);
Expand All @@ -230,6 +242,16 @@ function NavigationCard() {
void windowsCommands.windowShow({ type: "control" });
}, []);

const handleShowChangelog = useCallback(() => {
const latestVersion = getLatestVersion();
if (latestVersion) {
openNew({
type: "changelog",
state: { current: latestVersion, previous: null },
});
}
}, [openNew]);

return (
<DevtoolCard title="Navigation">
<div className="flex flex-col gap-1.5">
Expand Down Expand Up @@ -272,6 +294,19 @@ function NavigationCard() {
>
Control
</button>
<button
type="button"
onClick={handleShowChangelog}
className={cn([
"w-full px-2.5 py-1.5 rounded-md",
"text-xs font-medium text-left",
"border border-neutral-200 text-neutral-700",
"cursor-pointer transition-colors",
"hover:bg-neutral-50 hover:border-neutral-300",
])}
>
Changelog
</button>
</div>
</DevtoolCard>
);
Expand Down
3 changes: 0 additions & 3 deletions apps/web/content/changelog/1.0.0-nightly.1.mdx
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
---
---

- Initial release.
3 changes: 0 additions & 3 deletions apps/web/content/changelog/1.0.0-nightly.10.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
---
---

- Add Intel macOS (x86_64) support.
- Add Deno-powered extensions runtime with iframe-based isolation and TinyBase sync.
- Add calendar extension. (not included yet)
Expand Down
3 changes: 0 additions & 3 deletions apps/web/content/changelog/1.0.0-nightly.11.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
---
---

- Add .deb build for Linux x86_64.
- Add network availability monitoring plugin.
- Add NetworkContext for network status in desktop app.
Expand Down
3 changes: 0 additions & 3 deletions apps/web/content/changelog/1.0.0-nightly.12.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
---
---

- Add Gladia STT provider with realtime and batch transcription support.
- Add OpenAI STT provider with realtime and batch transcription support.
- Add prompt, shortcut, template, and extensions tabs in settings.
Expand Down
3 changes: 0 additions & 3 deletions apps/web/content/changelog/1.0.0-nightly.13.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
---
---

- Unify VAD logic with StreamingVad for better audio processing.
- Fix batch audio processing bugs in listener2 plugin.
- Update onboarding flow with new model selection.
Expand Down
Loading
Loading