fix(app): move session share into options menu
Remove the title bar share control and surface sharing from the session options dropdown, opening the share popover in-place after selection to avoid flicker and keep consistent dismissal behavior.
This commit is contained in:
parent
21f562fdae
commit
5ca2dc87b7
2 changed files with 191 additions and 216 deletions
|
|
@ -4,23 +4,19 @@ import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||||
import { Popover } from "@opencode-ai/ui/popover"
|
|
||||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { createEffect, createMemo, For, onCleanup, Show } from "solid-js"
|
import { createEffect, createMemo, For, onCleanup, Show } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { Portal } from "solid-js/web"
|
import { Portal } from "solid-js/web"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useServer } from "@/context/server"
|
import { useServer } from "@/context/server"
|
||||||
import { useSync } from "@/context/sync"
|
|
||||||
import { useTerminal } from "@/context/terminal"
|
import { useTerminal } from "@/context/terminal"
|
||||||
import { focusTerminalById } from "@/pages/session/helpers"
|
import { focusTerminalById } from "@/pages/session/helpers"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
|
|
@ -136,99 +132,11 @@ const showRequestError = (language: ReturnType<typeof useLanguage>, err: unknown
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function useSessionShare(args: {
|
|
||||||
globalSDK: ReturnType<typeof useGlobalSDK>
|
|
||||||
currentSession: () =>
|
|
||||||
| {
|
|
||||||
share?: {
|
|
||||||
url?: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
| undefined
|
|
||||||
sessionID: () => string | undefined
|
|
||||||
projectDirectory: () => string
|
|
||||||
platform: ReturnType<typeof usePlatform>
|
|
||||||
}) {
|
|
||||||
const [state, setState] = createStore({
|
|
||||||
share: false,
|
|
||||||
unshare: false,
|
|
||||||
copied: false,
|
|
||||||
timer: undefined as number | undefined,
|
|
||||||
})
|
|
||||||
const shareUrl = createMemo(() => args.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)
|
|
||||||
})
|
|
||||||
|
|
||||||
const shareSession = () => {
|
|
||||||
const sessionID = args.sessionID()
|
|
||||||
if (!sessionID || state.share) return
|
|
||||||
setState("share", true)
|
|
||||||
args.globalSDK.client.session
|
|
||||||
.share({ sessionID, directory: args.projectDirectory() })
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Failed to share session", error)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setState("share", false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const unshareSession = () => {
|
|
||||||
const sessionID = args.sessionID()
|
|
||||||
if (!sessionID || state.unshare) return
|
|
||||||
setState("unshare", true)
|
|
||||||
args.globalSDK.client.session
|
|
||||||
.unshare({ sessionID, directory: args.projectDirectory() })
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Failed to unshare session", error)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setState("unshare", false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const copyLink = (onError: (error: unknown) => void) => {
|
|
||||||
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(onError)
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewShare = () => {
|
|
||||||
const url = shareUrl()
|
|
||||||
if (!url) return
|
|
||||||
args.platform.openLink(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
return { state, shareUrl, shareSession, unshareSession, copyLink, viewShare }
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SessionHeader() {
|
export function SessionHeader() {
|
||||||
const globalSDK = useGlobalSDK()
|
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const sync = useSync()
|
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const terminal = useTerminal()
|
const terminal = useTerminal()
|
||||||
|
|
@ -246,9 +154,6 @@ export function SessionHeader() {
|
||||||
})
|
})
|
||||||
const hotkey = createMemo(() => command.keybind("file.open"))
|
const hotkey = createMemo(() => command.keybind("file.open"))
|
||||||
|
|
||||||
const currentSession = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
|
||||||
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
|
|
||||||
const showShare = createMemo(() => shareEnabled() && !!params.id)
|
|
||||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
const view = createMemo(() => layout.view(sessionKey))
|
const view = createMemo(() => layout.view(sessionKey))
|
||||||
const os = createMemo(() => detectOS(platform))
|
const os = createMemo(() => detectOS(platform))
|
||||||
|
|
@ -361,14 +266,6 @@ export function SessionHeader() {
|
||||||
.catch((err: unknown) => showRequestError(language, err))
|
.catch((err: unknown) => showRequestError(language, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
const share = useSessionShare({
|
|
||||||
globalSDK,
|
|
||||||
currentSession,
|
|
||||||
sessionID: () => params.id,
|
|
||||||
projectDirectory,
|
|
||||||
platform,
|
|
||||||
})
|
|
||||||
|
|
||||||
const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center"))
|
const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center"))
|
||||||
const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right"))
|
const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right"))
|
||||||
|
|
||||||
|
|
@ -523,112 +420,6 @@ export function SessionHeader() {
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={showShare()}>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<Popover
|
|
||||||
title={language.t("session.share.popover.title")}
|
|
||||||
description={
|
|
||||||
share.shareUrl()
|
|
||||||
? language.t("session.share.popover.description.shared")
|
|
||||||
: language.t("session.share.popover.description.unshared")
|
|
||||||
}
|
|
||||||
gutter={4}
|
|
||||||
placement="bottom-end"
|
|
||||||
shift={-64}
|
|
||||||
class="rounded-xl [&_[data-slot=popover-close-button]]:hidden"
|
|
||||||
triggerAs={Button}
|
|
||||||
triggerProps={{
|
|
||||||
variant: "ghost",
|
|
||||||
class:
|
|
||||||
"rounded-md h-[24px] px-3 border border-border-weak-base bg-surface-panel shadow-none data-[expanded]:bg-surface-base-active",
|
|
||||||
classList: {
|
|
||||||
"rounded-r-none": share.shareUrl() !== undefined,
|
|
||||||
"border-r-0": share.shareUrl() !== undefined,
|
|
||||||
},
|
|
||||||
style: { scale: 1 },
|
|
||||||
}}
|
|
||||||
trigger={<span class="text-12-regular">{language.t("session.share.action.share")}</span>}
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<Show
|
|
||||||
when={share.shareUrl()}
|
|
||||||
fallback={
|
|
||||||
<div class="flex">
|
|
||||||
<Button
|
|
||||||
size="large"
|
|
||||||
variant="primary"
|
|
||||||
class="w-1/2"
|
|
||||||
onClick={share.shareSession}
|
|
||||||
disabled={share.state.share}
|
|
||||||
>
|
|
||||||
{share.state.share
|
|
||||||
? language.t("session.share.action.publishing")
|
|
||||||
: language.t("session.share.action.publish")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<TextField
|
|
||||||
value={share.shareUrl() ?? ""}
|
|
||||||
readOnly
|
|
||||||
copyable
|
|
||||||
copyKind="link"
|
|
||||||
tabIndex={-1}
|
|
||||||
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={share.unshareSession}
|
|
||||||
disabled={share.state.unshare}
|
|
||||||
>
|
|
||||||
{share.state.unshare
|
|
||||||
? language.t("session.share.action.unpublishing")
|
|
||||||
: language.t("session.share.action.unpublish")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="large"
|
|
||||||
variant="primary"
|
|
||||||
class="w-full"
|
|
||||||
onClick={share.viewShare}
|
|
||||||
disabled={share.state.unshare}
|
|
||||||
>
|
|
||||||
{language.t("session.share.action.view")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
<Show when={share.shareUrl()} fallback={<div aria-hidden="true" />}>
|
|
||||||
<Tooltip
|
|
||||||
value={
|
|
||||||
share.state.copied
|
|
||||||
? language.t("session.share.copy.copied")
|
|
||||||
: language.t("session.share.copy.copyLink")
|
|
||||||
}
|
|
||||||
placement="top"
|
|
||||||
gutter={8}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
icon={share.state.copied ? "check" : "link"}
|
|
||||||
variant="ghost"
|
|
||||||
class="rounded-l-none h-[24px] border border-border-weak-base bg-surface-panel shadow-none"
|
|
||||||
onClick={() => share.copyLink((error) => showRequestError(language, error))}
|
|
||||||
disabled={share.state.unshare}
|
|
||||||
aria-label={
|
|
||||||
share.state.copied
|
|
||||||
? language.t("session.share.copy.copied")
|
|
||||||
: language.t("session.share.copy.copyLink")
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
title={language.t("command.terminal.toggle")}
|
title={language.t("command.terminal.toggle")}
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,18 @@ import { InlineInput } from "@opencode-ai/ui/inline-input"
|
||||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||||
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
||||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||||
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import type { AssistantMessage, Message as MessageType, Part, TextPart, UserMessage } from "@opencode-ai/sdk/v2"
|
import type { AssistantMessage, Message as MessageType, Part, TextPart, UserMessage } from "@opencode-ai/sdk/v2"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { Binary } from "@opencode-ai/util/binary"
|
import { Binary } from "@opencode-ai/util/binary"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
|
import { Popover as KobaltePopover } from "@kobalte/core/popover"
|
||||||
import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture"
|
import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture"
|
||||||
import { SessionContextUsage } from "@/components/session-context-usage"
|
import { SessionContextUsage } from "@/components/session-context-usage"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
|
|
@ -209,11 +213,13 @@ export function MessageTimeline(props: {
|
||||||
|
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const globalSDK = useGlobalSDK()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
const platform = usePlatform()
|
||||||
|
|
||||||
const rendered = createMemo(() => props.renderedUserMessages.map((message) => message.id))
|
const rendered = createMemo(() => props.renderedUserMessages.map((message) => message.id))
|
||||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
|
|
@ -292,6 +298,8 @@ export function MessageTimeline(props: {
|
||||||
return sync.session.get(id)
|
return sync.session.get(id)
|
||||||
})
|
})
|
||||||
const titleValue = createMemo(() => info()?.title)
|
const titleValue = createMemo(() => info()?.title)
|
||||||
|
const shareUrl = createMemo(() => info()?.share?.url)
|
||||||
|
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
|
||||||
const parentID = createMemo(() => info()?.parentID)
|
const parentID = createMemo(() => info()?.parentID)
|
||||||
const showHeader = createMemo(() => !!(titleValue() || parentID()))
|
const showHeader = createMemo(() => !!(titleValue() || parentID()))
|
||||||
const stageCfg = { init: 1, batch: 3 }
|
const stageCfg = { init: 1, batch: 3 }
|
||||||
|
|
@ -308,9 +316,55 @@ export function MessageTimeline(props: {
|
||||||
saving: false,
|
saving: false,
|
||||||
menuOpen: false,
|
menuOpen: false,
|
||||||
pendingRename: false,
|
pendingRename: false,
|
||||||
|
pendingShare: false,
|
||||||
})
|
})
|
||||||
let titleRef: HTMLInputElement | undefined
|
let titleRef: HTMLInputElement | undefined
|
||||||
|
|
||||||
|
const [share, setShare] = createStore({
|
||||||
|
open: false,
|
||||||
|
dismiss: null as "escape" | "outside" | null,
|
||||||
|
})
|
||||||
|
|
||||||
|
let more: HTMLButtonElement | undefined
|
||||||
|
|
||||||
|
const [req, setReq] = createStore({ share: false, unshare: false })
|
||||||
|
|
||||||
|
const shareSession = () => {
|
||||||
|
const id = sessionID()
|
||||||
|
if (!id || req.share) return
|
||||||
|
if (!shareEnabled()) return
|
||||||
|
setReq("share", true)
|
||||||
|
globalSDK.client.session
|
||||||
|
.share({ sessionID: id, directory: sdk.directory })
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Failed to share session", error)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setReq("share", false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const unshareSession = () => {
|
||||||
|
const id = sessionID()
|
||||||
|
if (!id || req.unshare) return
|
||||||
|
if (!shareEnabled()) return
|
||||||
|
setReq("unshare", true)
|
||||||
|
globalSDK.client.session
|
||||||
|
.unshare({ sessionID: id, directory: sdk.directory })
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Failed to unshare session", error)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setReq("unshare", false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewShare = () => {
|
||||||
|
const url = shareUrl()
|
||||||
|
if (!url) return
|
||||||
|
platform.openLink(url)
|
||||||
|
}
|
||||||
|
|
||||||
const errorMessage = (err: unknown) => {
|
const errorMessage = (err: unknown) => {
|
||||||
if (err && typeof err === "object" && "data" in err) {
|
if (err && typeof err === "object" && "data" in err) {
|
||||||
const data = (err as { data?: { message?: string } }).data
|
const data = (err as { data?: { message?: string } }).data
|
||||||
|
|
@ -323,7 +377,15 @@ export function MessageTimeline(props: {
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
sessionKey,
|
sessionKey,
|
||||||
() => setTitle({ draft: "", editing: false, saving: false, menuOpen: false, pendingRename: false }),
|
() =>
|
||||||
|
setTitle({
|
||||||
|
draft: "",
|
||||||
|
editing: false,
|
||||||
|
saving: false,
|
||||||
|
menuOpen: false,
|
||||||
|
pendingRename: false,
|
||||||
|
pendingShare: false,
|
||||||
|
}),
|
||||||
{ defer: true },
|
{ defer: true },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -672,7 +734,10 @@ export function MessageTimeline(props: {
|
||||||
gutter={4}
|
gutter={4}
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
open={title.menuOpen}
|
open={title.menuOpen}
|
||||||
onOpenChange={(open) => setTitle("menuOpen", open)}
|
onOpenChange={(open) => {
|
||||||
|
setTitle("menuOpen", open)
|
||||||
|
if (open) return
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Trigger
|
<DropdownMenu.Trigger
|
||||||
as={IconButton}
|
as={IconButton}
|
||||||
|
|
@ -680,15 +745,25 @@ export function MessageTimeline(props: {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
|
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
|
||||||
aria-label={language.t("common.moreOptions")}
|
aria-label={language.t("common.moreOptions")}
|
||||||
|
ref={(el: HTMLButtonElement) => {
|
||||||
|
more = el
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<DropdownMenu.Portal>
|
<DropdownMenu.Portal>
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
style={{ "min-width": "104px" }}
|
style={{ "min-width": "104px" }}
|
||||||
onCloseAutoFocus={(event) => {
|
onCloseAutoFocus={(event) => {
|
||||||
if (!title.pendingRename) return
|
if (title.pendingRename) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
setTitle("pendingRename", false)
|
setTitle("pendingRename", false)
|
||||||
openTitleEditor()
|
openTitleEditor()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (title.pendingShare) {
|
||||||
|
event.preventDefault()
|
||||||
|
setTitle("pendingShare", false)
|
||||||
|
requestAnimationFrame(() => setShare({ open: true, dismiss: null }))
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
|
|
@ -699,6 +774,17 @@ export function MessageTimeline(props: {
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
<Show when={shareEnabled()}>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onSelect={() => {
|
||||||
|
setTitle({ pendingShare: true, menuOpen: false })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemLabel>
|
||||||
|
{language.t("session.share.action.share")}
|
||||||
|
</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</Show>
|
||||||
<DropdownMenu.Item onSelect={() => void archiveSession(id())}>
|
<DropdownMenu.Item onSelect={() => void archiveSession(id())}>
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|
@ -711,6 +797,104 @@ export function MessageTimeline(props: {
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Portal>
|
</DropdownMenu.Portal>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
|
<KobaltePopover
|
||||||
|
open={share.open}
|
||||||
|
anchorRef={() => more}
|
||||||
|
placement="bottom-end"
|
||||||
|
gutter={4}
|
||||||
|
modal={false}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (open) setShare("dismiss", null)
|
||||||
|
setShare("open", open)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<KobaltePopover.Portal>
|
||||||
|
<KobaltePopover.Content
|
||||||
|
data-component="popover-content"
|
||||||
|
style={{ "min-width": "320px" }}
|
||||||
|
onEscapeKeyDown={(event) => {
|
||||||
|
setShare({ dismiss: "escape", open: false })
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}}
|
||||||
|
onPointerDownOutside={() => {
|
||||||
|
setShare({ dismiss: "outside", open: false })
|
||||||
|
}}
|
||||||
|
onFocusOutside={() => {
|
||||||
|
setShare({ dismiss: "outside", open: false })
|
||||||
|
}}
|
||||||
|
onCloseAutoFocus={(event) => {
|
||||||
|
if (share.dismiss === "outside") event.preventDefault()
|
||||||
|
setShare("dismiss", null)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex flex-col p-3">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<div class="text-13-medium text-text-strong">
|
||||||
|
{language.t("session.share.popover.title")}
|
||||||
|
</div>
|
||||||
|
<div class="text-12-regular text-text-weak">
|
||||||
|
{shareUrl()
|
||||||
|
? language.t("session.share.popover.description.shared")
|
||||||
|
: language.t("session.share.popover.description.unshared")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 flex flex-col gap-2">
|
||||||
|
<Show
|
||||||
|
when={shareUrl()}
|
||||||
|
fallback={
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
variant="primary"
|
||||||
|
class="w-full"
|
||||||
|
onClick={shareSession}
|
||||||
|
disabled={req.share}
|
||||||
|
>
|
||||||
|
{req.share
|
||||||
|
? language.t("session.share.action.publishing")
|
||||||
|
: language.t("session.share.action.publish")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<TextField
|
||||||
|
value={shareUrl() ?? ""}
|
||||||
|
readOnly
|
||||||
|
copyable
|
||||||
|
copyKind="link"
|
||||||
|
tabIndex={-1}
|
||||||
|
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={req.unshare}
|
||||||
|
>
|
||||||
|
{req.unshare
|
||||||
|
? language.t("session.share.action.unpublishing")
|
||||||
|
: language.t("session.share.action.unpublish")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
variant="primary"
|
||||||
|
class="w-full"
|
||||||
|
onClick={viewShare}
|
||||||
|
disabled={req.unshare}
|
||||||
|
>
|
||||||
|
{language.t("session.share.action.view")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</KobaltePopover.Content>
|
||||||
|
</KobaltePopover.Portal>
|
||||||
|
</KobaltePopover>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue