Add emoji reactions

This commit is contained in:
Jon Staab
2024-09-25 13:46:10 -07:00
parent 0b8f80ed0e
commit ce733e5743
10 changed files with 145 additions and 16 deletions
+2 -4
View File
@@ -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
+6
View File
@@ -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",
+1
View File
@@ -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",
+43 -8
View File
@@ -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>
+15
View File
@@ -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} />
+30
View File
@@ -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} />
+1 -2
View File
@@ -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,
+1 -1
View File
@@ -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">