Add up/edit to chats
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
# Current
|
||||||
|
|
||||||
|
* Enable email/password login
|
||||||
|
* Add up/edit to direct messages
|
||||||
|
|
||||||
# 1.6.5
|
# 1.6.5
|
||||||
|
|
||||||
* Attempt to fix permission grant for notifications
|
* Attempt to fix permission grant for notifications
|
||||||
|
|||||||
+122
-65
@@ -2,9 +2,11 @@
|
|||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {
|
import {
|
||||||
|
ago,
|
||||||
int,
|
int,
|
||||||
ms,
|
ms,
|
||||||
partition,
|
partition,
|
||||||
|
ifLet,
|
||||||
spec,
|
spec,
|
||||||
nthEq,
|
nthEq,
|
||||||
nthNe,
|
nthNe,
|
||||||
@@ -46,11 +48,12 @@
|
|||||||
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 ChatCompose from "@app/components/ChatCompose.svelte"
|
||||||
|
import ChatComposeEdit from "@app/components/ChatComposeEdit.svelte"
|
||||||
import ChatComposeParent from "@app/components/ChatComposeParent.svelte"
|
import ChatComposeParent from "@app/components/ChatComposeParent.svelte"
|
||||||
import ThunkToast from "@app/components/ThunkToast.svelte"
|
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||||
import {userSettingsValues, PLATFORM_NAME, deriveChat} from "@app/core/state"
|
import {userSettingsValues, PLATFORM_NAME, deriveChat} from "@app/core/state"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {prependParent} from "@app/core/commands"
|
import {makeDelete, prependParent} from "@app/core/commands"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -78,73 +81,115 @@
|
|||||||
parent = undefined
|
parent = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit = async (params: EventContent) => {
|
const clearEventToEdit = () => {
|
||||||
const ptags = remove($pubkey!, pubkeys).map(tagPubkey)
|
eventToEdit = undefined
|
||||||
|
|
||||||
// Remove p tags since they result in forking the conversation
|
|
||||||
params.tags = params.tags.filter(nthNe(0, "p"))
|
|
||||||
|
|
||||||
// Add our reply quote to content
|
|
||||||
params = prependParent(parent, params)
|
|
||||||
|
|
||||||
const [imetaTags, tags] = partition(nthEq(0, "imeta"), params.tags)
|
|
||||||
const imetas = getTags("imeta", imetaTags).map(tagsFromIMeta)
|
|
||||||
const templates: EventTemplate[] = []
|
|
||||||
const buffer = []
|
|
||||||
|
|
||||||
const addTemplate = (kind: number, content: string, tags: string[][]) => {
|
|
||||||
content = content.trim()
|
|
||||||
|
|
||||||
if (content) {
|
|
||||||
templates.push(makeEvent(kind, {content, tags: [...tags, ...ptags]}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const p of parse(params)) {
|
|
||||||
const imeta = isLink(p)
|
|
||||||
? imetas.find(tags => tags.find(spec(["url", p.value.url.toString()])))
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
if (isLink(p) && imeta) {
|
|
||||||
addTemplate(DIRECT_MESSAGE, buffer.splice(0).join(""), tags)
|
|
||||||
addTemplate(
|
|
||||||
DIRECT_MESSAGE_FILE,
|
|
||||||
p.value.url.toString(),
|
|
||||||
imeta.slice(1).filter(nthNe(0, "url")),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
buffer.push(p.raw)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addTemplate(DIRECT_MESSAGE, buffer.splice(0).join(""), tags)
|
|
||||||
|
|
||||||
// Split the message into multiple pieces so that we can use kind 15 to send images per nip 17
|
|
||||||
// Sleep 1 second between each one to make sure timestamps are distinct
|
|
||||||
const thunks = await Promise.all(
|
|
||||||
Array.from(enumerate(templates)).map(([i, event]) =>
|
|
||||||
sendWrapped({
|
|
||||||
event,
|
|
||||||
recipients: pubkeys,
|
|
||||||
delay: $userSettingsValues.send_delay + ms(i),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
pushToast({
|
|
||||||
timeout: 30_000,
|
|
||||||
children: {
|
|
||||||
component: ThunkToast,
|
|
||||||
props: {thunk: mergeThunks(thunks)},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
clearParent()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onSubmit = async (params: EventContent) => {
|
||||||
|
try {
|
||||||
|
const ptags = remove($pubkey!, pubkeys).map(tagPubkey)
|
||||||
|
|
||||||
|
// Remove p tags since they result in forking the conversation
|
||||||
|
params.tags = params.tags.filter(nthNe(0, "p"))
|
||||||
|
|
||||||
|
// Add our reply quote to content
|
||||||
|
params = prependParent(parent, params)
|
||||||
|
|
||||||
|
if (eventToEdit) {
|
||||||
|
if (eventToEdit.content === params.content) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendWrapped({
|
||||||
|
event: makeDelete({event: eventToEdit, protect: false}),
|
||||||
|
recipients: pubkeys,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const [imetaTags, tags] = partition(nthEq(0, "imeta"), params.tags)
|
||||||
|
const imetas = getTags("imeta", imetaTags).map(tagsFromIMeta)
|
||||||
|
const templates: EventTemplate[] = []
|
||||||
|
const buffer = []
|
||||||
|
|
||||||
|
const addTemplate = (kind: number, content: string, tags: string[][]) => {
|
||||||
|
content = content.trim()
|
||||||
|
|
||||||
|
if (content) {
|
||||||
|
templates.push(
|
||||||
|
makeEvent(kind, {
|
||||||
|
content,
|
||||||
|
tags: [...tags, ...ptags],
|
||||||
|
created_at: eventToEdit?.created_at,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const p of parse(params)) {
|
||||||
|
const imeta = isLink(p)
|
||||||
|
? imetas.find(tags => tags.find(spec(["url", p.value.url.toString()])))
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
if (isLink(p) && imeta) {
|
||||||
|
addTemplate(DIRECT_MESSAGE, buffer.splice(0).join(""), tags)
|
||||||
|
addTemplate(
|
||||||
|
DIRECT_MESSAGE_FILE,
|
||||||
|
p.value.url.toString(),
|
||||||
|
imeta.slice(1).filter(nthNe(0, "url")),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
buffer.push(p.raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addTemplate(DIRECT_MESSAGE, buffer.splice(0).join(""), tags)
|
||||||
|
|
||||||
|
// Split the message into multiple pieces so that we can use kind 15 to send images per nip 17
|
||||||
|
// Sleep 1 second between each one to make sure timestamps are distinct
|
||||||
|
const thunks = await Promise.all(
|
||||||
|
Array.from(enumerate(templates)).map(([i, event]) =>
|
||||||
|
sendWrapped({
|
||||||
|
event,
|
||||||
|
recipients: pubkeys,
|
||||||
|
delay: $userSettingsValues.send_delay + ms(i),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
pushToast({
|
||||||
|
timeout: 30_000,
|
||||||
|
children: {
|
||||||
|
component: ThunkToast,
|
||||||
|
props: {thunk: mergeThunks(thunks)},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
clearParent()
|
||||||
|
clearEventToEdit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEscape = () => {
|
||||||
|
clearParent()
|
||||||
|
clearEventToEdit()
|
||||||
|
}
|
||||||
|
|
||||||
|
const canEditEvent = (event: TrustedEvent) =>
|
||||||
|
event.pubkey === $pubkey &&
|
||||||
|
event.kind === DIRECT_MESSAGE &&
|
||||||
|
event.created_at >= ago(500, MINUTE)
|
||||||
|
|
||||||
|
const onEditEvent = (event: TrustedEvent) => {
|
||||||
|
clearParent()
|
||||||
|
eventToEdit = event
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEditPrevious = () => ifLet($chat?.messages.toReversed().find(canEditEvent), onEditEvent)
|
||||||
|
|
||||||
let loading = $state(true)
|
let loading = $state(true)
|
||||||
let compose: ChatCompose | undefined = $state()
|
let compose: ChatCompose | undefined = $state()
|
||||||
let parent: TrustedEvent | undefined = $state()
|
let parent: TrustedEvent | undefined = $state()
|
||||||
|
let eventToEdit: TrustedEvent | undefined = $state()
|
||||||
let chatCompose: HTMLElement | undefined = $state()
|
let chatCompose: HTMLElement | undefined = $state()
|
||||||
let dynamicPadding: HTMLElement | undefined = $state()
|
let dynamicPadding: HTMLElement | undefined = $state()
|
||||||
|
|
||||||
@@ -285,7 +330,9 @@
|
|||||||
event={$state.snapshot(value as TrustedEvent)}
|
event={$state.snapshot(value as TrustedEvent)}
|
||||||
{pubkeys}
|
{pubkeys}
|
||||||
{showPubkey}
|
{showPubkey}
|
||||||
{replyTo} />
|
{replyTo}
|
||||||
|
canEdit={canEditEvent}
|
||||||
|
onEdit={onEditEvent} />
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
<p class="m-auto flex h-10 max-w-sm flex-col items-center justify-center gap-4 py-20 text-center">
|
<p class="m-auto flex h-10 max-w-sm flex-col items-center justify-center gap-4 py-20 text-center">
|
||||||
@@ -305,6 +352,16 @@
|
|||||||
{#if parent}
|
{#if parent}
|
||||||
<ChatComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
<ChatComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if eventToEdit}
|
||||||
|
<ChatComposeEdit clear={clearEventToEdit} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<ChatCompose bind:this={compose} {onSubmit} />
|
{#key eventToEdit}
|
||||||
|
<ChatCompose
|
||||||
|
bind:this={compose}
|
||||||
|
{onSubmit}
|
||||||
|
{onEscape}
|
||||||
|
{onEditPrevious}
|
||||||
|
content={eventToEdit?.content} />
|
||||||
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {onDestroy, onMount} from "svelte"
|
||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import type {EventContent} from "@welshman/util"
|
import type {EventContent} from "@welshman/util"
|
||||||
import {isMobile, preventDefault} from "@lib/html"
|
import {isMobile, preventDefault} from "@lib/html"
|
||||||
@@ -10,10 +11,13 @@
|
|||||||
import {makeEditor} from "@app/editor"
|
import {makeEditor} from "@app/editor"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
content?: string
|
||||||
|
onEscape?: () => void
|
||||||
|
onEditPrevious?: () => void
|
||||||
onSubmit: (event: EventContent) => void
|
onSubmit: (event: EventContent) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const {onSubmit}: Props = $props()
|
const {content, onEscape, onEditPrevious, onSubmit}: Props = $props()
|
||||||
|
|
||||||
const autofocus = !isMobile
|
const autofocus = !isMobile
|
||||||
|
|
||||||
@@ -21,6 +25,19 @@
|
|||||||
|
|
||||||
export const focus = () => editor.then(ed => ed.chain().focus().run())
|
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())
|
const uploadFiles = () => editor.then(ed => ed.chain().selectFiles().run())
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
@@ -38,12 +55,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const editor = makeEditor({
|
const editor = makeEditor({
|
||||||
|
content,
|
||||||
autofocus,
|
autofocus,
|
||||||
submit,
|
submit,
|
||||||
uploading,
|
uploading,
|
||||||
aggressive: true,
|
aggressive: true,
|
||||||
encryptFiles: true,
|
encryptFiles: 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>
|
</script>
|
||||||
|
|
||||||
<form class="relative z-feature flex gap-2 p-2" onsubmit={preventDefault(submit)}>
|
<form class="relative z-feature flex gap-2 p-2" onsubmit={preventDefault(submit)}>
|
||||||
|
|||||||
@@ -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 flex h-8 items-center justify-between border-l-2 border-solid border-primary bg-base-300 px-2 pr-7 text-xs"
|
||||||
|
transition:slide>
|
||||||
|
<p class="text-primary">Editing message</p>
|
||||||
|
<Button onclick={clear} class="flex items-center">
|
||||||
|
<Icon icon={CloseCircle} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
@@ -23,11 +23,13 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
replyTo: (event: TrustedEvent) => void
|
replyTo: (event: TrustedEvent) => void
|
||||||
|
canEdit?: (event: TrustedEvent) => boolean
|
||||||
|
onEdit?: (event: TrustedEvent) => void
|
||||||
pubkeys: string[]
|
pubkeys: string[]
|
||||||
showPubkey?: boolean
|
showPubkey?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const {event, replyTo, pubkeys, showPubkey = false}: Props = $props()
|
const {event, replyTo, canEdit, onEdit, pubkeys, showPubkey = false}: Props = $props()
|
||||||
|
|
||||||
const isOwn = event.pubkey === $pubkey
|
const isOwn = event.pubkey === $pubkey
|
||||||
const profileDisplay = deriveProfileDisplay(event.pubkey)
|
const profileDisplay = deriveProfileDisplay(event.pubkey)
|
||||||
@@ -35,6 +37,7 @@
|
|||||||
const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
|
const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
|
||||||
|
|
||||||
const reply = () => replyTo(event)
|
const reply = () => replyTo(event)
|
||||||
|
const edit = canEdit?.(event) ? () => onEdit?.(event) : undefined
|
||||||
|
|
||||||
const deleteReaction = (event: TrustedEvent) =>
|
const deleteReaction = (event: TrustedEvent) =>
|
||||||
sendWrapped({event: makeDelete({event, protect: false}), recipients: pubkeys})
|
sendWrapped({event: makeDelete({event, protect: false}), recipients: pubkeys})
|
||||||
@@ -44,7 +47,7 @@
|
|||||||
|
|
||||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
||||||
|
|
||||||
const showMobileMenu = () => pushModal(ChatMessageMenuMobile, {event, pubkeys, reply})
|
const showMobileMenu = () => pushModal(ChatMessageMenuMobile, {event, pubkeys, reply, edit})
|
||||||
|
|
||||||
const togglePopover = () => {
|
const togglePopover = () => {
|
||||||
if (popoverIsVisible) {
|
if (popoverIsVisible) {
|
||||||
@@ -71,7 +74,7 @@
|
|||||||
<Tippy
|
<Tippy
|
||||||
bind:popover
|
bind:popover
|
||||||
component={ChatMessageMenu}
|
component={ChatMessageMenu}
|
||||||
props={{event, pubkeys, popover, replyTo}}
|
props={{event, pubkeys, popover, replyTo, edit}}
|
||||||
params={{
|
params={{
|
||||||
interactive: true,
|
interactive: true,
|
||||||
trigger: "manual",
|
trigger: "manual",
|
||||||
@@ -93,7 +96,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<div class="flex min-w-0 flex-col" class:items-end={isOwn}>
|
<div class="flex min-w-0 flex-col" class:items-end={isOwn}>
|
||||||
<TapTarget
|
<TapTarget
|
||||||
class="bg-alt chat-bubble mx-1 mb-2 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl"
|
class="bg-alt chat-bubble mx-1 mb-2 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl min-w-[100px]"
|
||||||
onTap={showMobileMenu}>
|
onTap={showMobileMenu}>
|
||||||
{#if showPubkey}
|
{#if showPubkey}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
|||||||
@@ -4,12 +4,14 @@
|
|||||||
import ChatMessageEmojiButton from "@app/components/ChatMessageEmojiButton.svelte"
|
import ChatMessageEmojiButton from "@app/components/ChatMessageEmojiButton.svelte"
|
||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||||
|
|
||||||
const {event, pubkeys, popover, replyTo} = $props()
|
const {event, pubkeys, popover, replyTo, edit} = $props()
|
||||||
|
|
||||||
const reply = () => replyTo(event)
|
const reply = () => replyTo(event)
|
||||||
|
const onEdit = () => edit?.()
|
||||||
|
|
||||||
const showInfo = () => {
|
const showInfo = () => {
|
||||||
popover.hide()
|
popover.hide()
|
||||||
@@ -24,6 +26,11 @@
|
|||||||
<Icon size={4} icon={Reply} />
|
<Icon size={4} icon={Reply} />
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if edit}
|
||||||
|
<Button class="btn join-item btn-xs" onclick={onEdit}>
|
||||||
|
<Icon size={4} icon={Pen} />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
<Button class="btn join-item btn-xs" onclick={showInfo}>
|
<Button class="btn join-item btn-xs" onclick={showInfo}>
|
||||||
<Icon size={4} icon={Code2} />
|
<Icon size={4} icon={Code2} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {sendWrapped} from "@welshman/app"
|
import {sendWrapped} from "@welshman/app"
|
||||||
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
|
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
|
||||||
|
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||||
import Copy from "@assets/icons/copy.svg?dataurl"
|
import Copy from "@assets/icons/copy.svg?dataurl"
|
||||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||||
@@ -20,9 +21,10 @@
|
|||||||
pubkeys: string[]
|
pubkeys: string[]
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
reply: () => void
|
reply: () => void
|
||||||
|
edit?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const {event, pubkeys, reply}: Props = $props()
|
const {event, pubkeys, reply, edit}: Props = $props()
|
||||||
|
|
||||||
const onEmoji = ((event: TrustedEvent, pubkeys: string[], emoji: NativeEmoji) => {
|
const onEmoji = ((event: TrustedEvent, pubkeys: string[], emoji: NativeEmoji) => {
|
||||||
history.back()
|
history.back()
|
||||||
@@ -39,6 +41,11 @@
|
|||||||
reply()
|
reply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sendEdit = () => {
|
||||||
|
history.back()
|
||||||
|
edit?.()
|
||||||
|
}
|
||||||
|
|
||||||
const copyText = () => {
|
const copyText = () => {
|
||||||
history.back()
|
history.back()
|
||||||
clip(event.content)
|
clip(event.content)
|
||||||
@@ -62,6 +69,12 @@
|
|||||||
<Icon size={4} icon={Reply} />
|
<Icon size={4} icon={Reply} />
|
||||||
Send Reply
|
Send Reply
|
||||||
</Button>
|
</Button>
|
||||||
|
{#if edit}
|
||||||
|
<Button class="btn btn-neutral w-full" onclick={sendEdit}>
|
||||||
|
<Icon size={4} icon={Pen} />
|
||||||
|
Edit Message
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
<Button class="btn btn-primary w-full" onclick={showEmojiPicker}>
|
<Button class="btn btn-primary w-full" onclick={showEmojiPicker}>
|
||||||
<Icon size={4} icon={SmileCircle} />
|
<Icon size={4} icon={SmileCircle} />
|
||||||
Send Reaction
|
Send Reaction
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
<p>Your recovery codes have been sent!</p>
|
<p>Your recovery codes have been sent!</p>
|
||||||
<p>
|
<p>
|
||||||
For security reasons, you may receive three or more emails with recovery codes in them. Please
|
For security reasons, you may receive three or more emails with recovery codes in them. Please
|
||||||
paste <strong>all</strong> recovery codes into the text box below, on separate lines.
|
paste <strong>all</strong> recovery codes into the text box below.
|
||||||
</p>
|
</p>
|
||||||
<StringMultiInput bind:value={otps} placeholder="Enter your recovery codes..." />
|
<StringMultiInput bind:value={otps} placeholder="Enter your recovery codes..." />
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|||||||
@@ -86,7 +86,7 @@
|
|||||||
<p>Your login codes have been sent!</p>
|
<p>Your login codes have been sent!</p>
|
||||||
<p>
|
<p>
|
||||||
For security reasons, you may receive three or more emails with login codes in them. Please
|
For security reasons, you may receive three or more emails with login codes in them. Please
|
||||||
paste <strong>all</strong> login codes into the text box below, on separate lines.
|
paste <strong>all</strong> login codes into the text box below.
|
||||||
</p>
|
</p>
|
||||||
<StringMultiInput bind:value={otps} placeholder="Enter your login codes..." />
|
<StringMultiInput bind:value={otps} placeholder="Enter your login codes..." />
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|||||||
@@ -89,7 +89,7 @@
|
|||||||
<p>Let's start by confirming your email.</p>
|
<p>Let's start by confirming your email.</p>
|
||||||
<p>
|
<p>
|
||||||
For security reasons, you may receive three or more emails with confirmation codes in them.
|
For security reasons, you may receive three or more emails with confirmation codes in them.
|
||||||
Please paste <strong>all</strong> confirmation codes into the text box below, on separate lines.
|
Please paste <strong>all</strong> confirmation codes into the text box below.
|
||||||
</p>
|
</p>
|
||||||
<StringMultiInput bind:value={otps} placeholder="Enter your confirmation codes..." />
|
<StringMultiInput bind:value={otps} placeholder="Enter your confirmation codes..." />
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|||||||
+44
-13
@@ -14,6 +14,7 @@ import {
|
|||||||
uniqBy,
|
uniqBy,
|
||||||
sortBy,
|
sortBy,
|
||||||
append,
|
append,
|
||||||
|
reject,
|
||||||
sort,
|
sort,
|
||||||
uniq,
|
uniq,
|
||||||
indexBy,
|
indexBy,
|
||||||
@@ -457,7 +458,7 @@ export const splitChatId = (id: string) => getChatPubkeys(id.split(","))
|
|||||||
|
|
||||||
export const chatsById = call(() => {
|
export const chatsById = call(() => {
|
||||||
const chatsById = new Map<string, Chat>()
|
const chatsById = new Map<string, Chat>()
|
||||||
const chatsByPubkey = new Map<string, Chat[]>()
|
const chatsByPubkey = new Map<string, string[]>()
|
||||||
|
|
||||||
const addSearchText = (chat: Override<Chat, {search_text?: string}>) => {
|
const addSearchText = (chat: Override<Chat, {search_text?: string}>) => {
|
||||||
chat.search_text =
|
chat.search_text =
|
||||||
@@ -469,6 +470,12 @@ export const chatsById = call(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return readable(chatsById, set => {
|
return readable(chatsById, set => {
|
||||||
|
const indexChatByPubkeys = (chat: Chat) => {
|
||||||
|
for (const pubkey of chat.pubkeys) {
|
||||||
|
chatsByPubkey.set(pubkey, uniq(append(chat.id, chatsByPubkey.get(pubkey) || [])))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const addEvents = (events: TrustedEvent[]) => {
|
const addEvents = (events: TrustedEvent[]) => {
|
||||||
let dirty = false
|
let dirty = false
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
@@ -484,21 +491,19 @@ export const chatsById = call(() => {
|
|||||||
const updatedChat = addSearchText({id, pubkeys, messages, last_activity})
|
const updatedChat = addSearchText({id, pubkeys, messages, last_activity})
|
||||||
|
|
||||||
chatsById.set(id, updatedChat)
|
chatsById.set(id, updatedChat)
|
||||||
|
indexChatByPubkeys(updatedChat)
|
||||||
for (const pubkey of pubkeys) {
|
|
||||||
const pubkeyChats = chatsByPubkey.get(pubkey) || []
|
|
||||||
const uniqueChats = uniqBy(chat => chat.id, append(updatedChat, pubkeyChats))
|
|
||||||
|
|
||||||
chatsByPubkey.set(pubkey, uniqueChats)
|
|
||||||
}
|
|
||||||
|
|
||||||
dirty = true
|
dirty = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.kind === PROFILE) {
|
if (event.kind === PROFILE) {
|
||||||
for (const chat of chatsByPubkey.get(event.pubkey) || []) {
|
for (const chatId of chatsByPubkey.get(event.pubkey) || []) {
|
||||||
addSearchText(chat)
|
const chat = chatsById.get(chatId)
|
||||||
dirty = true
|
|
||||||
|
if (chat) {
|
||||||
|
addSearchText(chat)
|
||||||
|
dirty = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -508,10 +513,36 @@ export const chatsById = call(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addEvents(repository.query([{kinds: [...DM_KINDS, PROFILE]}]))
|
const removeEvents = (removed: Set<string>) => {
|
||||||
|
let dirty = false
|
||||||
|
|
||||||
|
for (const id of removed) {
|
||||||
|
const event = repository.getEvent(id)
|
||||||
|
|
||||||
|
if (event && DM_KINDS.includes(event.kind)) {
|
||||||
|
for (const chatId of chatsByPubkey.get(event.pubkey) || []) {
|
||||||
|
const chat = chatsById.get(chatId)
|
||||||
|
|
||||||
|
if (chat) {
|
||||||
|
chat.messages = reject(spec({id: event.id}), chat.messages)
|
||||||
|
dirty = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dirty) {
|
||||||
|
set(chatsById)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addEvents(repository.query([{kinds: [...DM_KINDS, DELETE, PROFILE]}]))
|
||||||
|
|
||||||
const unsubscribers = [
|
const unsubscribers = [
|
||||||
on(repository, "update", ({added}: RepositoryUpdate) => addEvents(added)),
|
on(repository, "update", ({added, removed}: RepositoryUpdate) => {
|
||||||
|
addEvents(added)
|
||||||
|
removeEvents(removed)
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
return () => unsubscribers.forEach(call)
|
return () => unsubscribers.forEach(call)
|
||||||
|
|||||||
@@ -156,7 +156,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<SignerStatus />
|
<SignerStatus />
|
||||||
{#if $session?.method === SessionMethod.Pomade}
|
{#if $session?.method === SessionMethod.Pomade}
|
||||||
<div class="flex gap-2 justify-end">
|
<div class="flex flex-col lg:flex-row gap-4 lg:gap-2 justify-end">
|
||||||
<Button class="btn" onclick={startPasswordReset}>
|
<Button class="btn" onclick={startPasswordReset}>
|
||||||
<Spinner {loading}>Update your password</Spinner>
|
<Spinner {loading}>Update your password</Spinner>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
import type {Readable} from "svelte/store"
|
import type {Readable} from "svelte/store"
|
||||||
import {pubkey, publishThunk, waitForThunkError, joinRoom, leaveRoom} from "@welshman/app"
|
import {pubkey, publishThunk, waitForThunkError, joinRoom, leaveRoom} from "@welshman/app"
|
||||||
import {now, int, formatTimestampAsDate, ago, MINUTE} from "@welshman/lib"
|
import {now, ifLet, int, formatTimestampAsDate, ago, MINUTE} from "@welshman/lib"
|
||||||
import type {MakeNonOptional} from "@welshman/lib"
|
import type {MakeNonOptional} from "@welshman/lib"
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
@@ -336,13 +336,7 @@
|
|||||||
eventToEdit = event
|
eventToEdit = event
|
||||||
}
|
}
|
||||||
|
|
||||||
const onEditPrevious = () => {
|
const onEditPrevious = () => ifLet($events.toReversed().find(canEditEvent), onEditEvent)
|
||||||
const prev = $events.toReversed().find(e => e.pubkey === $pubkey)
|
|
||||||
|
|
||||||
if (prev && canEditEvent(prev)) {
|
|
||||||
onEditEvent(prev)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const observer = new ResizeObserver(() => {
|
const observer = new ResizeObserver(() => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
import type {Readable} from "svelte/store"
|
import type {Readable} from "svelte/store"
|
||||||
import {readable} from "svelte/store"
|
import {readable} from "svelte/store"
|
||||||
import {now, int, formatTimestampAsDate, MINUTE, ago} from "@welshman/lib"
|
import {now, int, ifLet, formatTimestampAsDate, MINUTE, ago} from "@welshman/lib"
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {makeEvent, MESSAGE, RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER} from "@welshman/util"
|
import {makeEvent, MESSAGE, RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER} from "@welshman/util"
|
||||||
import {pubkey, publishThunk} from "@welshman/app"
|
import {pubkey, publishThunk} from "@welshman/app"
|
||||||
@@ -272,13 +272,7 @@
|
|||||||
eventToEdit = event
|
eventToEdit = event
|
||||||
}
|
}
|
||||||
|
|
||||||
const onEditPrevious = () => {
|
const onEditPrevious = () => ifLet($events.toReversed().find(canEditEvent), onEditEvent)
|
||||||
const prev = $events.toReversed().find(e => e.pubkey === $pubkey)
|
|
||||||
|
|
||||||
if (prev && canEditEvent(prev)) {
|
|
||||||
onEditEvent(prev)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
|
|||||||
Reference in New Issue
Block a user