adamelmore пре 3 недеља
родитељ
комит
ccc7aa49c3

+ 18 - 12
packages/app/src/components/dialog-release-notes.tsx

@@ -2,11 +2,11 @@ import { createSignal, createEffect, onMount, onCleanup } from "solid-js"
 import { Dialog } from "@opencode-ai/ui/dialog"
 import { Button } from "@opencode-ai/ui/button"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
+import { useSettings } from "@/context/settings"
 
 export type Highlight = {
   title: string
   description: string
-  tag?: string
   media?: {
     type: "image" | "video"
     src: string
@@ -16,6 +16,7 @@ export type Highlight = {
 
 export function DialogReleaseNotes(props: { highlights: Highlight[] }) {
   const dialog = useDialog()
+  const settings = useSettings()
   const [index, setIndex] = createSignal(0)
 
   const total = () => props.highlights.length
@@ -34,9 +35,20 @@ export function DialogReleaseNotes(props: { highlights: Highlight[] }) {
     dialog.close()
   }
 
+  function handleDisable() {
+    settings.general.setReleaseNotes(false)
+    handleClose()
+  }
+
   let focusTrap: HTMLDivElement | undefined
 
   function handleKeyDown(e: KeyboardEvent) {
+    if (e.key === "Escape") {
+      e.preventDefault()
+      handleClose()
+      return
+    }
+
     if (!paged()) return
     if (e.key === "ArrowLeft" && !isFirst()) {
       e.preventDefault()
@@ -50,8 +62,6 @@ export function DialogReleaseNotes(props: { highlights: Highlight[] }) {
 
   onMount(() => {
     focusTrap?.focus()
-
-    if (!paged()) return
     document.addEventListener("keydown", handleKeyDown)
     onCleanup(() => document.removeEventListener("keydown", handleKeyDown))
   })
@@ -72,14 +82,6 @@ export function DialogReleaseNotes(props: { highlights: Highlight[] }) {
         <div class="flex flex-col gap-2 pt-22">
           <div class="flex items-center gap-2">
             <h1 class="text-16-medium text-text-strong">{feature()?.title ?? ""}</h1>
-            {feature()?.tag && (
-              <span
-                class="text-12-medium text-text-weak px-1.5 py-0.5 bg-surface-base rounded-sm border border-border-weak-base"
-                style={{ "border-width": "0.5px" }}
-              >
-                {feature()!.tag}
-              </span>
-            )}
           </div>
           <p class="text-14-regular text-text-base">{feature()?.description ?? ""}</p>
         </div>
@@ -89,7 +91,7 @@ export function DialogReleaseNotes(props: { highlights: Highlight[] }) {
 
         {/* Bottom section - buttons and indicators (fixed position) */}
         <div class="flex flex-col gap-12">
-          <div class="flex items-center gap-3">
+          <div class="flex flex-col items-start gap-3">
             {isLast() ? (
               <Button variant="primary" size="large" onClick={handleClose}>
                 Get started
@@ -99,6 +101,10 @@ export function DialogReleaseNotes(props: { highlights: Highlight[] }) {
                 Next
               </Button>
             )}
+
+            <Button variant="ghost" size="small" onClick={handleDisable}>
+              Don't show these in the future
+            </Button>
           </div>
 
           {paged() && (

+ 17 - 0
packages/app/src/components/settings-general.tsx

@@ -214,6 +214,23 @@ export const SettingsGeneral: Component = () => {
           </div>
         </div>
 
+        {/* Updates Section */}
+        <div class="flex flex-col gap-1">
+          <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.updates")}</h3>
+
+          <div class="bg-surface-raised-base px-4 rounded-lg">
+            <SettingsRow
+              title={language.t("settings.general.row.releaseNotes.title")}
+              description={language.t("settings.general.row.releaseNotes.description")}
+            >
+              <Switch
+                checked={settings.general.releaseNotes()}
+                onChange={(checked) => settings.general.setReleaseNotes(checked)}
+              />
+            </SettingsRow>
+          </div>
+        </div>
+
         {/* Sound effects Section */}
         <div class="flex flex-col gap-1">
           <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.sounds")}</h3>

+ 37 - 17
packages/app/src/context/highlights.tsx

@@ -3,10 +3,11 @@ import { createStore } from "solid-js/store"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { usePlatform } from "@/context/platform"
+import { useSettings } from "@/context/settings"
 import { persisted } from "@/utils/persist"
 import { DialogReleaseNotes, type Highlight } from "@/components/dialog-release-notes"
 
-const CHANGELOG_URL = "https://dev.opencode.ai/changelog.json"
+const CHANGELOG_URL = "https://opencode.ai/changelog.json"
 
 type Store = {
   version?: string
@@ -18,7 +19,7 @@ type ParsedRelease = {
 }
 
 function isRecord(value: unknown): value is Record<string, unknown> {
-  return typeof value === "object" && value !== null
+  return typeof value === "object" && value !== null && !Array.isArray(value)
 }
 
 function getText(value: unknown): string | undefined {
@@ -40,14 +41,14 @@ function normalizeVersion(value: string | undefined) {
 function parseMedia(value: unknown, alt: string): Highlight["media"] | undefined {
   if (!isRecord(value)) return
   const type = getText(value.type)?.toLowerCase()
-  const src = getText(value.src)
+  const src = getText(value.src) ?? getText(value.url)
   if (!src) return
   if (type !== "image" && type !== "video") return
 
   return { type, src, alt }
 }
 
-function parseHighlight(value: unknown, tag: string | undefined): Highlight | undefined {
+function parseHighlight(value: unknown): Highlight | undefined {
   if (!isRecord(value)) return
 
   const title = getText(value.title)
@@ -57,7 +58,7 @@ function parseHighlight(value: unknown, tag: string | undefined): Highlight | un
   if (!description) return
 
   const media = parseMedia(value.media, title)
-  return { title, description, tag, media }
+  return { title, description, media }
 }
 
 function parseRelease(value: unknown): ParsedRelease | undefined {
@@ -70,11 +71,18 @@ function parseRelease(value: unknown): ParsedRelease | undefined {
 
   const highlights = value.highlights.flatMap((group) => {
     if (!isRecord(group)) return []
-    if (!Array.isArray(group.items)) return []
+
     const source = getText(group.source)
-    return group.items
-      .map((item) => parseHighlight(item, source))
-      .filter((item): item is Highlight => item !== undefined)
+    if (!source) return []
+    if (!source.toLowerCase().includes("desktop")) return []
+
+    if (Array.isArray(group.items)) {
+      return group.items.map((item) => parseHighlight(item)).filter((item): item is Highlight => item !== undefined)
+    }
+
+    const item = parseHighlight(group)
+    if (!item) return []
+    return [item]
   })
 
   return { tag, highlights }
@@ -108,10 +116,17 @@ function sliceHighlights(input: { releases: ParsedRelease[]; current?: string; p
     return index === -1 ? releases.length : index
   })()
 
-  return releases
-    .slice(start, end)
-    .flatMap((release) => release.highlights)
-    .slice(0, 3)
+  const highlights = releases.slice(start, end).flatMap((release) => release.highlights)
+  const seen = new Set<string>()
+  const unique = highlights.filter((highlight) => {
+    const key = [highlight.title, highlight.description, highlight.media?.type ?? "", highlight.media?.src ?? ""].join(
+      "\n",
+    )
+    if (seen.has(key)) return false
+    seen.add(key)
+    return true
+  })
+  return unique.slice(0, 3)
 }
 
 export const { use: useHighlights, provider: HighlightsProvider } = createSimpleContext({
@@ -120,6 +135,7 @@ export const { use: useHighlights, provider: HighlightsProvider } = createSimple
   init: () => {
     const platform = usePlatform()
     const dialog = useDialog()
+    const settings = useSettings()
     const [store, setStore, _, ready] = persisted("highlights.v1", createStore<Store>({ version: undefined }))
 
     const [from, setFrom] = createSignal<string | undefined>(undefined)
@@ -135,6 +151,7 @@ export const { use: useHighlights, provider: HighlightsProvider } = createSimple
     createEffect(() => {
       if (state.started) return
       if (!ready()) return
+      if (!settings.ready()) return
       if (!platform.version) return
       state.started = true
 
@@ -149,6 +166,11 @@ export const { use: useHighlights, provider: HighlightsProvider } = createSimple
       setFrom(previous)
       setTo(platform.version)
 
+      if (!settings.general.releaseNotes()) {
+        markSeen()
+        return
+      }
+
       const fetcher = platform.fetch ?? fetch
       const controller = new AbortController()
       onCleanup(() => {
@@ -182,10 +204,8 @@ export const { use: useHighlights, provider: HighlightsProvider } = createSimple
           }
 
           const timer = setTimeout(() => {
-            dialog.show(
-              () => <DialogReleaseNotes highlights={highlights} />,
-              () => markSeen(),
-            )
+            markSeen()
+            dialog.show(() => <DialogReleaseNotes highlights={highlights} />)
           }, 500)
           setTimer(timer)
         })

+ 6 - 0
packages/app/src/context/settings.tsx

@@ -18,6 +18,7 @@ export interface SoundSettings {
 export interface Settings {
   general: {
     autoSave: boolean
+    releaseNotes: boolean
   }
   appearance: {
     fontSize: number
@@ -34,6 +35,7 @@ export interface Settings {
 const defaultSettings: Settings = {
   general: {
     autoSave: true,
+    releaseNotes: true,
   },
   appearance: {
     fontSize: 14,
@@ -97,6 +99,10 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
         setAutoSave(value: boolean) {
           setStore("general", "autoSave", value)
         },
+        releaseNotes: createMemo(() => store.general?.releaseNotes ?? defaultSettings.general.releaseNotes),
+        setReleaseNotes(value: boolean) {
+          setStore("general", "releaseNotes", value)
+        },
       },
       appearance: {
         fontSize: createMemo(() => store.appearance?.fontSize ?? defaultSettings.appearance.fontSize),

+ 4 - 0
packages/app/src/i18n/en.ts

@@ -525,6 +525,7 @@ export const dict = {
 
   "settings.general.section.appearance": "Appearance",
   "settings.general.section.notifications": "System notifications",
+  "settings.general.section.updates": "Updates",
   "settings.general.section.sounds": "Sound effects",
 
   "settings.general.row.language.title": "Language",
@@ -535,6 +536,9 @@ export const dict = {
   "settings.general.row.theme.description": "Customise how OpenCode is themed.",
   "settings.general.row.font.title": "Font",
   "settings.general.row.font.description": "Customise the mono font used in code blocks",
+
+  "settings.general.row.releaseNotes.title": "Release notes",
+  "settings.general.row.releaseNotes.description": "Show What's New popups after updates",
   "font.option.ibmPlexMono": "IBM Plex Mono",
   "font.option.cascadiaCode": "Cascadia Code",
   "font.option.firaCode": "Fira Code",