Kaynağa Gözat

feat(desktop): implement session unshare button (#8660)

Mani Sundararajan 1 ay önce
ebeveyn
işleme
06bc4dcb06

+ 146 - 34
packages/app/src/components/session/session-header.tsx

@@ -1,15 +1,17 @@
-import { createMemo, createResource, Show } from "solid-js"
+import { createEffect, createMemo, onCleanup, Show } from "solid-js"
+import { createStore } from "solid-js/store"
 import { Portal } from "solid-js/web"
 import { useParams } from "@solidjs/router"
 import { useLayout } from "@/context/layout"
 import { useCommand } from "@/context/command"
 // import { useServer } from "@/context/server"
 // import { useDialog } from "@opencode-ai/ui/context/dialog"
+import { usePlatform } from "@/context/platform"
 import { useSync } from "@/context/sync"
 import { useGlobalSDK } from "@/context/global-sdk"
 import { getFilename } from "@opencode-ai/util/path"
 import { base64Decode } from "@opencode-ai/util/encode"
-import { iife } from "@opencode-ai/util/iife"
+
 import { Icon } from "@opencode-ai/ui/icon"
 import { IconButton } from "@opencode-ai/ui/icon-button"
 import { Button } from "@opencode-ai/ui/button"
@@ -26,6 +28,7 @@ export function SessionHeader() {
   // const server = useServer()
   // const dialog = useDialog()
   const sync = useSync()
+  const platform = usePlatform()
 
   const projectDirectory = createMemo(() => base64Decode(params.dir ?? ""))
   const project = createMemo(() => {
@@ -45,6 +48,78 @@ export function SessionHeader() {
   const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
   const view = createMemo(() => layout.view(sessionKey()))
 
+  const [state, setState] = createStore({
+    share: false,
+    unshare: false,
+    copied: false,
+    timer: undefined as number | undefined,
+  })
+  const shareUrl = createMemo(() => currentSession()?.share?.url)
+
+  createEffect(() => {
+    const url = shareUrl()
+    if (url) return
+    if (state.timer) window.clearTimeout(state.timer)
+    setState({ copied: false, timer: undefined })
+  })
+
+  onCleanup(() => {
+    if (state.timer) window.clearTimeout(state.timer)
+  })
+
+  function shareSession() {
+    const session = currentSession()
+    if (!session || state.share) return
+    setState("share", true)
+    globalSDK.client.session
+      .share({ sessionID: session.id, directory: projectDirectory() })
+      .catch((error) => {
+        console.error("Failed to share session", error)
+      })
+      .finally(() => {
+        setState("share", false)
+      })
+  }
+
+  function unshareSession() {
+    const session = currentSession()
+    if (!session || state.unshare) return
+    setState("unshare", true)
+    globalSDK.client.session
+      .unshare({ sessionID: session.id, directory: projectDirectory() })
+      .catch((error) => {
+        console.error("Failed to unshare session", error)
+      })
+      .finally(() => {
+        setState("unshare", false)
+      })
+  }
+
+  function copyLink() {
+    const url = shareUrl()
+    if (!url) return
+    navigator.clipboard
+      .writeText(url)
+      .then(() => {
+        if (state.timer) window.clearTimeout(state.timer)
+        setState("copied", true)
+        const timer = window.setTimeout(() => {
+          setState("copied", false)
+          setState("timer", undefined)
+        }, 3000)
+        setState("timer", timer)
+      })
+      .catch((error) => {
+        console.error("Failed to copy share link", error)
+      })
+  }
+
+  function viewShare() {
+    const url = shareUrl()
+    if (!url) return
+    platform.openLink(url)
+  }
+
   const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center"))
   const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right"))
 
@@ -159,40 +234,77 @@ export function SessionHeader() {
                 </TooltipKeybind>
               </div>
               <Show when={shareEnabled() && currentSession()}>
-                <Popover
-                  title="Share session"
-                  trigger={
-                    <Tooltip class="shrink-0" value="Share session">
-                      <IconButton icon="share" variant="ghost" class="" />
-                    </Tooltip>
-                  }
-                >
-                  {iife(() => {
-                    const [url] = createResource(
-                      () => currentSession(),
-                      async (session) => {
-                        if (!session) return
-                        let shareURL = session.share?.url
-                        if (!shareURL) {
-                          shareURL = await globalSDK.client.session
-                            .share({ sessionID: session.id, directory: projectDirectory() })
-                            .then((r) => r.data?.share?.url)
-                            .catch((e) => {
-                              console.error("Failed to share session", e)
-                              return undefined
-                            })
+                <div class="flex items-center">
+                  <Popover
+                    title="Publish on web"
+                    description={
+                      shareUrl()
+                        ? "This session is public on the web. It is accessible to anyone with the link."
+                        : "Share session publicly on the web. It will be accessible to anyone with the link."
+                    }
+                    trigger={
+                      <Tooltip class="shrink-0" value="Share session">
+                        <Button variant="secondary" classList={{ "rounded-r-none": shareUrl() !== undefined }}>
+                          Share
+                        </Button>
+                      </Tooltip>
+                    }
+                  >
+                    <div class="flex flex-col gap-2">
+                      <Show
+                        when={shareUrl()}
+                        fallback={
+                          <div class="flex">
+                            <Button
+                              size="large"
+                              variant="primary"
+                              class="w-1/2"
+                              onClick={shareSession}
+                              disabled={state.share}
+                            >
+                              {state.share ? "Publishing..." : "Publish"}
+                            </Button>
+                          </div>
                         }
-                        return shareURL
-                      },
-                      { initialValue: "" },
-                    )
-                    return (
-                      <Show when={url.latest}>
-                        {(shareUrl) => <TextField value={shareUrl()} readOnly copyable class="w-72" />}
+                      >
+                        <div class="flex flex-col gap-2 w-72">
+                          <TextField value={shareUrl() ?? ""} readOnly copyable class="w-full" />
+                          <div class="grid grid-cols-2 gap-2">
+                            <Button
+                              size="large"
+                              variant="secondary"
+                              class="w-full shadow-none border border-border-weak-base"
+                              onClick={unshareSession}
+                              disabled={state.unshare}
+                            >
+                              {state.unshare ? "Unpublishing..." : "Unpublish"}
+                            </Button>
+                            <Button
+                              size="large"
+                              variant="primary"
+                              class="w-full"
+                              onClick={viewShare}
+                              disabled={state.unshare}
+                            >
+                              View
+                            </Button>
+                          </div>
+                        </div>
                       </Show>
-                    )
-                  })}
-                </Popover>
+                    </div>
+                  </Popover>
+                  <Show when={shareUrl()}>
+                    <Tooltip value={state.copied ? "Copied" : "Copy link"} placement="top" gutter={8}>
+                      <IconButton
+                        icon={state.copied ? "check" : "copy"}
+                        variant="secondary"
+                        class="rounded-l-none border-l border-border-weak-base"
+                        onClick={copyLink}
+                        disabled={state.unshare}
+                      />
+                    </Tooltip>
+                  </Show>
+                </div>
               </Show>
             </div>
           </Portal>

+ 66 - 0
packages/app/src/pages/session.tsx

@@ -654,6 +654,72 @@ export default function Page() {
       disabled: !params.id || visibleUserMessages().length === 0,
       onSelect: () => dialog.show(() => <DialogFork />),
     },
+    ...(sync.data.config.share !== "disabled"
+      ? [
+          {
+            id: "session.share",
+            title: "Share session",
+            description: "Share this session and copy the URL to clipboard",
+            category: "Session",
+            slash: "share",
+            disabled: !params.id || !!info()?.share?.url,
+            onSelect: async () => {
+              if (!params.id) return
+              await sdk.client.session
+                .share({ sessionID: params.id })
+                .then((res) => {
+                  navigator.clipboard.writeText(res.data!.share!.url).catch(() =>
+                    showToast({
+                      title: "Failed to copy URL to clipboard",
+                      variant: "error",
+                    }),
+                  )
+                })
+                .then(() =>
+                  showToast({
+                    title: "Session shared",
+                    description: "Share URL copied to clipboard!",
+                    variant: "success",
+                  }),
+                )
+                .catch(() =>
+                  showToast({
+                    title: "Failed to share session",
+                    description: "An error occurred while sharing the session",
+                    variant: "error",
+                  }),
+                )
+            },
+          },
+          {
+            id: "session.unshare",
+            title: "Unshare session",
+            description: "Stop sharing this session",
+            category: "Session",
+            slash: "unshare",
+            disabled: !params.id || !info()?.share?.url,
+            onSelect: async () => {
+              if (!params.id) return
+              await sdk.client.session
+                .unshare({ sessionID: params.id })
+                .then(() =>
+                  showToast({
+                    title: "Session unshared",
+                    description: "Session unshared successfully!",
+                    variant: "success",
+                  }),
+                )
+                .catch(() =>
+                  showToast({
+                    title: "Failed to unshare session",
+                    description: "An error occurred while unsharing the session",
+                    variant: "error",
+                  }),
+                )
+            },
+          },
+        ]
+      : []),
   ])
 
   const handleKeyDown = (event: KeyboardEvent) => {