Merge the room and DM composers into Compose and share their reply, edit and share state through ComposeArea
This commit is contained in:
parent
964a1bb6fa
commit
c710a15879
8 changed files with 233 additions and 430 deletions
|
|
@ -25,7 +25,7 @@ type Seeded = {readonly id: string; readonly event: SignedEvent}
|
||||||
const composerForm = (page: Page) =>
|
const composerForm = (page: Page) =>
|
||||||
page.locator("form").filter({has: page.locator(".note-editor")})
|
page.locator("form").filter({has: page.locator(".note-editor")})
|
||||||
|
|
||||||
// RoomCompose's join is the upload button and then the compose menu.
|
// A room composer's join is the upload button and then the compose menu.
|
||||||
const openComposeMenu = (page: Page) =>
|
const openComposeMenu = (page: Page) =>
|
||||||
page
|
page
|
||||||
.locator("form")
|
.locator("form")
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import {
|
||||||
|
|
||||||
const suggestions = (page: Page) => page.locator(".tiptap-suggestions__item")
|
const suggestions = (page: Page) => page.locator(".tiptap-suggestions__item")
|
||||||
|
|
||||||
// RoomCompose leads with a two button group: upload, then the create menu.
|
// A room composer leads with a two button group: upload, then the create menu.
|
||||||
const roomUploadButton = (page: Page) => page.locator(".room__compose-inner .join-item").first()
|
const roomUploadButton = (page: Page) => page.locator(".room__compose-inner .join-item").first()
|
||||||
|
|
||||||
const chatUploadButton = (page: Page) => page.locator("button[data-tip='Add an image']")
|
const chatUploadButton = (page: Page) => page.locator("button[data-tip='Add an image']")
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
int,
|
int,
|
||||||
ms,
|
ms,
|
||||||
partition,
|
partition,
|
||||||
ifLet,
|
|
||||||
spec,
|
spec,
|
||||||
nthEq,
|
nthEq,
|
||||||
nthNe,
|
nthNe,
|
||||||
|
|
@ -16,8 +15,7 @@
|
||||||
enumerate,
|
enumerate,
|
||||||
formatTimestampAsDate,
|
formatTimestampAsDate,
|
||||||
} from "@welshman/lib"
|
} from "@welshman/lib"
|
||||||
import type {Maybe} from "@welshman/lib"
|
import type {TrustedEvent, EventTemplate} from "@welshman/util"
|
||||||
import type {TrustedEvent, EventTemplate, EventContent} from "@welshman/util"
|
|
||||||
import {makeEvent, userInbox, DIRECT_MESSAGE, DIRECT_MESSAGE_FILE} from "@welshman/util"
|
import {makeEvent, userInbox, DIRECT_MESSAGE, DIRECT_MESSAGE_FILE} from "@welshman/util"
|
||||||
import {parse, isLink} from "@welshman/content"
|
import {parse, isLink} from "@welshman/content"
|
||||||
import {MessagingRelayLists, Thunks} from "@welshman/app"
|
import {MessagingRelayLists, Thunks} from "@welshman/app"
|
||||||
|
|
@ -36,18 +34,14 @@
|
||||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
import ChatMembers from "@app/components/ChatMembers.svelte"
|
import ChatMembers from "@app/components/ChatMembers.svelte"
|
||||||
import ChatMessage from "@app/components/ChatMessage.svelte"
|
import ChatMessage from "@app/components/ChatMessage.svelte"
|
||||||
import ChatCompose from "@app/components/ChatCompose.svelte"
|
import ComposeArea, {type ComposeValues} from "@app/components/ComposeArea.svelte"
|
||||||
import ComposeEdit from "@app/components/ComposeEdit.svelte"
|
|
||||||
import ComposeParent from "@app/components/ComposeParent.svelte"
|
|
||||||
import ThunkToast from "@app/components/ThunkToast.svelte"
|
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||||
import {app, deletes, router, user, wraps} from "@app/core"
|
import {app, deletes, router, user, wraps} from "@app/core"
|
||||||
import {loadSendDelay} from "@app/settings"
|
import {loadSendDelay} from "@app/settings"
|
||||||
import {deriveChat, makeChatId} from "@app/chats"
|
import {deriveChat, makeChatId} from "@app/chats"
|
||||||
import {makeFeedContext} from "@app/feeds"
|
import {makeFeedContext} from "@app/feeds"
|
||||||
import {navigate, pushModal} from "@app/modal"
|
import {navigate, pushModal} from "@app/modal"
|
||||||
import {DraftKey, type Draft} from "@app/drafts"
|
|
||||||
import {prependParent} from "@app/rooms"
|
import {prependParent} from "@app/rooms"
|
||||||
import {pendingShare, type Share} from "@app/share"
|
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -62,7 +56,6 @@
|
||||||
|
|
||||||
onDestroy(context.cleanup)
|
onDestroy(context.cleanup)
|
||||||
const chat = deriveChat(chatId)
|
const chat = deriveChat(chatId)
|
||||||
const draftKey = new DraftKey<Draft>(`dm:${chatId}`)
|
|
||||||
const others = remove($user.pubkey, pubkeys)
|
const others = remove($user.pubkey, pubkeys)
|
||||||
const messagingRelayLists = $app.use(MessagingRelayLists).index.$
|
const messagingRelayLists = $app.use(MessagingRelayLists).index.$
|
||||||
const missingRelayLists = $derived(others.filter(pk => !$messagingRelayLists.has(pk)))
|
const missingRelayLists = $derived(others.filter(pk => !$messagingRelayLists.has(pk)))
|
||||||
|
|
@ -74,35 +67,19 @@
|
||||||
|
|
||||||
const back = () => navigate("/chat")
|
const back = () => navigate("/chat")
|
||||||
|
|
||||||
const replyTo = (event: TrustedEvent) => {
|
const replyTo = (event: TrustedEvent) => composeArea?.reply(event)
|
||||||
parent = event
|
|
||||||
compose?.focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearParent = () => {
|
const onSubmit = async ({parent, edit, ...values}: ComposeValues) => {
|
||||||
parent = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearEventToEdit = () => {
|
|
||||||
eventToEdit = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSubmit = async (params: EventContent) => {
|
|
||||||
const ptags = others.map(pk => ["p", pk])
|
const ptags = others.map(pk => ["p", pk])
|
||||||
|
|
||||||
// Remove p tags since they result in forking the conversation
|
// p tags would fork the conversation, so drop them before quoting the parent
|
||||||
params.tags = params.tags.filter(nthNe(0, "p"))
|
const params = await prependParent(parent, {
|
||||||
|
content: values.content,
|
||||||
|
tags: values.tags.filter(nthNe(0, "p")),
|
||||||
|
})
|
||||||
|
|
||||||
// Add our reply quote to content
|
if (edit) {
|
||||||
params = await prependParent(parent, params)
|
const command = await $deletes.deleteEvent(edit)
|
||||||
|
|
||||||
if (eventToEdit) {
|
|
||||||
// An edit that changes nothing is just a dismissal
|
|
||||||
if (eventToEdit.content === params.content) {
|
|
||||||
return clearEventToEdit()
|
|
||||||
}
|
|
||||||
|
|
||||||
const command = await $deletes.deleteEvent(eventToEdit)
|
|
||||||
|
|
||||||
await $wraps.publish({event: command.event, recipients: pubkeys, pow: 16})
|
await $wraps.publish({event: command.event, recipients: pubkeys, pow: 16})
|
||||||
}
|
}
|
||||||
|
|
@ -120,7 +97,7 @@
|
||||||
makeEvent(kind, {
|
makeEvent(kind, {
|
||||||
content,
|
content,
|
||||||
tags: [...tags, ...ptags],
|
tags: [...tags, ...ptags],
|
||||||
created_at: eventToEdit?.created_at,
|
created_at: edit?.created_at,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -155,10 +132,6 @@
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Publishing reads each recipient's messaging relays first, and a failed read throws before any thunk.
|
|
||||||
clearParent()
|
|
||||||
clearEventToEdit()
|
|
||||||
|
|
||||||
pushToast({
|
pushToast({
|
||||||
timeout: 30_000,
|
timeout: 30_000,
|
||||||
children: {
|
children: {
|
||||||
|
|
@ -168,46 +141,15 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const onEscape = () => {
|
|
||||||
clearParent()
|
|
||||||
clearEventToEdit()
|
|
||||||
}
|
|
||||||
|
|
||||||
const canEditEvent = (event: TrustedEvent) =>
|
const canEditEvent = (event: TrustedEvent) =>
|
||||||
event.pubkey === $user.pubkey &&
|
event.pubkey === $user.pubkey &&
|
||||||
event.kind === DIRECT_MESSAGE &&
|
event.kind === DIRECT_MESSAGE &&
|
||||||
event.created_at >= ago(500, MINUTE)
|
event.created_at >= ago(500, MINUTE)
|
||||||
|
|
||||||
const onEditEvent = (event: TrustedEvent) => {
|
const onEditEvent = (event: TrustedEvent) => composeArea?.edit(event)
|
||||||
clearParent()
|
|
||||||
eventToEdit = event
|
|
||||||
}
|
|
||||||
|
|
||||||
const onEditPrevious = () => ifLet($chat?.messages.find(canEditEvent), onEditEvent)
|
|
||||||
|
|
||||||
let loading = $state(true)
|
let loading = $state(true)
|
||||||
let compose: ChatCompose | undefined = $state()
|
let composeArea: ComposeArea | undefined = $state()
|
||||||
let parent: TrustedEvent | undefined = $state()
|
|
||||||
let eventToEdit: TrustedEvent | undefined = $state()
|
|
||||||
let share: Maybe<Share> = $state()
|
|
||||||
|
|
||||||
// Sharing into the conversation already on screen doesn't re-create this component.
|
|
||||||
$effect(() => {
|
|
||||||
if ($pendingShare) {
|
|
||||||
share = $pendingShare
|
|
||||||
pendingShare.set(undefined)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const initialValues = $derived.by((): Share | undefined => {
|
|
||||||
if (eventToEdit) {
|
|
||||||
return {type: "text", value: eventToEdit.content}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (share) {
|
|
||||||
return share
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const elements = $derived.by(() => {
|
const elements = $derived.by(() => {
|
||||||
const elements = []
|
const elements = []
|
||||||
|
|
@ -335,25 +277,14 @@
|
||||||
</p>
|
</p>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
|
||||||
<div class="room__compose bg-surface">
|
<div class="room__compose bg-surface px-2">
|
||||||
<div class="flex flex-col gap-px">
|
<ComposeArea
|
||||||
{#if parent}
|
{onSubmit}
|
||||||
<ComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
encryptFiles
|
||||||
{/if}
|
draftKey="dm:{chatId}"
|
||||||
{#if eventToEdit}
|
messages={$chat?.messages || []}
|
||||||
<ComposeEdit clear={clearEventToEdit} />
|
canEdit={canEditEvent}
|
||||||
{/if}
|
disabled={Boolean(missingRelayLists.length)}
|
||||||
</div>
|
bind:this={composeArea} />
|
||||||
{#key initialValues}
|
|
||||||
<ChatCompose
|
|
||||||
bind:this={compose}
|
|
||||||
{onSubmit}
|
|
||||||
{onEscape}
|
|
||||||
{onEditPrevious}
|
|
||||||
{initialValues}
|
|
||||||
dictationKey={draftKey.key}
|
|
||||||
draftKey={eventToEdit ? undefined : draftKey}
|
|
||||||
disabled={Boolean(missingRelayLists.length)} />
|
|
||||||
{/key}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,189 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import {onDestroy, onMount} from "svelte"
|
|
||||||
import {writable} from "svelte/store"
|
|
||||||
import cx from "classnames"
|
|
||||||
import type {MaybeAsync} from "@welshman/lib"
|
|
||||||
import type {EventContent} from "@welshman/util"
|
|
||||||
import {escapeHtml, isMobile, preventDefault} from "@lib/html"
|
|
||||||
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
|
|
||||||
import Plane from "@assets/icons/plane-2.svg?dataurl"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
|
||||||
import DictationButton from "@app/components/DictationButton.svelte"
|
|
||||||
import EditorEmojiButton from "@app/components/EditorEmojiButton.svelte"
|
|
||||||
import EditorContent from "@app/editor/EditorContent.svelte"
|
|
||||||
import {makeEditor} from "@app/editor"
|
|
||||||
import {getDictation} from "@app/dictation"
|
|
||||||
import {type DraftKey, type Draft} from "@app/drafts"
|
|
||||||
import {pushToast} from "@app/toast"
|
|
||||||
import type {Share} from "@app/share"
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
disabled?: boolean
|
|
||||||
dictationKey: string
|
|
||||||
draftKey?: DraftKey<Draft>
|
|
||||||
onEscape?: () => void
|
|
||||||
onEditPrevious?: () => void
|
|
||||||
onSubmit: (event: EventContent) => MaybeAsync<void>
|
|
||||||
initialValues?: Share
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
initialValues,
|
|
||||||
disabled = false,
|
|
||||||
dictationKey,
|
|
||||||
draftKey,
|
|
||||||
onEscape,
|
|
||||||
onEditPrevious,
|
|
||||||
onSubmit,
|
|
||||||
}: Props = $props()
|
|
||||||
|
|
||||||
const autofocus = !isMobile && !disabled
|
|
||||||
|
|
||||||
const uploading = writable(false)
|
|
||||||
|
|
||||||
const editorClass = $derived(
|
|
||||||
cx("chat-editor grow overflow-hidden", {
|
|
||||||
"pointer-events-none opacity-50": disabled,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const focus = () => editor.then(ed => ed.chain().focus().run())
|
|
||||||
|
|
||||||
export const canEnterEditPrevious = () =>
|
|
||||||
editor.then(ed => ed.getText({blockSeparator: "\n"}) === "")
|
|
||||||
|
|
||||||
const handleKeyDown = async (event: KeyboardEvent) => {
|
|
||||||
if (event.key === "Escape") {
|
|
||||||
onEscape?.()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === "ArrowUp" && (await canEnterEditPrevious())) {
|
|
||||||
onEditPrevious?.()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadFiles = () => editor.then(ed => ed.chain().selectFiles().run())
|
|
||||||
|
|
||||||
// Tiptap parses a string handed to insertContent as html, so an angle bracket would eat the sentence.
|
|
||||||
const insertTranscript = async (text: string) => {
|
|
||||||
const ed = await editor
|
|
||||||
|
|
||||||
ed.chain().focus().insertContent(escapeHtml(text)).run()
|
|
||||||
}
|
|
||||||
|
|
||||||
const attachVoiceNote = async (audio: File) => {
|
|
||||||
const ed = await editor
|
|
||||||
|
|
||||||
ed.chain()
|
|
||||||
.addFile(audio, ed.state.selection.from + 1)
|
|
||||||
.run()
|
|
||||||
}
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
if ($uploading || disabled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const ed = await editor
|
|
||||||
const content = ed.getText({blockSeparator: "\n"}).trim()
|
|
||||||
const tags = ed.storage.nostr.getEditorTags()
|
|
||||||
|
|
||||||
if (!content) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await onSubmit({content, tags})
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to send message", error)
|
|
||||||
|
|
||||||
return pushToast({theme: "error", message: "Failed to send your message."})
|
|
||||||
}
|
|
||||||
|
|
||||||
draftKey?.clear()
|
|
||||||
ed.chain().clearContent(true).run()
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = $state(
|
|
||||||
initialValues?.type === "text" ? initialValues.value : (draftKey?.get()?.content ?? ""),
|
|
||||||
)
|
|
||||||
let dictating = $state(Boolean(getDictation(dictationKey)))
|
|
||||||
|
|
||||||
const onChange = (json: object) => {
|
|
||||||
content = json
|
|
||||||
}
|
|
||||||
|
|
||||||
const empty = writable(true)
|
|
||||||
|
|
||||||
const editor = makeEditor({
|
|
||||||
content,
|
|
||||||
empty,
|
|
||||||
submit,
|
|
||||||
uploading,
|
|
||||||
onChange,
|
|
||||||
aggressive: true,
|
|
||||||
encryptFiles: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if ($empty) {
|
|
||||||
draftKey?.clear()
|
|
||||||
} else {
|
|
||||||
draftKey?.set({content})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
const ed = await editor
|
|
||||||
ed.view.dom.addEventListener("keydown", handleKeyDown)
|
|
||||||
|
|
||||||
if (initialValues?.type === "file") {
|
|
||||||
ed.chain()
|
|
||||||
.addFile(initialValues.value, ed.state.selection.from + 1)
|
|
||||||
.run()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onDestroy(async () => {
|
|
||||||
const ed = await editor
|
|
||||||
ed?.view?.dom.removeEventListener("keydown", handleKeyDown)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<form class="relative z-feature flex gap-2 p-2" onsubmit={preventDefault(submit)}>
|
|
||||||
<Button
|
|
||||||
data-tip="Add an image"
|
|
||||||
class="button button-neutral button-square tip tip-right h-10 w-10 min-w-10 rounded-2xl transition-colors"
|
|
||||||
disabled={$uploading || disabled}
|
|
||||||
onclick={uploadFiles}>
|
|
||||||
{#if $uploading}
|
|
||||||
<Spinner size="xs" />
|
|
||||||
{:else}
|
|
||||||
<Icon icon={GallerySend} />
|
|
||||||
{/if}
|
|
||||||
</Button>
|
|
||||||
<EditorEmojiButton
|
|
||||||
{editor}
|
|
||||||
disabled={$uploading || disabled}
|
|
||||||
class="button button-neutral button-square tip tip-right h-10 w-10 min-w-10 rounded-2xl transition-colors" />
|
|
||||||
<div class={editorClass} aria-disabled={disabled}>
|
|
||||||
<EditorContent {autofocus} {editor} />
|
|
||||||
</div>
|
|
||||||
{#if dictating || ($empty && !disabled)}
|
|
||||||
<DictationButton
|
|
||||||
key={dictationKey}
|
|
||||||
bind:dictating
|
|
||||||
onTranscript={insertTranscript}
|
|
||||||
onVoiceNote={attachVoiceNote} />
|
|
||||||
{:else}
|
|
||||||
<Button
|
|
||||||
data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send"
|
|
||||||
class="button button-primary button-circle tip tip-left h-10 w-10 min-w-10"
|
|
||||||
disabled={$uploading || disabled}
|
|
||||||
onclick={submit}>
|
|
||||||
<Icon icon={Plane} />
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
</form>
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {onDestroy, onMount} from "svelte"
|
||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
|
import cx from "classnames"
|
||||||
import type {Maybe, MaybeAsync} from "@welshman/lib"
|
import type {Maybe, MaybeAsync} from "@welshman/lib"
|
||||||
import {MESSAGE} from "@welshman/util"
|
import {MESSAGE} from "@welshman/util"
|
||||||
import type {EventContent} from "@welshman/util"
|
import type {EventContent} from "@welshman/util"
|
||||||
|
|
@ -23,35 +25,52 @@
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
import {DraftKey, type Draft} from "@app/drafts"
|
import {DraftKey, type Draft} from "@app/drafts"
|
||||||
import type {Share} from "@app/share"
|
import type {Share} from "@app/share"
|
||||||
import {onDestroy, onMount} from "svelte"
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url?: string
|
url?: string
|
||||||
h?: string
|
h?: string
|
||||||
|
draftKey: string
|
||||||
|
encryptFiles?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
initialValues?: Share
|
||||||
onEscape?: () => void
|
onEscape?: () => void
|
||||||
onEditPrevious?: () => void
|
onEditPrevious?: () => void
|
||||||
onSubmit: (event: EventContent) => MaybeAsync<void>
|
onSubmit: (event: EventContent) => MaybeAsync<void>
|
||||||
initialValues?: Share
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url, h, initialValues, onEscape, onEditPrevious, onSubmit}: Props = $props()
|
const {
|
||||||
|
url,
|
||||||
|
h,
|
||||||
|
draftKey,
|
||||||
|
encryptFiles = false,
|
||||||
|
disabled = false,
|
||||||
|
initialValues,
|
||||||
|
onEscape,
|
||||||
|
onEditPrevious,
|
||||||
|
onSubmit,
|
||||||
|
}: Props = $props()
|
||||||
|
|
||||||
const key = `room:${url ?? ""}:${h ?? ""}`
|
const draft = initialValues ? undefined : new DraftKey<Draft>(draftKey)
|
||||||
|
|
||||||
const draftKey = (url || h) && !initialValues ? new DraftKey<Draft>(key) : undefined
|
const autofocus = !isMobile && !disabled
|
||||||
|
|
||||||
const autofocus = !isMobile
|
|
||||||
|
|
||||||
const uploading = writable(false)
|
const uploading = writable(false)
|
||||||
|
|
||||||
const text = writable("")
|
const text = writable("")
|
||||||
|
|
||||||
const commandTarget = {
|
const commandTarget = url
|
||||||
url,
|
? {url, kind: MESSAGE, pubkey: $app.user?.pubkey, tags: h ? [["h", h]] : []}
|
||||||
kind: MESSAGE,
|
: undefined
|
||||||
pubkey: $app.user?.pubkey,
|
|
||||||
tags: h ? [["h", h]] : [],
|
const buttonClass = url
|
||||||
}
|
? "join-item h-10 w-10 min-w-10 button button-neutral"
|
||||||
|
: "button button-neutral button-square tip tip-right h-10 w-10 min-w-10 rounded-2xl transition-colors"
|
||||||
|
|
||||||
|
const editorClass = $derived(
|
||||||
|
cx("chat-editor grow overflow-hidden", {
|
||||||
|
"pointer-events-none opacity-50": disabled,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
export const focus = () => editor.then(ed => ed.chain().focus().run())
|
export const focus = () => editor.then(ed => ed.chain().focus().run())
|
||||||
|
|
||||||
|
|
@ -96,12 +115,12 @@
|
||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
const showPopover = () => tippy?.show()
|
const showMenu = () => tippy?.show()
|
||||||
|
|
||||||
const hidePopover = () => tippy?.hide()
|
const hideMenu = () => tippy?.hide()
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if ($uploading || sending) {
|
if ($uploading || disabled || sending) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,16 +140,16 @@
|
||||||
sending = false
|
sending = false
|
||||||
}
|
}
|
||||||
|
|
||||||
draftKey?.clear()
|
draft?.clear()
|
||||||
ed.chain().clearContent(true).run()
|
ed.chain().clearContent(true).run()
|
||||||
}
|
}
|
||||||
|
|
||||||
let sending = false
|
let sending = false
|
||||||
let tippy: Maybe<TippyController> = $state()
|
let tippy: Maybe<TippyController> = $state()
|
||||||
let content = $state(
|
let content = $state(
|
||||||
initialValues?.type === "text" ? initialValues.value : (draftKey?.get()?.content ?? ""),
|
initialValues?.type === "text" ? initialValues.value : (draft?.get()?.content ?? ""),
|
||||||
)
|
)
|
||||||
let dictating = $state(Boolean(getDictation(key)))
|
let dictating = $state(Boolean(getDictation(draftKey)))
|
||||||
|
|
||||||
const onChange = (json: object) => {
|
const onChange = (json: object) => {
|
||||||
content = json
|
content = json
|
||||||
|
|
@ -147,14 +166,15 @@
|
||||||
uploading,
|
uploading,
|
||||||
onChange,
|
onChange,
|
||||||
commandTarget,
|
commandTarget,
|
||||||
|
encryptFiles,
|
||||||
aggressive: true,
|
aggressive: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if ($empty) {
|
if ($empty) {
|
||||||
draftKey?.clear()
|
draft?.clear()
|
||||||
} else {
|
} else {
|
||||||
draftKey?.set({content})
|
draft?.set({content})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -175,12 +195,15 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CommandArgBar target={commandTarget} content={$text} insert={insertCommandToken} />
|
{#if commandTarget}
|
||||||
|
<CommandArgBar target={commandTarget} content={$text} insert={insertCommandToken} />
|
||||||
|
{/if}
|
||||||
<form class="relative flex gap-2 py-2" onsubmit={preventDefault(submit)}>
|
<form class="relative flex gap-2 py-2" onsubmit={preventDefault(submit)}>
|
||||||
<div class="join">
|
<div class={cx({join: url, "flex gap-2": !url})}>
|
||||||
<Button
|
<Button
|
||||||
class="join-item h-10 w-10 min-w-10 button button-neutral"
|
data-tip="Add an image"
|
||||||
disabled={$uploading}
|
class={buttonClass}
|
||||||
|
disabled={$uploading || disabled}
|
||||||
onclick={uploadFiles}>
|
onclick={uploadFiles}>
|
||||||
{#if $uploading}
|
{#if $uploading}
|
||||||
<Spinner size="xs" />
|
<Spinner size="xs" />
|
||||||
|
|
@ -188,29 +211,25 @@
|
||||||
<Icon icon={GallerySend} />
|
<Icon icon={GallerySend} />
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
{#if url}
|
||||||
disabled={$uploading}
|
<Button disabled={$uploading} onclick={showMenu} class={buttonClass}>
|
||||||
onclick={showPopover}
|
<Tippy
|
||||||
class="join-item h-10 w-10 min-w-10 button button-neutral">
|
bind:controller={tippy}
|
||||||
<Tippy
|
component={ComposeMenu}
|
||||||
bind:controller={tippy}
|
props={{url, h, onClick: hideMenu}}
|
||||||
component={ComposeMenu}
|
params={{trigger: "manual", interactive: true}}>
|
||||||
props={{url, h, onClick: hidePopover}}
|
<Icon icon={WidgetAdd} />
|
||||||
params={{trigger: "manual", interactive: true}}>
|
</Tippy>
|
||||||
<Icon icon={WidgetAdd} />
|
</Button>
|
||||||
</Tippy>
|
{/if}
|
||||||
</Button>
|
<EditorEmojiButton {editor} disabled={$uploading || disabled} class={buttonClass} />
|
||||||
<EditorEmojiButton
|
|
||||||
{editor}
|
|
||||||
disabled={$uploading}
|
|
||||||
class="join-item h-10 w-10 min-w-10 button button-neutral" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-editor grow overflow-hidden">
|
<div class={editorClass} aria-disabled={disabled}>
|
||||||
<EditorContent {autofocus} {editor} />
|
<EditorContent {autofocus} {editor} />
|
||||||
</div>
|
</div>
|
||||||
{#if dictating || $empty}
|
{#if dictating || ($empty && !disabled)}
|
||||||
<DictationButton
|
<DictationButton
|
||||||
{key}
|
key={draftKey}
|
||||||
bind:dictating
|
bind:dictating
|
||||||
onTranscript={insertTranscript}
|
onTranscript={insertTranscript}
|
||||||
onVoiceNote={attachVoiceNote} />
|
onVoiceNote={attachVoiceNote} />
|
||||||
|
|
@ -218,7 +237,7 @@
|
||||||
<Button
|
<Button
|
||||||
data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send"
|
data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send"
|
||||||
class="button button-primary button-circle flex justify-center items-center tip tip-left h-10 w-10 min-w-10"
|
class="button button-primary button-circle flex justify-center items-center tip tip-left h-10 w-10 min-w-10"
|
||||||
disabled={$uploading}
|
disabled={$uploading || disabled}
|
||||||
onclick={submit}>
|
onclick={submit}>
|
||||||
<Icon icon={Plane} />
|
<Icon icon={Plane} />
|
||||||
</Button>
|
</Button>
|
||||||
122
src/app/components/ComposeArea.svelte
Normal file
122
src/app/components/ComposeArea.svelte
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
<script module lang="ts">
|
||||||
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
|
|
||||||
|
export type ComposeValues = EventContent & {
|
||||||
|
parent?: TrustedEvent
|
||||||
|
share?: TrustedEvent
|
||||||
|
edit?: TrustedEvent
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type {ComponentProps} from "svelte"
|
||||||
|
import {page} from "$app/stores"
|
||||||
|
import {ifLet} from "@welshman/lib"
|
||||||
|
import type {Maybe} from "@welshman/lib"
|
||||||
|
import Compose from "@app/components/Compose.svelte"
|
||||||
|
import ComposeEdit from "@app/components/ComposeEdit.svelte"
|
||||||
|
import ComposeParent from "@app/components/ComposeParent.svelte"
|
||||||
|
import {pendingShare, type Share} from "@app/share"
|
||||||
|
|
||||||
|
type Props = Pick<
|
||||||
|
ComponentProps<typeof Compose>,
|
||||||
|
"url" | "h" | "draftKey" | "encryptFiles" | "disabled"
|
||||||
|
> & {
|
||||||
|
messages: TrustedEvent[]
|
||||||
|
canEdit: (event: TrustedEvent) => boolean
|
||||||
|
onSubmit: (values: ComposeValues) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
const {messages, canEdit, onSubmit, ...props}: Props = $props()
|
||||||
|
|
||||||
|
export const reply = (event: TrustedEvent) => {
|
||||||
|
parent = event
|
||||||
|
compose?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const edit = (event: TrustedEvent) => {
|
||||||
|
clearParent()
|
||||||
|
clearShare()
|
||||||
|
eventToEdit = event
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearParent = () => {
|
||||||
|
parent = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearShare = () => {
|
||||||
|
share = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearEventToEdit = () => {
|
||||||
|
eventToEdit = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEscape = () => {
|
||||||
|
clearParent()
|
||||||
|
clearShare()
|
||||||
|
clearEventToEdit()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEditPrevious = () => ifLet(messages.find(canEdit), edit)
|
||||||
|
|
||||||
|
const submit = async ({content, tags}: EventContent) => {
|
||||||
|
// An edit that changes nothing is just a dismissal
|
||||||
|
if (eventToEdit?.content === content) {
|
||||||
|
clearEventToEdit()
|
||||||
|
} else if (content || sharedEvent) {
|
||||||
|
await onSubmit({content, tags, parent, share: sharedEvent, edit: eventToEdit})
|
||||||
|
|
||||||
|
// Only once it's sent, so a failure leaves the reply or edit in place to try again.
|
||||||
|
clearParent()
|
||||||
|
clearShare()
|
||||||
|
clearEventToEdit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let compose: Compose | undefined = $state()
|
||||||
|
let parent: Maybe<TrustedEvent> = $state()
|
||||||
|
let share: Maybe<Share> = $state()
|
||||||
|
let eventToEdit: Maybe<TrustedEvent> = $state()
|
||||||
|
|
||||||
|
const sharedEvent = $derived(share?.type === "event" ? share.value : undefined)
|
||||||
|
|
||||||
|
const initialValues = $derived.by((): Maybe<Share> => {
|
||||||
|
if (eventToEdit) {
|
||||||
|
return {type: "text", value: eventToEdit.content}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (share) {
|
||||||
|
return share
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Only the conversation a share was sent to takes it, including the one already on screen.
|
||||||
|
$effect(() => {
|
||||||
|
if ($pendingShare?.path === $page.url.pathname) {
|
||||||
|
share = $pendingShare.share
|
||||||
|
pendingShare.set(undefined)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-px">
|
||||||
|
{#if parent}
|
||||||
|
<ComposeParent url={props.url} event={parent} clear={clearParent} verb="Replying to" />
|
||||||
|
{/if}
|
||||||
|
{#if sharedEvent}
|
||||||
|
<ComposeParent url={props.url} event={sharedEvent} clear={clearShare} verb="Sharing" />
|
||||||
|
{/if}
|
||||||
|
{#if eventToEdit}
|
||||||
|
<ComposeEdit clear={clearEventToEdit} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#key initialValues}
|
||||||
|
<Compose
|
||||||
|
{...props}
|
||||||
|
{initialValues}
|
||||||
|
{onEscape}
|
||||||
|
{onEditPrevious}
|
||||||
|
onSubmit={submit}
|
||||||
|
bind:this={compose} />
|
||||||
|
{/key}
|
||||||
|
|
@ -6,9 +6,9 @@
|
||||||
import type {Readable} from "svelte/store"
|
import type {Readable} from "svelte/store"
|
||||||
import {debounce} from "throttle-debounce"
|
import {debounce} from "throttle-debounce"
|
||||||
import cx from "classnames"
|
import cx from "classnames"
|
||||||
import {now, ifLet, ago, spec, MINUTE} from "@welshman/lib"
|
import {now, ago, spec, MINUTE} from "@welshman/lib"
|
||||||
import type {Maybe} from "@welshman/lib"
|
import type {Maybe} from "@welshman/lib"
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {stamp, MESSAGE, RELAY_ADD_MEMBER, ROOM_ADD_MEMBER} from "@welshman/util"
|
import {stamp, MESSAGE, RELAY_ADD_MEMBER, ROOM_ADD_MEMBER} from "@welshman/util"
|
||||||
import {Message} from "@welshman/domain"
|
import {Message} from "@welshman/domain"
|
||||||
import {MembershipStatus} from "@welshman/app"
|
import {MembershipStatus} from "@welshman/app"
|
||||||
|
|
@ -24,9 +24,7 @@
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import VirtualList from "@lib/components/VirtualList.svelte"
|
import VirtualList from "@lib/components/VirtualList.svelte"
|
||||||
import type {VirtualListController} from "@lib/components/VirtualList.svelte"
|
import type {VirtualListController} from "@lib/components/VirtualList.svelte"
|
||||||
import RoomCompose from "@app/components/RoomCompose.svelte"
|
import ComposeArea, {type ComposeValues} from "@app/components/ComposeArea.svelte"
|
||||||
import ComposeEdit from "@app/components/ComposeEdit.svelte"
|
|
||||||
import ComposeParent from "@app/components/ComposeParent.svelte"
|
|
||||||
import RoomItem from "@app/components/RoomItem.svelte"
|
import RoomItem from "@app/components/RoomItem.svelte"
|
||||||
import RoomItemAddMember from "@app/components/RoomItemAddMember.svelte"
|
import RoomItemAddMember from "@app/components/RoomItemAddMember.svelte"
|
||||||
import RoomPinnedMessages from "@app/components/RoomPinnedMessages.svelte"
|
import RoomPinnedMessages from "@app/components/RoomPinnedMessages.svelte"
|
||||||
|
|
@ -49,7 +47,6 @@
|
||||||
import {pageLoading} from "@app/loading"
|
import {pageLoading} from "@app/loading"
|
||||||
import {checked, deferredRoomPath, setChecked} from "@app/notifications"
|
import {checked, deferredRoomPath, setChecked} from "@app/notifications"
|
||||||
import {getPermalinkTarget, highlightedEvent, makeRoomPath} from "@app/routes"
|
import {getPermalinkTarget, highlightedEvent, makeRoomPath} from "@app/routes"
|
||||||
import {pendingShare, type Share} from "@app/share"
|
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -153,38 +150,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const replyTo = (event: TrustedEvent) => {
|
const replyTo = (event: TrustedEvent) => composeArea?.reply(event)
|
||||||
parent = event
|
|
||||||
compose?.focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearParent = () => {
|
|
||||||
parent = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearShare = () => {
|
|
||||||
share = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearEventToEdit = () => {
|
|
||||||
eventToEdit = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSubmit = async ({content, tags}: EventContent) => {
|
|
||||||
if (!content && !sharedEvent) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventToEdit) {
|
|
||||||
// Don't do anything if message hasn't changed
|
|
||||||
if (eventToEdit.content === content) {
|
|
||||||
return clearEventToEdit()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const onSubmit = async ({content, tags, parent, share, edit}: ComposeValues) => {
|
||||||
|
if (edit) {
|
||||||
const protect = await shouldProtect
|
const protect = await shouldProtect
|
||||||
|
|
||||||
// Delete the previous message, to be republished below with the same timestamp
|
// Delete the previous message, to be republished below with the same timestamp
|
||||||
const command = await $deletes.deleteEvent($state.snapshot(eventToEdit), w =>
|
const command = await $deletes.deleteEvent($state.snapshot(edit), w =>
|
||||||
w.setProtected(protect),
|
w.setProtected(protect),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -192,8 +165,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// A share is a quote rather than a reply, so setParent prepends the reply's own reference ahead of it.
|
// A share is a quote rather than a reply, so setParent prepends the reply's own reference ahead of it.
|
||||||
if (sharedEvent) {
|
if (share) {
|
||||||
;({content, tags} = await prependParent(sharedEvent, {content, tags}, url))
|
;({content, tags} = await prependParent(share, {content, tags}, url))
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventWriter = await toSpace(
|
const eventWriter = await toSpace(
|
||||||
|
|
@ -212,7 +185,7 @@
|
||||||
|
|
||||||
const thunk = $thunks.publish({
|
const thunk = $thunks.publish({
|
||||||
relays: [url],
|
relays: [url],
|
||||||
event: stamp(await eventWriter.renderTemplate(), eventToEdit?.created_at),
|
event: stamp(await eventWriter.renderTemplate(), edit?.created_at),
|
||||||
delay,
|
delay,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -225,11 +198,6 @@
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only once it's sent, so a failure leaves the reply or edit in place to try again.
|
|
||||||
clearParent()
|
|
||||||
clearShare()
|
|
||||||
clearEventToEdit()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getElementKey = (element: {id: string}) => element.id
|
const getElementKey = (element: {id: string}) => element.id
|
||||||
|
|
@ -382,8 +350,6 @@
|
||||||
let virtualList: Maybe<VirtualListController> = $state()
|
let virtualList: Maybe<VirtualListController> = $state()
|
||||||
let older: Maybe<ReturnType<typeof makeScrollLoader>> = $state()
|
let older: Maybe<ReturnType<typeof makeScrollLoader>> = $state()
|
||||||
let newer: Maybe<ReturnType<typeof makeScrollLoader>> = $state()
|
let newer: Maybe<ReturnType<typeof makeScrollLoader>> = $state()
|
||||||
let share: Maybe<Share> = $state()
|
|
||||||
let parent: TrustedEvent | undefined = $state()
|
|
||||||
let element: HTMLElement | undefined = $state()
|
let element: HTMLElement | undefined = $state()
|
||||||
let lastVisibleAt = now()
|
let lastVisibleAt = now()
|
||||||
let newMessagesAfter = $state($checked[$page.url.pathname])
|
let newMessagesAfter = $state($checked[$page.url.pathname])
|
||||||
|
|
@ -393,8 +359,7 @@
|
||||||
let scrolledUp = $state(false)
|
let scrolledUp = $state(false)
|
||||||
let cleanup: () => void
|
let cleanup: () => void
|
||||||
let events: Readable<TrustedEvent[]> = $state(readable([]))
|
let events: Readable<TrustedEvent[]> = $state(readable([]))
|
||||||
let compose: RoomCompose | undefined = $state()
|
let composeArea: ComposeArea | undefined = $state()
|
||||||
let eventToEdit: TrustedEvent | undefined = $state()
|
|
||||||
|
|
||||||
// A link into history renders the newest messages first and only then scrolls.
|
// A link into history renders the newest messages first and only then scrolls.
|
||||||
const awaitingJump = $derived(!isNaN(at) && !jumpSettled)
|
const awaitingJump = $derived(!isNaN(at) && !jumpSettled)
|
||||||
|
|
@ -408,26 +373,6 @@
|
||||||
return () => pageLoading.set(false)
|
return () => pageLoading.set(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Sharing into the room already on screen doesn't re-create this component.
|
|
||||||
$effect(() => {
|
|
||||||
if ($pendingShare) {
|
|
||||||
share = $pendingShare
|
|
||||||
pendingShare.set(undefined)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const sharedEvent = $derived(share?.type === "event" ? share.value : undefined)
|
|
||||||
|
|
||||||
const initialValues = $derived.by((): Share | undefined => {
|
|
||||||
if (eventToEdit) {
|
|
||||||
return {type: "text", value: eventToEdit.content}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (share) {
|
|
||||||
return share
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const clearIsUserScrolling = debounce(150, () => {
|
const clearIsUserScrolling = debounce(150, () => {
|
||||||
isUserScrolling = false
|
isUserScrolling = false
|
||||||
})
|
})
|
||||||
|
|
@ -519,22 +464,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onEscape = () => {
|
|
||||||
clearParent()
|
|
||||||
clearShare()
|
|
||||||
eventToEdit = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const canEditEvent = (event: TrustedEvent) =>
|
const canEditEvent = (event: TrustedEvent) =>
|
||||||
event.pubkey === $user.pubkey && event.created_at >= ago(5, MINUTE)
|
event.pubkey === $user.pubkey && event.created_at >= ago(5, MINUTE)
|
||||||
|
|
||||||
const onEditEvent = (event: TrustedEvent) => {
|
const onEditEvent = (event: TrustedEvent) => composeArea?.edit(event)
|
||||||
clearParent()
|
|
||||||
clearShare()
|
|
||||||
eventToEdit = event
|
|
||||||
}
|
|
||||||
|
|
||||||
const onEditPrevious = () => ifLet($events.toReversed().find(canEditEvent), onEditEvent)
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Defer rendering until navigation finishes
|
// Defer rendering until navigation finishes
|
||||||
|
|
@ -711,27 +644,14 @@
|
||||||
{@render membershipButton("Ask to Join")}
|
{@render membershipButton("Ask to Join")}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col gap-px">
|
<ComposeArea
|
||||||
{#if parent}
|
{url}
|
||||||
<ComposeParent {url} event={parent} clear={clearParent} verb="Replying to" />
|
{h}
|
||||||
{/if}
|
{onSubmit}
|
||||||
{#if sharedEvent}
|
draftKey="room:{url}:{h ?? ''}"
|
||||||
<ComposeParent {url} event={sharedEvent} clear={clearShare} verb="Sharing" />
|
messages={$events.toReversed()}
|
||||||
{/if}
|
canEdit={canEditEvent}
|
||||||
{#if eventToEdit}
|
bind:this={composeArea} />
|
||||||
<ComposeEdit clear={clearEventToEdit} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#key initialValues}
|
|
||||||
<RoomCompose
|
|
||||||
{url}
|
|
||||||
{h}
|
|
||||||
{onSubmit}
|
|
||||||
{onEscape}
|
|
||||||
{onEditPrevious}
|
|
||||||
{initialValues}
|
|
||||||
bind:this={compose} />
|
|
||||||
{/key}
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if h && isVoiceRoom && !voiceConnectedHere}
|
{#if h && isVoiceRoom && !voiceConnectedHere}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export type Share =
|
||||||
| {type: "text"; value: string}
|
| {type: "text"; value: string}
|
||||||
| {type: "file"; value: File}
|
| {type: "file"; value: File}
|
||||||
|
|
||||||
export const pendingShare = writable<Maybe<Share>>(undefined)
|
export const pendingShare = writable<Maybe<{path: string; share: Share}>>(undefined)
|
||||||
|
|
||||||
// Set pendingShare after navigating so the current view doesn't pop it
|
// Set pendingShare after navigating so the current view doesn't pop it
|
||||||
export const shareTo = async (
|
export const shareTo = async (
|
||||||
|
|
@ -26,7 +26,7 @@ export const shareTo = async (
|
||||||
) => {
|
) => {
|
||||||
await navigate(path, options)
|
await navigate(path, options)
|
||||||
|
|
||||||
pendingShare.set(share)
|
pendingShare.set({path, share})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const shareEvent = (url: string, noun: string, event: TrustedEvent) => {
|
export const shareEvent = (url: string, noun: string, event: TrustedEvent) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue