Add emoji reactions
This commit is contained in:
@@ -4,7 +4,5 @@ A discord-like nostr client. WIP.
|
||||
|
||||
# Notes
|
||||
|
||||
- Privacy, migrations, and content replication
|
||||
- Allow relays to strip signatures based on auth'd user
|
||||
- Federated relays/admins can get signatures
|
||||
- Other users have to opt-in to using the relay in trusted mode
|
||||
- [ ] Delete events when leaving a space
|
||||
- [ ] Add topic and event tags to compose
|
||||
|
||||
Generated
+6
@@ -32,6 +32,7 @@
|
||||
"@welshman/util": "^0.0.33",
|
||||
"daisyui": "^4.12.10",
|
||||
"date-picker-svelte": "^2.13.0",
|
||||
"emoji-picker-element": "^1.22.8",
|
||||
"fuse.js": "^7.0.0",
|
||||
"idb": "^8.0.0",
|
||||
"nostr-editor": "^0.0.1",
|
||||
@@ -2543,6 +2544,11 @@
|
||||
"integrity": "sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/emoji-picker-element": {
|
||||
"version": "1.22.8",
|
||||
"resolved": "https://registry.npmjs.org/emoji-picker-element/-/emoji-picker-element-1.22.8.tgz",
|
||||
"integrity": "sha512-EFgRjrlIcdA1ilyMH/f9KjB0Pi/vynrojNgMDZfU1Jv2YLrhdLJWx6xCehizPyxm4/NUuB8DfFvIT4v+1njjPQ=="
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"@welshman/util": "^0.0.33",
|
||||
"daisyui": "^4.12.10",
|
||||
"date-picker-svelte": "^2.13.0",
|
||||
"emoji-picker-element": "^1.22.8",
|
||||
"fuse.js": "^7.0.0",
|
||||
"idb": "^8.0.0",
|
||||
"nostr-editor": "^0.0.1",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import type {Emoji} from 'emoji-picker-element/shared'
|
||||
import twColors from "tailwindcss/colors"
|
||||
import type {Readable} from "svelte/store"
|
||||
import {readable, derived} from "svelte/store"
|
||||
import {hash, groupBy, now} from "@welshman/lib"
|
||||
import {hash, uniqBy, groupBy, now} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {deriveEvents} from "@welshman/store"
|
||||
import {PublishStatus} from "@welshman/net"
|
||||
@@ -12,17 +13,25 @@
|
||||
deriveProfile,
|
||||
deriveProfileDisplay,
|
||||
formatTimestampAsTime,
|
||||
tagReactionTo,
|
||||
tagEvent,
|
||||
makeThunk,
|
||||
publishThunk,
|
||||
pubkey,
|
||||
} from "@welshman/app"
|
||||
import type {PublishStatusData} from "@welshman/app"
|
||||
import {REACTION, ZAP_RESPONSE, displayRelayUrl, getAncestorTags} from "@welshman/util"
|
||||
import {REACTION, DELETE, ZAP_RESPONSE, createEvent, displayRelayUrl, getAncestorTags} from "@welshman/util"
|
||||
import {repository} from "@welshman/app"
|
||||
import {fly} from "@lib/transition"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import {REPLY, deriveEvent, displayReaction} from "@app/state"
|
||||
import ChatMessageEmojiButton from "@app/components/ChatMessageEmojiButton.svelte"
|
||||
import {ROOM, REPLY, deriveEvent, displayReaction} from "@app/state"
|
||||
|
||||
export let url
|
||||
export let room
|
||||
export let event: TrustedEvent
|
||||
export let showPubkey: boolean
|
||||
|
||||
@@ -62,6 +71,34 @@
|
||||
const findStatus = ($ps: PublishStatusData[], statuses: PublishStatus[]) =>
|
||||
$ps.find(({status}) => statuses.includes(status))
|
||||
|
||||
const createReaction = (content: string) => {
|
||||
const reaction = createEvent(REACTION, {
|
||||
content,
|
||||
tags: [
|
||||
[ROOM, room, url],
|
||||
...tagReactionTo(event),
|
||||
],
|
||||
})
|
||||
|
||||
publishThunk(makeThunk({event: reaction, relays: [url]}))
|
||||
}
|
||||
|
||||
const onReactionClick = (content: string, events: TrustedEvent[]) => {
|
||||
const reaction = events.find(e => e.pubkey === $pubkey)
|
||||
|
||||
if (reaction) {
|
||||
const deleteEvent = createEvent(DELETE, {
|
||||
tags: [["k", String(reaction.kind)], ...tagEvent(reaction)],
|
||||
})
|
||||
|
||||
publishThunk(makeThunk({event: deleteEvent, relays: [url]}))
|
||||
} else {
|
||||
createReaction(content)
|
||||
}
|
||||
}
|
||||
|
||||
const onEmoji = (emoji: Emoji) => createReaction(emoji.unicode)
|
||||
|
||||
$: parentPubkey = $parentEvent?.pubkey || replies[0]?.[4]
|
||||
$: parentProfile = deriveProfile(parentPubkey || "")
|
||||
$: parentProfileDisplay = deriveProfileDisplay(parentPubkey || "")
|
||||
@@ -119,8 +156,8 @@
|
||||
</div>
|
||||
{#if $reactions.length > 0 || $zaps.length > 0}
|
||||
<div class="ml-12 text-xs">
|
||||
{#each groupBy(e => e.content, $reactions).entries() as [content, events]}
|
||||
<Button class="flex-inline btn btn-neutral btn-xs mr-2 gap-1 rounded-full">
|
||||
{#each groupBy(e => e.content, uniqBy(e => e.pubkey + e.content, $reactions)).entries() as [content, events]}
|
||||
<Button class="flex-inline btn btn-neutral btn-xs mr-2 gap-1 rounded-full" on:click={() => onReactionClick(content, events)}>
|
||||
<span>{displayReaction(content)}</span>
|
||||
{#if events.length > 1}
|
||||
<span>{events.length}</span>
|
||||
@@ -134,9 +171,7 @@
|
||||
<button class="btn join-item btn-xs">
|
||||
<Icon icon="reply" size={4} />
|
||||
</button>
|
||||
<button class="btn join-item btn-xs">
|
||||
<Icon icon="smile-circle" size={4} />
|
||||
</button>
|
||||
<ChatMessageEmojiButton onEmoji={onEmoji} />
|
||||
<button class="btn join-item btn-xs">
|
||||
<Icon icon="menu-dots" size={4} />
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import tippy, {type Instance} from "tippy.js"
|
||||
import type {Emoji} from 'emoji-picker-element/shared'
|
||||
import {between} from '@welshman/lib'
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Tippy from "@lib/components/Tippy.svelte"
|
||||
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
|
||||
|
||||
export let onEmoji
|
||||
|
||||
const open = () => popover.show()
|
||||
|
||||
const onClick = (emoji: Emoji) => {
|
||||
onEmoji(emoji)
|
||||
popover.hide()
|
||||
}
|
||||
|
||||
const onMouseMove = ({clientX, clientY}: any) => {
|
||||
const {x, y, width, height} = popover.popper.getBoundingClientRect()
|
||||
|
||||
if (!between([x, x + width], clientX) || !between([y, y + height + 30], clientY)) {
|
||||
popover.hide()
|
||||
}
|
||||
}
|
||||
|
||||
let popover: Instance
|
||||
</script>
|
||||
|
||||
<svelte:document on:mousemove={onMouseMove} />
|
||||
|
||||
<div class="flex">
|
||||
<Button class="btn join-item btn-xs" on:click={open}>
|
||||
<Icon icon="smile-circle" size={4} />
|
||||
</Button>
|
||||
<Tippy
|
||||
bind:popover
|
||||
component={EmojiPicker}
|
||||
props={{onClick}}
|
||||
params={{
|
||||
trigger: "manual",
|
||||
interactive: true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import 'emoji-picker-element'
|
||||
import type {Emoji} from 'emoji-picker-element/shared'
|
||||
import {onMount} from 'svelte'
|
||||
|
||||
export let onClick: (emoji: Emoji) => void
|
||||
|
||||
let element: Element
|
||||
|
||||
onMount(() => {
|
||||
element.addEventListener('emoji-click', (event: any) => onClick(event.detail as Emoji))
|
||||
})
|
||||
</script>
|
||||
|
||||
<emoji-picker bind:this={element} />
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from 'svelte'
|
||||
import type {SvelteComponent, ComponentType, ComponentProps} from 'svelte'
|
||||
import tippy, {type Instance, type Props} from "tippy.js"
|
||||
|
||||
export let component: ComponentType
|
||||
export let props: ComponentProps<any> = {}
|
||||
export let params: Partial<Props> = {}
|
||||
export let popover: Instance | undefined = undefined
|
||||
export let instance: SvelteComponent | undefined = undefined
|
||||
|
||||
let element: Element
|
||||
|
||||
$: instance?.$set(props)
|
||||
|
||||
onMount(() => {
|
||||
const target = document.createElement("div")
|
||||
|
||||
popover = tippy(element, {content: target, ...params})
|
||||
|
||||
instance = new component({target, props})
|
||||
|
||||
return () => {
|
||||
popover?.destroy()
|
||||
instance?.$destroy()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div bind:this={element} />
|
||||
@@ -51,7 +51,6 @@ export const createSuggestions = (options: SuggestionsOptions) =>
|
||||
},
|
||||
render: () => {
|
||||
let popover: Instance[]
|
||||
let target: HTMLElement
|
||||
let suggestions: SvelteComponent
|
||||
|
||||
const mapProps = (props: any) => ({
|
||||
@@ -64,7 +63,7 @@ export const createSuggestions = (options: SuggestionsOptions) =>
|
||||
|
||||
return {
|
||||
onStart: props => {
|
||||
target = document.createElement("div")
|
||||
const target = document.createElement("div")
|
||||
|
||||
popover = tippy("body", {
|
||||
getReferenceClientRect: props.clientRect as any,
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
|
||||
onMount(() => {
|
||||
const kinds = [NOTE, MESSAGE, EVENT_DATE, EVENT_TIME, CLASSIFIED]
|
||||
const sub = subscribe({filters: [{kinds, since: now() - 30}], relays: [url]})
|
||||
const sub = subscribe({filters: [{kinds}], relays: [url]})
|
||||
|
||||
return () => sub.close()
|
||||
})
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
{#if type === "date"}
|
||||
<Divider>{value}</Divider>
|
||||
{:else}
|
||||
<ChatMessage event={assertEvent(value)} {showPubkey} />
|
||||
<ChatMessage {url} {room} event={assertEvent(value)} {showPubkey} />
|
||||
{/if}
|
||||
{/each}
|
||||
<p class="flex h-10 items-center justify-center py-20">
|
||||
|
||||
Reference in New Issue
Block a user