diff --git a/src/app/components/ChannelCompose.svelte b/src/app/components/ChannelCompose.svelte index cf5ac22..8762d6b 100644 --- a/src/app/components/ChannelCompose.svelte +++ b/src/app/components/ChannelCompose.svelte @@ -8,13 +8,16 @@ import Button from "@lib/components/Button.svelte" import EditorContent from "@app/editor/EditorContent.svelte" import {makeEditor} from "@app/editor" + import {onDestroy, onMount} from "svelte" type Props = { url?: string + content?: string + onEditPrevious?: () => void onSubmit: (event: EventContent) => void } - const {onSubmit, url}: Props = $props() + const {content, onEditPrevious, onSubmit, url}: Props = $props() const autofocus = !isMobile @@ -22,6 +25,15 @@ 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 === "ArrowUp" && (await canEnterEditPrevious())) { + onEditPrevious?.() + } + } + const uploadFiles = () => editor.then(ed => ed.chain().selectFiles().run()) const submit = async () => { @@ -38,7 +50,17 @@ ed.chain().clearContent().run() } - const editor = makeEditor({url, autofocus, submit, uploading, aggressive: true}) + const editor = makeEditor({url, autofocus, content, submit, uploading, aggressive: true}) + + onMount(async () => { + const ed = await editor + ed.view.dom.addEventListener("keydown", handleKeyDown) + }) + + onDestroy(async () => { + const ed = await editor + ed?.view?.dom.removeEventListener("keydown", handleKeyDown) + })
diff --git a/src/app/components/ChannelComposeEdit.svelte b/src/app/components/ChannelComposeEdit.svelte new file mode 100644 index 0000000..5949cf5 --- /dev/null +++ b/src/app/components/ChannelComposeEdit.svelte @@ -0,0 +1,21 @@ + + +
+

Editing message

+ +
diff --git a/src/app/components/ChannelMessage.svelte b/src/app/components/ChannelMessage.svelte index aa8e02e..356af20 100644 --- a/src/app/components/ChannelMessage.svelte +++ b/src/app/components/ChannelMessage.svelte @@ -6,6 +6,7 @@ import TapTarget from "@lib/components/TapTarget.svelte" import Avatar from "@lib/components/Avatar.svelte" import Reply from "@assets/icons/reply-2.svg?dataurl" + import Pen from "@assets/icons/pen.svg?dataurl" import Icon from "@lib/components/Icon.svelte" import Button from "@lib/components/Button.svelte" import Content from "@app/components/Content.svelte" @@ -26,9 +27,19 @@ replyTo?: (event: TrustedEvent) => void showPubkey?: boolean inert?: boolean + canEdit: (event: TrustedEvent) => boolean + onEdit: (event: TrustedEvent) => void } - const {url, event, replyTo = undefined, showPubkey = false, inert = false}: Props = $props() + const { + url, + event, + replyTo = undefined, + showPubkey = false, + inert = false, + canEdit, + onEdit, + }: Props = $props() const thunk = $thunks[event.id] const shouldProtect = canEnforceNip70(url) @@ -38,8 +49,9 @@ const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length] const reply = () => replyTo!(event) + const edit = canEdit(event) ? () => onEdit(event) : undefined - const onTap = () => pushModal(ChannelMessageMenuMobile, {url, event, reply}) + const onTap = () => pushModal(ChannelMessageMenuMobile, {url, event, reply, edit}) const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey, url}) @@ -107,6 +119,11 @@ {/if} + {#if edit} + + {/if} {/if} diff --git a/src/routes/spaces/[relay]/[room]/+page.svelte b/src/routes/spaces/[relay]/[room]/+page.svelte index 7990fdf..4ab42d7 100644 --- a/src/routes/spaces/[relay]/[room]/+page.svelte +++ b/src/routes/spaces/[relay]/[room]/+page.svelte @@ -4,8 +4,8 @@ import {onMount, onDestroy} from "svelte" import {page} from "$app/stores" import type {Readable} from "svelte/store" - import {now, formatTimestampAsDate} from "@welshman/lib" import type {MakeNonOptional} from "@welshman/lib" + import {now, formatTimestampAsDate, ago, MINUTE} from "@welshman/lib" import {request} from "@welshman/net" import type {TrustedEvent, EventContent} from "@welshman/util" import { @@ -52,11 +52,13 @@ canEnforceNip70, removeRoomMembership, prependParent, + publishDelete, } from "@app/core/commands" import {PROTECTED} from "@app/core/state" import {makeFeed} from "@app/core/requests" import {popKey} from "@lib/implicit" import {pushToast} from "@app/util/toast" + import ChannelComposeEdit from "@src/app/components/ChannelComposeEdit.svelte" const {room, relay} = $page.params as MakeNonOptional const mounted = now() @@ -115,6 +117,10 @@ share = undefined } + const clearEventToEdit = () => { + eventToEdit = undefined + } + const onSubmit = async ({content, tags}: EventContent) => { tags.push(["h", room]) @@ -122,7 +128,13 @@ tags.push(PROTECTED) } - let template = {content, tags} + let template: EventContent & {created_at?: number} = {content, tags} + + if (eventToEdit) { + // Delete previous message, to be republished with same timestamp + template.created_at = eventToEdit.created_at + publishDelete({relays: [url], event: eventToEdit, protect: await shouldProtect}) + } if (share) { template = prependParent(share, template) @@ -150,6 +162,7 @@ clearParent() clearShare() + clearEventToEdit() } const onScroll = () => { @@ -188,6 +201,7 @@ let cleanup: () => void let events: Readable = $state(readable([])) let compose: ChannelCompose | undefined = $state() + let eventToEdit: TrustedEvent | undefined = $state() const elements = $derived.by(() => { const elements = [] @@ -266,6 +280,23 @@ cleanup = feed.cleanup } + const canEditEvent = (event: TrustedEvent) => + event.pubkey === $pubkey && event.created_at >= ago(5, MINUTE) + + const onEditEvent = (event: TrustedEvent) => { + clearParent() + clearShare() + eventToEdit = event + } + + const onEditPrevious = () => { + const prev = $events.find(e => e.pubkey === $pubkey) + + if (prev && canEditEvent(prev)) { + onEditEvent(prev) + } + } + onMount(() => { const controller = new AbortController() @@ -402,7 +433,9 @@ {url} {replyTo} event={$state.snapshot(value as TrustedEvent)} - {showPubkey} /> + {showPubkey} + canEdit={canEditEvent} + onEdit={onEditEvent} /> {/if} {/each} @@ -446,8 +479,18 @@ {#if share} {/if} + {#if eventToEdit} + + {/if} - + {#key eventToEdit} + + {/key} {/if} diff --git a/src/routes/spaces/[relay]/chat/+page.svelte b/src/routes/spaces/[relay]/chat/+page.svelte index c070b6a..b4c7c77 100644 --- a/src/routes/spaces/[relay]/chat/+page.svelte +++ b/src/routes/spaces/[relay]/chat/+page.svelte @@ -3,7 +3,7 @@ import {page} from "$app/stores" import type {Readable} from "svelte/store" import {readable} from "svelte/store" - import {now, formatTimestampAsDate} from "@welshman/lib" + import {now, formatTimestampAsDate, MINUTE, ago} from "@welshman/lib" import type {TrustedEvent, EventContent} from "@welshman/util" import {makeEvent, getTag, MESSAGE, DELETE} from "@welshman/util" import {pubkey, publishThunk} from "@welshman/app" @@ -28,11 +28,12 @@ PROTECTED, REACTION_KINDS, } from "@app/core/state" - import {prependParent, canEnforceNip70} from "@app/core/commands" + import {prependParent, canEnforceNip70, publishDelete} from "@app/core/commands" import {setChecked, checked} from "@app/util/notifications" import {pushToast} from "@app/util/toast" import {makeFeed} from "@app/core/requests" import {popKey} from "@lib/implicit" + import ChannelComposeEdit from "@src/app/components/ChannelComposeEdit.svelte" const mounted = now() const lastChecked = $checked[$page.url.pathname] @@ -49,17 +50,27 @@ parent = undefined } + const clearEventToEdit = () => { + eventToEdit = undefined + } + const clearShare = () => { share = undefined } const onSubmit = async ({content, tags}: EventContent) => { + let template: EventContent & {created_at?: number} = {content, tags} + + if (eventToEdit) { + // Delete previous message, to be republished with same timestamp + template.created_at = eventToEdit.created_at + publishDelete({relays: [url], event: eventToEdit, protect: await shouldProtect}) + } + if (await shouldProtect) { tags.push(PROTECTED) } - let template = {content, tags} - if (share) { template = prependParent(share, template) } @@ -86,6 +97,7 @@ clearParent() clearShare() + clearEventToEdit() } const onScroll = () => { @@ -122,6 +134,7 @@ let cleanup: () => void let events: Readable = $state(readable([])) let compose: ChannelCompose | undefined = $state() + let eventToEdit: TrustedEvent | undefined = $state() const elements = $derived.by(() => { const elements = [] @@ -184,6 +197,23 @@ return elements }) + const canEditEvent = (event: TrustedEvent) => + event.pubkey === $pubkey && event.created_at >= ago(5, MINUTE) + + const onEditEvent = (event: TrustedEvent) => { + clearParent() + clearShare() + eventToEdit = event + } + + const onEditPrevious = () => { + const prev = $events.find(e => e.pubkey === $pubkey) + + if (prev && canEditEvent(prev)) { + onEditEvent(prev) + } + } + onMount(() => { const controller = new AbortController() @@ -261,7 +291,9 @@ {url} {replyTo} event={$state.snapshot(value as TrustedEvent)} - {showPubkey} /> + {showPubkey} + canEdit={canEditEvent} + onEdit={onEditEvent} /> {/if} {/each} @@ -282,8 +314,18 @@ {#if share} {/if} + {#if eventToEdit} + + {/if} - + {#key eventToEdit} + + {/key} {#if showScrollButton}