mirror of
https://github.com/coracle-social/flotilla.git
synced 2025-12-10 10:57:04 +00:00
Get nip 17 chat working
This commit is contained in:
@@ -125,7 +125,7 @@ export const updateList = async (kind: number, modifyTags: ModifyTags) => {
|
||||
? {...prev, tags: modifyTags(prev.tags)}
|
||||
: createEvent(kind, {tags: modifyTags([])})
|
||||
|
||||
publishThunk(makeThunk({event, relays}))
|
||||
return publishThunk(makeThunk({event, relays}))
|
||||
}
|
||||
|
||||
export const addSpaceMembership = (url: string) =>
|
||||
@@ -215,13 +215,12 @@ export const sendWrapped = async ({
|
||||
|
||||
await Promise.all(
|
||||
uniq(pubkeys).map(async recipient => {
|
||||
const rumor = await nip59.wrap(recipient, stamp(template))
|
||||
const thunk = makeThunk({
|
||||
event: rumor.wrap,
|
||||
relays: ctx.app.router.PublishMessage(recipient).getUrls(),
|
||||
})
|
||||
|
||||
return publishThunk(thunk)
|
||||
return publishThunk(
|
||||
makeThunk({
|
||||
event: await nip59.wrap(recipient, stamp(template)),
|
||||
relays: ctx.app.router.PublishMessage(recipient).getUrls(),
|
||||
})
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,65 +50,67 @@
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="group relative flex w-full flex-col gap-1 p-2 text-left transition-colors hover:bg-base-300">
|
||||
<div class="flex gap-2">
|
||||
{#if showPubkey}
|
||||
<Avatar src={$profile?.picture} class="border border-solid border-base-content" size={10} />
|
||||
{:else}
|
||||
<div class="w-10 min-w-10 max-w-10" />
|
||||
{/if}
|
||||
<div class="-mt-1 flex-grow pr-1">
|
||||
class="group chat relative flex w-full flex-col gap-1 p-2 text-left"
|
||||
class:chat-start={event.pubkey !== $pubkey}
|
||||
class:chat-end={event.pubkey === $pubkey}>
|
||||
<div class="chat-bubble max-w-sm">
|
||||
<div class="flex gap-2">
|
||||
{#if showPubkey}
|
||||
<div class="flex items-center gap-2">
|
||||
<strong class="text-sm" style="color: {colorValue}" data-color={colorName}
|
||||
>{$profileDisplay}</strong>
|
||||
<span class="text-xs opacity-50">{formatTimestampAsTime(event.created_at)}</span>
|
||||
</div>
|
||||
<Avatar src={$profile?.picture} class="border border-solid border-base-content" size={10} />
|
||||
{/if}
|
||||
<div class="text-sm">
|
||||
<Content showEntire {event} />
|
||||
{#if isPending}
|
||||
<span class="flex-inline ml-1 gap-1">
|
||||
<span class="loading loading-spinner mx-1 h-3 w-3 translate-y-px" />
|
||||
<span class="opacity-50">Sending...</span>
|
||||
</span>
|
||||
{:else if failure}
|
||||
<span
|
||||
class="flex-inline tooltip ml-1 cursor-pointer gap-1"
|
||||
data-tip="{failure.message} ({displayRelayUrl(failure.url)})">
|
||||
<Icon icon="danger" class="translate-y-px" size={3} />
|
||||
<span class="opacity-50">Failed to send!</span>
|
||||
</span>
|
||||
<div class="-mt-1 flex-grow pr-1">
|
||||
{#if showPubkey}
|
||||
<div class="flex items-center gap-2">
|
||||
<strong class="text-sm" style="color: {colorValue}" data-color={colorName}
|
||||
>{$profileDisplay}</strong>
|
||||
<span class="text-xs opacity-50">{formatTimestampAsTime(event.created_at)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="text-sm">
|
||||
<Content showEntire {event} />
|
||||
{#if isPending}
|
||||
<span class="flex-inline ml-1 gap-1">
|
||||
<span class="loading loading-spinner mx-1 h-3 w-3 translate-y-px" />
|
||||
<span class="opacity-50">Sending...</span>
|
||||
</span>
|
||||
{:else if failure}
|
||||
<span
|
||||
class="flex-inline tooltip ml-1 cursor-pointer gap-1"
|
||||
data-tip="{failure.message} ({displayRelayUrl(failure.url)})">
|
||||
<Icon icon="danger" class="translate-y-px" size={3} />
|
||||
<span class="opacity-50">Failed to send!</span>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if $reactions.length > 0 || $zaps.length > 0}
|
||||
<div class="ml-12 text-xs">
|
||||
{#each groupBy( e => e.content, uniqBy(e => e.pubkey + e.content, $reactions), ).entries() as [content, events]}
|
||||
{@const isOwn = events.some(e => e.pubkey === $pubkey)}
|
||||
{@const onClick = () => onReactionClick(content, events)}
|
||||
<button
|
||||
type="button"
|
||||
class="flex-inline btn btn-neutral btn-xs mr-2 gap-1 rounded-full"
|
||||
class:border={isOwn}
|
||||
class:border-solid={isOwn}
|
||||
class:border-primary={isOwn}
|
||||
on:click|stopPropagation={onClick}>
|
||||
<span>{displayReaction(content)}</span>
|
||||
{#if events.length > 1}
|
||||
<span>{events.length}</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<button
|
||||
class="join absolute -top-2 right-0 border border-solid border-neutral text-xs opacity-0 transition-all group-hover:opacity-100"
|
||||
on:click|stopPropagation>
|
||||
<ChatMessageEmojiButton {event} {pubkeys} />
|
||||
<button class="btn join-item btn-xs">
|
||||
<Icon icon="menu-dots" size={4} />
|
||||
{#if $reactions.length > 0 || $zaps.length > 0}
|
||||
<div class="ml-12 text-xs">
|
||||
{#each groupBy( e => e.content, uniqBy(e => e.pubkey + e.content, $reactions), ).entries() as [content, events]}
|
||||
{@const isOwn = events.some(e => e.pubkey === $pubkey)}
|
||||
{@const onClick = () => onReactionClick(content, events)}
|
||||
<button
|
||||
type="button"
|
||||
class="flex-inline btn btn-neutral btn-xs mr-2 gap-1 rounded-full"
|
||||
class:border={isOwn}
|
||||
class:border-solid={isOwn}
|
||||
class:border-primary={isOwn}
|
||||
on:click|stopPropagation={onClick}>
|
||||
<span>{displayReaction(content)}</span>
|
||||
{#if events.length > 1}
|
||||
<span>{events.length}</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<button
|
||||
class="join absolute -top-2 right-0 border border-solid border-neutral text-xs opacity-0 transition-all group-hover:opacity-100"
|
||||
on:click|stopPropagation>
|
||||
<ChatMessageEmojiButton {event} {pubkeys} />
|
||||
<button class="btn join-item btn-xs">
|
||||
<Icon icon="menu-dots" size={4} />
|
||||
</button>
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
const back = () => history.back()
|
||||
|
||||
const tryCreate = async () => {
|
||||
await addRoomMembership(url, room)
|
||||
|
||||
addRoomMembership(url, room)
|
||||
goto(makeSpacePath(url, room))
|
||||
}
|
||||
|
||||
|
||||
@@ -149,7 +149,10 @@ export const ensureUnwrapped = async (event: TrustedEvent) => {
|
||||
}
|
||||
|
||||
if (rumor && isHashedEvent(rumor)) {
|
||||
// Copy urls over to the rumor
|
||||
tracker.copy(event.id, rumor.id)
|
||||
|
||||
// Send the rumor via our relay so listeners get updated
|
||||
relay.send("EVENT", rumor)
|
||||
}
|
||||
|
||||
@@ -187,12 +190,6 @@ setContext({
|
||||
}),
|
||||
})
|
||||
|
||||
repository.on("update", ({added}) => {
|
||||
for (const event of added) {
|
||||
ensureUnwrapped(event)
|
||||
}
|
||||
})
|
||||
|
||||
export const deriveEvent = (idOrAddress: string, hints: string[] = []) => {
|
||||
let attempted = false
|
||||
|
||||
@@ -350,7 +347,7 @@ export const chats = derived(
|
||||
const messagesByChatId = new Map<string, TrustedEvent[]>()
|
||||
|
||||
for (const message of $messages) {
|
||||
const chatId = makeChatId(getPubkeyTagValues(message.tags))
|
||||
const chatId = makeChatId(getPubkeyTagValues(message.tags).concat(message.pubkey))
|
||||
|
||||
pushToMapKey(messagesByChatId, chatId, message)
|
||||
}
|
||||
@@ -383,16 +380,6 @@ export const {
|
||||
name: "chats",
|
||||
store: chats,
|
||||
getKey: chat => chat.id,
|
||||
load: async (id: string, request: Partial<SubscribeRequestWithHandlers> = {}) => {
|
||||
const $pubkey = pubkey.get()
|
||||
const chat = get(chatsById).get(id)
|
||||
const timestamps = chat?.messages.map(e => e.created_at) || []
|
||||
const since = Math.max(0, max(timestamps) - 3600)
|
||||
|
||||
if ($pubkey) {
|
||||
await load({...request, filters: [{kinds: [WRAP], "#p": [$pubkey], since}]})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const chatSearch = derived(chats, $chats =>
|
||||
|
||||
@@ -68,28 +68,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
onMount(async () => {
|
||||
Object.assign(window, {get, ...app, ...state})
|
||||
|
||||
ready = db
|
||||
? Promise.resolve()
|
||||
: initStorage("flotilla", 4, {
|
||||
events: storageAdapters.fromRepository(repository, {throttle: 300}),
|
||||
relays: {keyPath: "url", store: throttled(1000, relays)},
|
||||
handles: {keyPath: "nip05", store: throttled(1000, handles)},
|
||||
publishStatus: storageAdapters.fromObjectStore(publishStatusData),
|
||||
freshness: storageAdapters.fromObjectStore(freshness, {throttle: 1000}),
|
||||
plaintext: storageAdapters.fromObjectStore(plaintext, {throttle: 1000}),
|
||||
tracker: storageAdapters.fromTracker(tracker, {throttle: 1000}),
|
||||
}).then(() => sleep(300)) // Wait an extra few ms because of repository throttle
|
||||
if (!db) {
|
||||
await initStorage("flotilla", 4, {
|
||||
events: storageAdapters.fromRepository(repository, {throttle: 300}),
|
||||
relays: {keyPath: "url", store: throttled(1000, relays)},
|
||||
handles: {keyPath: "nip05", store: throttled(1000, handles)},
|
||||
publishStatus: storageAdapters.fromObjectStore(publishStatusData),
|
||||
freshness: storageAdapters.fromObjectStore(freshness, {throttle: 1000}),
|
||||
plaintext: storageAdapters.fromObjectStore(plaintext, {throttle: 1000}),
|
||||
tracker: storageAdapters.fromTracker(tracker, {throttle: 1000}),
|
||||
})
|
||||
|
||||
dialog.addEventListener("close", () => {
|
||||
if (modal) {
|
||||
clearModal()
|
||||
}
|
||||
})
|
||||
repository.on("update", ({added}) => {
|
||||
for (const event of added) {
|
||||
state.ensureUnwrapped(event)
|
||||
}
|
||||
})
|
||||
|
||||
dialog.addEventListener("close", () => {
|
||||
if (modal) {
|
||||
clearModal()
|
||||
}
|
||||
})
|
||||
|
||||
ready.then(() => {
|
||||
for (const url of INDEXER_RELAYS) {
|
||||
loadRelay(url)
|
||||
}
|
||||
@@ -97,7 +101,7 @@
|
||||
if ($pubkey) {
|
||||
loadUserData($pubkey)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {page} from "$app/stores"
|
||||
import {ctx, ago, remove} from "@welshman/lib"
|
||||
import {WRAP} from "@welshman/util"
|
||||
import {pubkey, subscribe} from "@welshman/app"
|
||||
@@ -65,7 +66,7 @@
|
||||
</div>
|
||||
</SecondaryNavSection>
|
||||
<label
|
||||
class="input input-sm input-bordered mx-6 -mt-4 flex items-center gap-2"
|
||||
class="input input-sm input-bordered mx-6 -mt-4 flex items-center gap-2 mb-2"
|
||||
in:fly={{delay: 200}}>
|
||||
<Icon icon="magnifer" />
|
||||
<input bind:value={term} class="grow" type="text" />
|
||||
@@ -74,8 +75,10 @@
|
||||
{#each chats as { id, pubkeys, messages }, i (id)}
|
||||
{@const message = messages[0]}
|
||||
{@const others = remove($pubkey, pubkeys)}
|
||||
{@const active = $page.params.chat === id}
|
||||
<div
|
||||
class="cursor-pointer border-t border-solid border-base-100 px-6 py-2 transition-colors hover:bg-base-100">
|
||||
class="cursor-pointer border-t border-solid border-base-100 px-6 py-2 transition-colors hover:bg-base-100"
|
||||
class:bg-base-100={active}>
|
||||
<Link class="flex flex-col justify-start gap-1" href="/home/{id}">
|
||||
<div class="flex items-center gap-2">
|
||||
{#if others.length === 1}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
import ChatMessage from "@app/components/ChatMessage.svelte"
|
||||
import ChatCompose from "@app/components/ChannelCompose.svelte"
|
||||
import {deriveChat, splitChatId} from "@app/state"
|
||||
import {sendWrapped} from "@app/commands"
|
||||
|
||||
const {chat: id} = $page.params
|
||||
const chat = deriveChat(id)
|
||||
@@ -41,18 +42,8 @@
|
||||
const onSubmit = async ({content, ...params}: EventContent) => {
|
||||
const tags = [...params.tags, ...pubkeys.map(pubkey => tagPubkey(pubkey))]
|
||||
const template = createEvent(DIRECT_MESSAGE, {content, tags})
|
||||
const nip59 = Nip59.fromSigner($signer!)
|
||||
|
||||
for (const recipient of uniq(pubkeys)) {
|
||||
const rumor = await nip59.wrap(recipient, template)
|
||||
|
||||
publishThunk(
|
||||
makeThunk({
|
||||
event: rumor.wrap,
|
||||
relays: ctx.app.router.PublishMessage(recipient).getUrls(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
await sendWrapped({template, pubkeys})
|
||||
}
|
||||
|
||||
let loading = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {page} from "$app/stores"
|
||||
import {sort} from "@welshman/lib"
|
||||
import {sort, ago} from "@welshman/lib"
|
||||
import {displayRelayUrl, REACTION, NOTE, EVENT_DATE, EVENT_TIME, CLASSIFIED} from "@welshman/util"
|
||||
import {subscribe} from "@welshman/app"
|
||||
import {fly, slide} from "@lib/transition"
|
||||
@@ -16,7 +16,7 @@
|
||||
import SpaceExit from "@app/components/SpaceExit.svelte"
|
||||
import SpaceJoin from "@app/components/SpaceJoin.svelte"
|
||||
import RoomCreate from "@app/components/RoomCreate.svelte"
|
||||
import {userMembership, roomsByUrl, decodeNRelay, GENERAL, MESSAGE} from "@app/state"
|
||||
import {userMembership, pullConservatively, roomsByUrl, decodeNRelay, GENERAL, MESSAGE} from "@app/state"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
|
||||
@@ -52,8 +52,10 @@
|
||||
$: otherRooms = ($roomsByUrl.get(url) || []).filter(room => !rooms.concat(GENERAL).includes(room))
|
||||
|
||||
onMount(() => {
|
||||
const kinds = [NOTE, REACTION, MESSAGE, EVENT_DATE, EVENT_TIME, CLASSIFIED]
|
||||
const sub = subscribe({filters: [{kinds}], relays: [url]})
|
||||
const filter = {kinds: [NOTE, REACTION, MESSAGE, EVENT_DATE, EVENT_TIME, CLASSIFIED]}
|
||||
const sub = subscribe({filters: [{...filter, since: ago(30)}]})
|
||||
|
||||
pullConservatively({filters: [filter], relays: [url]})
|
||||
|
||||
return () => sub.close()
|
||||
})
|
||||
@@ -99,7 +101,7 @@
|
||||
</div>
|
||||
<div in:fly={{delay: getDelay(true)}}>
|
||||
<SecondaryNavItem href={makeSpacePath(url, "threads")}>
|
||||
<Icon icon="notes-minimalistic" /> Threads
|
||||
<Icon icon="notes-minimalistic" /> Notes
|
||||
</SecondaryNavItem>
|
||||
</div>
|
||||
<div in:fly={{delay: getDelay()}}>
|
||||
|
||||
Reference in New Issue
Block a user