Просмотр исходного кода

feat(app): clear notifications action (#13668)

Co-authored-by: adamelmore <[email protected]>
Adam 2 месяцев назад
Родитель
Сommit
85b5f5b705

+ 3 - 0
packages/app/e2e/selectors.ts

@@ -30,6 +30,9 @@ export const projectMenuTriggerSelector = (slug: string) =>
 
 export const projectCloseMenuSelector = (slug: string) => `[data-action="project-close-menu"][data-project="${slug}"]`
 
+export const projectClearNotificationsSelector = (slug: string) =>
+  `[data-action="project-clear-notifications"][data-project="${slug}"]`
+
 export const projectWorkspacesToggleSelector = (slug: string) =>
   `[data-action="project-workspaces-toggle"][data-project="${slug}"]`
 

+ 1 - 0
packages/app/src/i18n/ar.ts

@@ -509,6 +509,7 @@ export const dict = {
   "sidebar.gettingStarted.line2": "قم بتوصيل أي موفر لاستخدام النماذج، بما في ذلك Claude و GPT و Gemini وما إلى ذلك.",
   "sidebar.project.recentSessions": "الجلسات الحديثة",
   "sidebar.project.viewAllSessions": "عرض جميع الجلسات",
+  "sidebar.project.clearNotifications": "مسح الإشعارات",
   "app.name.desktop": "OpenCode Desktop",
   "settings.section.desktop": "سطح المكتب",
   "settings.section.server": "الخادم",

+ 1 - 0
packages/app/src/i18n/br.ts

@@ -515,6 +515,7 @@ export const dict = {
   "sidebar.gettingStarted.line2": "Conecte qualquer provedor para usar modelos, incluindo Claude, GPT, Gemini etc.",
   "sidebar.project.recentSessions": "Sessões recentes",
   "sidebar.project.viewAllSessions": "Ver todas as sessões",
+  "sidebar.project.clearNotifications": "Limpar notificações",
   "app.name.desktop": "OpenCode Desktop",
   "settings.section.desktop": "Desktop",
   "settings.section.server": "Servidor",

+ 1 - 0
packages/app/src/i18n/bs.ts

@@ -576,6 +576,7 @@ export const dict = {
   "sidebar.gettingStarted.line2": "Poveži bilo kojeg provajdera da koristiš modele, npr. Claude, GPT, Gemini itd.",
   "sidebar.project.recentSessions": "Nedavne sesije",
   "sidebar.project.viewAllSessions": "Prikaži sve sesije",
+  "sidebar.project.clearNotifications": "Očisti obavijesti",
 
   "app.name.desktop": "OpenCode Desktop",
 

+ 1 - 0
packages/app/src/i18n/da.ts

@@ -572,6 +572,7 @@ export const dict = {
   "sidebar.gettingStarted.line2": "Forbind enhver udbyder for at bruge modeller, inkl. Claude, GPT, Gemini osv.",
   "sidebar.project.recentSessions": "Seneste sessioner",
   "sidebar.project.viewAllSessions": "Vis alle sessioner",
+  "sidebar.project.clearNotifications": "Ryd notifikationer",
 
   "app.name.desktop": "OpenCode Desktop",
   "settings.section.desktop": "Desktop",

+ 1 - 0
packages/app/src/i18n/de.ts

@@ -524,6 +524,7 @@ export const dict = {
     "Verbinden Sie einen beliebigen Anbieter, um Modelle wie Claude, GPT, Gemini usw. zu nutzen.",
   "sidebar.project.recentSessions": "Letzte Sitzungen",
   "sidebar.project.viewAllSessions": "Alle Sitzungen anzeigen",
+  "sidebar.project.clearNotifications": "Benachrichtigungen löschen",
   "app.name.desktop": "OpenCode Desktop",
   "settings.section.desktop": "Desktop",
   "settings.section.server": "Server",

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

@@ -577,6 +577,7 @@ export const dict = {
   "sidebar.gettingStarted.line2": "Connect any provider to use models, inc. Claude, GPT, Gemini etc.",
   "sidebar.project.recentSessions": "Recent sessions",
   "sidebar.project.viewAllSessions": "View all sessions",
+  "sidebar.project.clearNotifications": "Clear notifications",
 
   "app.name.desktop": "OpenCode Desktop",
 

+ 1 - 0
packages/app/src/i18n/es.ts

@@ -579,6 +579,7 @@ export const dict = {
   "sidebar.gettingStarted.line2": "Conecta cualquier proveedor para usar modelos, inc. Claude, GPT, Gemini etc.",
   "sidebar.project.recentSessions": "Sesiones recientes",
   "sidebar.project.viewAllSessions": "Ver todas las sesiones",
+  "sidebar.project.clearNotifications": "Borrar notificaciones",
 
   "app.name.desktop": "OpenCode Desktop",
 

+ 1 - 0
packages/app/src/i18n/fr.ts

@@ -523,6 +523,7 @@ export const dict = {
     "Connectez n'importe quel fournisseur pour utiliser des modèles, y compris Claude, GPT, Gemini etc.",
   "sidebar.project.recentSessions": "Sessions récentes",
   "sidebar.project.viewAllSessions": "Voir toutes les sessions",
+  "sidebar.project.clearNotifications": "Effacer les notifications",
   "app.name.desktop": "OpenCode Desktop",
   "settings.section.desktop": "Bureau",
   "settings.section.server": "Serveur",

+ 1 - 0
packages/app/src/i18n/ja.ts

@@ -513,6 +513,7 @@ export const dict = {
   "sidebar.gettingStarted.line2": "プロバイダーを接続して、Claude、GPT、Geminiなどのモデルを使用できます。",
   "sidebar.project.recentSessions": "最近のセッション",
   "sidebar.project.viewAllSessions": "すべてのセッションを表示",
+  "sidebar.project.clearNotifications": "通知をクリア",
   "app.name.desktop": "OpenCode Desktop",
   "settings.section.desktop": "デスクトップ",
   "settings.section.server": "サーバー",

+ 1 - 0
packages/app/src/i18n/ko.ts

@@ -514,6 +514,7 @@ export const dict = {
   "sidebar.gettingStarted.line2": "Claude, GPT, Gemini 등을 포함한 모델을 사용하려면 공급자를 연결하세요.",
   "sidebar.project.recentSessions": "최근 세션",
   "sidebar.project.viewAllSessions": "모든 세션 보기",
+  "sidebar.project.clearNotifications": "알림 지우기",
   "app.name.desktop": "OpenCode Desktop",
   "settings.section.desktop": "데스크톱",
   "settings.section.server": "서버",

+ 1 - 0
packages/app/src/i18n/no.ts

@@ -579,6 +579,7 @@ export const dict = {
   "sidebar.gettingStarted.line2": "Koble til en leverandør for å bruke modeller, inkl. Claude, GPT, Gemini osv.",
   "sidebar.project.recentSessions": "Nylige sesjoner",
   "sidebar.project.viewAllSessions": "Vis alle sesjoner",
+  "sidebar.project.clearNotifications": "Fjern varsler",
 
   "app.name.desktop": "OpenCode Desktop",
 

+ 1 - 0
packages/app/src/i18n/pl.ts

@@ -514,6 +514,7 @@ export const dict = {
   "sidebar.gettingStarted.line2": "Połącz dowolnego dostawcę, aby używać modeli, w tym Claude, GPT, Gemini itp.",
   "sidebar.project.recentSessions": "Ostatnie sesje",
   "sidebar.project.viewAllSessions": "Zobacz wszystkie sesje",
+  "sidebar.project.clearNotifications": "Wyczyść powiadomienia",
   "app.name.desktop": "OpenCode Desktop",
   "settings.section.desktop": "Pulpit",
   "settings.section.server": "Serwer",

+ 1 - 0
packages/app/src/i18n/ru.ts

@@ -578,6 +578,7 @@ export const dict = {
     "Подключите любого провайдера для использования моделей, включая Claude, GPT, Gemini и др.",
   "sidebar.project.recentSessions": "Недавние сессии",
   "sidebar.project.viewAllSessions": "Посмотреть все сессии",
+  "sidebar.project.clearNotifications": "Очистить уведомления",
 
   "app.name.desktop": "OpenCode Desktop",
   "settings.section.desktop": "Приложение",

+ 1 - 0
packages/app/src/i18n/th.ts

@@ -571,6 +571,7 @@ export const dict = {
   "sidebar.gettingStarted.line2": "เชื่อมต่อผู้ให้บริการใด ๆ เพื่อใช้โมเดล รวมถึง Claude, GPT, Gemini ฯลฯ",
   "sidebar.project.recentSessions": "เซสชันล่าสุด",
   "sidebar.project.viewAllSessions": "ดูเซสชันทั้งหมด",
+  "sidebar.project.clearNotifications": "ล้างการแจ้งเตือน",
 
   "app.name.desktop": "OpenCode Desktop",
 

+ 1 - 0
packages/app/src/i18n/zh.ts

@@ -569,6 +569,7 @@ export const dict = {
   "sidebar.gettingStarted.line2": "连接任意提供商即可使用更多模型,如 Claude、GPT、Gemini 等。",
   "sidebar.project.recentSessions": "最近会话",
   "sidebar.project.viewAllSessions": "查看全部会话",
+  "sidebar.project.clearNotifications": "清除通知",
 
   "app.name.desktop": "OpenCode Desktop",
 

+ 1 - 0
packages/app/src/i18n/zht.ts

@@ -567,6 +567,7 @@ export const dict = {
   "sidebar.gettingStarted.line2": "連線任意提供者即可使用更多模型,如 Claude、GPT、Gemini 等。",
   "sidebar.project.recentSessions": "最近工作階段",
   "sidebar.project.viewAllSessions": "查看全部工作階段",
+  "sidebar.project.clearNotifications": "清除通知",
 
   "app.name.desktop": "OpenCode Desktop",
   "settings.section.desktop": "桌面",

+ 17 - 0
packages/app/src/pages/layout.tsx

@@ -1692,6 +1692,13 @@ export default function Layout(props: ParentProps) {
     })
     const projectId = createMemo(() => panelProps.project?.id ?? "")
     const workspaces = createMemo(() => workspaceIds(panelProps.project))
+    const unseenCount = createMemo(() =>
+      workspaces().reduce((total, directory) => total + notification.project.unseenCount(directory), 0),
+    )
+    const clearNotifications = () =>
+      workspaces()
+        .filter((directory) => notification.project.unseenCount(directory) > 0)
+        .forEach((directory) => notification.project.markViewed(directory))
     const workspacesEnabled = createMemo(() => {
       const project = panelProps.project
       if (!project) return false
@@ -1769,6 +1776,16 @@ export default function Layout(props: ParentProps) {
                               : language.t("sidebar.workspaces.enable")}
                           </DropdownMenu.ItemLabel>
                         </DropdownMenu.Item>
+                        <DropdownMenu.Item
+                          data-action="project-clear-notifications"
+                          data-project={base64Encode(p().worktree)}
+                          disabled={unseenCount() === 0}
+                          onSelect={clearNotifications}
+                        >
+                          <DropdownMenu.ItemLabel>
+                            {language.t("sidebar.project.clearNotifications")}
+                          </DropdownMenu.ItemLabel>
+                        </DropdownMenu.Item>
                         <DropdownMenu.Separator />
                         <DropdownMenu.Item
                           data-action="project-close-menu"

+ 90 - 65
packages/app/src/pages/layout/sidebar-project.tsx

@@ -10,6 +10,7 @@ import { createSortable } from "@thisbeyond/solid-dnd"
 import { type LocalProject } from "@/context/layout"
 import { useGlobalSync } from "@/context/global-sync"
 import { useLanguage } from "@/context/language"
+import { useNotification } from "@/context/notification"
 import { ProjectIcon, SessionItem, type SessionItemProps } from "./sidebar-items"
 import { childMapByParent, displayName, sortedRootSessions } from "./helpers"
 import { projectSelected, projectTileActive } from "./sidebar-project-helpers"
@@ -59,6 +60,7 @@ const ProjectTile = (props: {
   selected: Accessor<boolean>
   active: Accessor<boolean>
   overlay: Accessor<boolean>
+  dirs: Accessor<string[]>
   onProjectMouseEnter: (worktree: string, event: MouseEvent) => void
   onProjectMouseLeave: (worktree: string) => void
   onProjectFocus: (worktree: string) => void
@@ -70,73 +72,94 @@ const ProjectTile = (props: {
   setMenu: (value: boolean) => void
   setOpen: (value: boolean) => void
   language: ReturnType<typeof useLanguage>
-}): JSX.Element => (
-  <ContextMenu
-    modal={!props.sidebarHovering()}
-    onOpenChange={(value) => {
-      props.setMenu(value)
-      if (value) props.setOpen(false)
-    }}
-  >
-    <ContextMenu.Trigger
-      as="button"
-      type="button"
-      aria-label={displayName(props.project)}
-      data-action="project-switch"
-      data-project={base64Encode(props.project.worktree)}
-      classList={{
-        "flex items-center justify-center size-10 p-1 rounded-lg overflow-hidden transition-colors cursor-default": true,
-        "bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover": props.selected(),
-        "bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
-          !props.selected() && !props.active(),
-        "bg-surface-base-hover border border-border-weak-base": !props.selected() && props.active(),
-      }}
-      onMouseEnter={(event: MouseEvent) => {
-        if (!props.overlay()) return
-        props.onProjectMouseEnter(props.project.worktree, event)
-      }}
-      onMouseLeave={() => {
-        if (!props.overlay()) return
-        props.onProjectMouseLeave(props.project.worktree)
-      }}
-      onFocus={() => {
-        if (!props.overlay()) return
-        props.onProjectFocus(props.project.worktree)
+}): JSX.Element => {
+  const notification = useNotification()
+  const unseenCount = createMemo(() =>
+    props.dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0),
+  )
+
+  const clear = () =>
+    props
+      .dirs()
+      .filter((directory) => notification.project.unseenCount(directory) > 0)
+      .forEach((directory) => notification.project.markViewed(directory))
+
+  return (
+    <ContextMenu
+      modal={!props.sidebarHovering()}
+      onOpenChange={(value) => {
+        props.setMenu(value)
+        if (value) props.setOpen(false)
       }}
-      onClick={() => props.navigateToProject(props.project.worktree)}
-      onBlur={() => props.setOpen(false)}
     >
-      <ProjectIcon project={props.project} notify />
-    </ContextMenu.Trigger>
-    <ContextMenu.Portal mount={!props.mobile ? props.nav() : undefined}>
-      <ContextMenu.Content>
-        <ContextMenu.Item onSelect={() => props.showEditProjectDialog(props.project)}>
-          <ContextMenu.ItemLabel>{props.language.t("common.edit")}</ContextMenu.ItemLabel>
-        </ContextMenu.Item>
-        <ContextMenu.Item
-          data-action="project-workspaces-toggle"
-          data-project={base64Encode(props.project.worktree)}
-          disabled={props.project.vcs !== "git" && !props.workspacesEnabled(props.project)}
-          onSelect={() => props.toggleProjectWorkspaces(props.project)}
-        >
-          <ContextMenu.ItemLabel>
-            {props.workspacesEnabled(props.project)
-              ? props.language.t("sidebar.workspaces.disable")
-              : props.language.t("sidebar.workspaces.enable")}
-          </ContextMenu.ItemLabel>
-        </ContextMenu.Item>
-        <ContextMenu.Separator />
-        <ContextMenu.Item
-          data-action="project-close-menu"
-          data-project={base64Encode(props.project.worktree)}
-          onSelect={() => props.closeProject(props.project.worktree)}
-        >
-          <ContextMenu.ItemLabel>{props.language.t("common.close")}</ContextMenu.ItemLabel>
-        </ContextMenu.Item>
-      </ContextMenu.Content>
-    </ContextMenu.Portal>
-  </ContextMenu>
-)
+      <ContextMenu.Trigger
+        as="button"
+        type="button"
+        aria-label={displayName(props.project)}
+        data-action="project-switch"
+        data-project={base64Encode(props.project.worktree)}
+        classList={{
+          "flex items-center justify-center size-10 p-1 rounded-lg overflow-hidden transition-colors cursor-default": true,
+          "bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover": props.selected(),
+          "bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
+            !props.selected() && !props.active(),
+          "bg-surface-base-hover border border-border-weak-base": !props.selected() && props.active(),
+        }}
+        onMouseEnter={(event: MouseEvent) => {
+          if (!props.overlay()) return
+          props.onProjectMouseEnter(props.project.worktree, event)
+        }}
+        onMouseLeave={() => {
+          if (!props.overlay()) return
+          props.onProjectMouseLeave(props.project.worktree)
+        }}
+        onFocus={() => {
+          if (!props.overlay()) return
+          props.onProjectFocus(props.project.worktree)
+        }}
+        onClick={() => props.navigateToProject(props.project.worktree)}
+        onBlur={() => props.setOpen(false)}
+      >
+        <ProjectIcon project={props.project} notify />
+      </ContextMenu.Trigger>
+      <ContextMenu.Portal mount={!props.mobile ? props.nav() : undefined}>
+        <ContextMenu.Content>
+          <ContextMenu.Item onSelect={() => props.showEditProjectDialog(props.project)}>
+            <ContextMenu.ItemLabel>{props.language.t("common.edit")}</ContextMenu.ItemLabel>
+          </ContextMenu.Item>
+          <ContextMenu.Item
+            data-action="project-workspaces-toggle"
+            data-project={base64Encode(props.project.worktree)}
+            disabled={props.project.vcs !== "git" && !props.workspacesEnabled(props.project)}
+            onSelect={() => props.toggleProjectWorkspaces(props.project)}
+          >
+            <ContextMenu.ItemLabel>
+              {props.workspacesEnabled(props.project)
+                ? props.language.t("sidebar.workspaces.disable")
+                : props.language.t("sidebar.workspaces.enable")}
+            </ContextMenu.ItemLabel>
+          </ContextMenu.Item>
+          <ContextMenu.Item
+            data-action="project-clear-notifications"
+            data-project={base64Encode(props.project.worktree)}
+            disabled={unseenCount() === 0}
+            onSelect={clear}
+          >
+            <ContextMenu.ItemLabel>{props.language.t("sidebar.project.clearNotifications")}</ContextMenu.ItemLabel>
+          </ContextMenu.Item>
+          <ContextMenu.Separator />
+          <ContextMenu.Item
+            data-action="project-close-menu"
+            data-project={base64Encode(props.project.worktree)}
+            onSelect={() => props.closeProject(props.project.worktree)}
+          >
+            <ContextMenu.ItemLabel>{props.language.t("common.close")}</ContextMenu.ItemLabel>
+          </ContextMenu.Item>
+        </ContextMenu.Content>
+      </ContextMenu.Portal>
+    </ContextMenu>
+  )
+}
 
 const ProjectPreviewPanel = (props: {
   project: LocalProject
@@ -254,6 +277,7 @@ export const SortableProject = (props: {
   )
   const workspaces = createMemo(() => props.ctx.workspaceIds(props.project).slice(0, 2))
   const workspaceEnabled = createMemo(() => props.ctx.workspacesEnabled(props.project))
+  const dirs = createMemo(() => props.ctx.workspaceIds(props.project))
   const [open, setOpen] = createSignal(false)
   const [menu, setMenu] = createSignal(false)
 
@@ -304,6 +328,7 @@ export const SortableProject = (props: {
       selected={selected}
       active={active}
       overlay={overlay}
+      dirs={dirs}
       onProjectMouseEnter={props.ctx.onProjectMouseEnter}
       onProjectMouseLeave={props.ctx.onProjectMouseLeave}
       onProjectFocus={props.ctx.onProjectFocus}