mirror of
https://github.com/coracle-social/flotilla.git
synced 2025-12-10 10:57:04 +00:00
Allow editing previous messages in channel chat
This commit is contained in:
@@ -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)
|
||||
})
|
||||
</script>
|
||||
|
||||
<form class="relative z-feature flex gap-2 p-2" onsubmit={preventDefault(submit)}>
|
||||
|
||||
21
src/app/components/ChannelComposeEdit.svelte
Normal file
21
src/app/components/ChannelComposeEdit.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import {slide} from "@lib/transition"
|
||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
|
||||
const {
|
||||
clear,
|
||||
}: {
|
||||
clear: () => void
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8 text-xs"
|
||||
transition:slide>
|
||||
<p class="text-primary">Editing message</p>
|
||||
<Button class="absolute right-2 top-2 cursor-pointer" onclick={clear}>
|
||||
<Icon icon={CloseCircle} />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -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 @@
|
||||
<Icon icon={Reply} size={4} />
|
||||
</Button>
|
||||
{/if}
|
||||
{#if edit}
|
||||
<Button class="btn join-item btn-xs" onclick={edit}>
|
||||
<Icon icon={Pen} size={4} />
|
||||
</Button>
|
||||
{/if}
|
||||
<ChannelMessageMenuButton {url} {event} />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -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<typeof $page.params>
|
||||
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<TrustedEvent[]> = $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} />
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
@@ -446,8 +479,18 @@
|
||||
{#if share}
|
||||
<ChannelComposeParent event={share} clear={clearShare} verb="Sharing" />
|
||||
{/if}
|
||||
{#if eventToEdit}
|
||||
<ChannelComposeEdit clear={clearEventToEdit} />
|
||||
{/if}
|
||||
</div>
|
||||
<ChannelCompose bind:this={compose} {onSubmit} {url} />
|
||||
{#key eventToEdit}
|
||||
<ChannelCompose
|
||||
bind:this={compose}
|
||||
content={eventToEdit?.content}
|
||||
{onSubmit}
|
||||
{url}
|
||||
{onEditPrevious} />
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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<TrustedEvent[]> = $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} />
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
@@ -282,8 +314,18 @@
|
||||
{#if share}
|
||||
<ChannelComposeParent event={share} clear={clearShare} verb="Sharing" />
|
||||
{/if}
|
||||
{#if eventToEdit}
|
||||
<ChannelComposeEdit clear={clearEventToEdit} />
|
||||
{/if}
|
||||
</div>
|
||||
<ChannelCompose bind:this={compose} {onSubmit} {url} />
|
||||
{#key eventToEdit}
|
||||
<ChannelCompose
|
||||
bind:this={compose}
|
||||
content={eventToEdit?.content}
|
||||
{onSubmit}
|
||||
{url}
|
||||
{onEditPrevious} />
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
{#if showScrollButton}
|
||||
|
||||
Reference in New Issue
Block a user