diff options
| -rw-r--r-- | packages/app/src/components/session/session-header.tsx | 180 | ||||
| -rw-r--r-- | packages/app/src/pages/session.tsx | 66 |
2 files changed, 212 insertions, 34 deletions
diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 7070f0c93..4c709feef 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/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> diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index dbdbbc7eb..d76ff99b3 100644 --- a/packages/app/src/pages/session.tsx +++ b/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) => { |
