recognize reactions

This commit is contained in:
Jon Staab
2024-08-23 14:42:50 -07:00
parent f12e7ef77c
commit 6722bd8493
8 changed files with 73 additions and 20 deletions

View File

@@ -1,7 +1,7 @@
import {derived} from "svelte/store" import {derived} from "svelte/store"
import {memoize, assoc} from "@welshman/lib" import {memoize, assoc} from "@welshman/lib"
import type {TrustedEvent, HashedEvent} from "@welshman/util" import type {TrustedEvent, HashedEvent} from "@welshman/util"
import {Repository, createEvent, Relay} from "@welshman/util" import {Repository, createEvent, Relay, REACTION, ZAP_RESPONSE} from "@welshman/util"
import {withGetter} from "@welshman/store" import {withGetter} from "@welshman/store"
import {NetworkContext, Tracker} from "@welshman/net" import {NetworkContext, Tracker} from "@welshman/net"
import {Nip46Broker, Nip46Signer, Nip07Signer, Nip01Signer} from "@welshman/signer" import {Nip46Broker, Nip46Signer, Nip07Signer, Nip01Signer} from "@welshman/signer"
@@ -12,12 +12,15 @@ export const DEFAULT_RELAYS = [
"wss://groups.fiatjaf.com/", "wss://groups.fiatjaf.com/",
"wss://relay29.galaxoidlabs.com/", "wss://relay29.galaxoidlabs.com/",
"wss://devrelay.highlighter.com/", "wss://devrelay.highlighter.com/",
"wss://relay.groups.nip29.com/",
] ]
export const INDEXER_RELAYS = ["wss://purplepag.es/", "wss://relay.damus.io/", "wss://nos.lol/"] export const INDEXER_RELAYS = ["wss://purplepag.es/", "wss://relay.damus.io/", "wss://nos.lol/"]
export const DUFFLEPUD_URL = "https://dufflepud.onrender.com" export const DUFFLEPUD_URL = "https://dufflepud.onrender.com"
export const REACTION_KINDS = [REACTION, ZAP_RESPONSE]
export const repository = new Repository<HashedEvent>() export const repository = new Repository<HashedEvent>()
export const relay = new Relay(repository) export const relay = new Relay(repository)

View File

@@ -12,8 +12,11 @@ import {
createEvent, createEvent,
displayProfile, displayProfile,
} from "@welshman/util" } from "@welshman/util"
import {PublishStatus} from "@welshman/net"
import {pk, signer, repository, INDEXER_RELAYS} from "@app/base" import {pk, signer, repository, INDEXER_RELAYS} from "@app/base"
import { import {
loadOne,
subscribe,
getWriteRelayUrls, getWriteRelayUrls,
loadGroup, loadGroup,
loadGroupMembership, loadGroupMembership,
@@ -98,9 +101,17 @@ export const addGroupMemberships = (newTags: string[][]) =>
export const removeGroupMemberships = (noms: string[]) => export const removeGroupMemberships = (noms: string[]) =>
updateList(GROUPS, (tags: string[][]) => tags.filter(t => !noms.includes(t[1]))) updateList(GROUPS, (tags: string[][]) => tags.filter(t => !noms.includes(t[1])))
export const sendJoinRequest = async (nom: string, url: string) => { export const sendJoinRequest = async (nom: string, url: string): Promise<[boolean, string]> => {
const event = createEvent(GROUP_JOIN, {tags: [["h", nom]]}) const relays = [url]
const result = await publishThunk(makeThunk({event, relays: [url]})) const filters = [{kinds: [9000], '#h': [nom], '#p': [pk.get()!], since: now() - 30}]
return result[url] const event = createEvent(GROUP_JOIN, {tags: [["h", nom]]})
const statusData = await publishThunk(makeThunk({event, relays}))
const {status, message} = statusData[url]
if (message.includes('already a member')) return [true, ""]
if (status !== PublishStatus.Success) return [false, message]
if (await loadOne({filters, relays})) return [true, ""]
return [false, "Your request was not automatically approved, but may be approved manually later. Please try again later or contact the group admin."]
} }

View File

@@ -1,13 +1,17 @@
<script lang="ts"> <script lang="ts">
import twColors from "tailwindcss/colors" import twColors from "tailwindcss/colors"
import {readable, derived} from "svelte/store" import {readable, derived} from "svelte/store"
import {hash} from "@welshman/lib" import {hash, groupBy, now} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {deriveEvents} from "@welshman/store"
import {PublishStatus} from "@welshman/net" import {PublishStatus} from "@welshman/net"
import {GROUP_REPLY, displayRelayUrl, getAncestorTags, displayPubkey} from "@welshman/util" import {GROUP_REPLY, REACTION, ZAP_RESPONSE, displayRelayUrl, getAncestorTags, displayPubkey} from "@welshman/util"
import {fly, fade} from "@lib/transition" import {fly, fade} from "@lib/transition"
import {formatTimestampAsTime} from '@lib/util'
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Avatar from "@lib/components/Avatar.svelte" import Avatar from "@lib/components/Avatar.svelte"
import {repository} from '@app/base'
import type {PublishStatusData} from "@app/state" import type {PublishStatusData} from "@app/state"
import {deriveProfile, deriveProfileDisplay, deriveEvent, publishStatusData} from "@app/state" import {deriveProfile, deriveProfileDisplay, deriveEvent, publishStatusData} from "@app/state"
@@ -38,6 +42,8 @@
const profile = deriveProfile(event.pubkey) const profile = deriveProfile(event.pubkey)
const profileDisplay = deriveProfileDisplay(event.pubkey) const profileDisplay = deriveProfileDisplay(event.pubkey)
const reactions = deriveEvents(repository, {filters: [{kinds: [REACTION], '#e': [event.id]}]})
const zaps = deriveEvents(repository, {filters: [{kinds: [ZAP_RESPONSE], '#e': [event.id]}]})
const {replies} = getAncestorTags(event.tags) const {replies} = getAncestorTags(event.tags)
const parentId = replies[0]?.[1] const parentId = replies[0]?.[1]
const parentHints = [replies[0]?.[2]].filter(Boolean) const parentHints = [replies[0]?.[2]].filter(Boolean)
@@ -45,6 +51,12 @@
const [colorName, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length] const [colorName, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
const ps = derived(publishStatusData, $m => Object.values($m[event.id] || {})) const ps = derived(publishStatusData, $m => Object.values($m[event.id] || {}))
const displayReaction = (content: string) => {
if (content === '+') return "❤️"
if (content === '-') return "👎"
return content
}
const findStatus = ($ps: PublishStatusData[], statuses: PublishStatus[]) => const findStatus = ($ps: PublishStatusData[], statuses: PublishStatus[]) =>
$ps.find(({status}) => statuses.includes(status)) $ps.find(({status}) => statuses.includes(status))
@@ -52,7 +64,7 @@
$: parentProfile = deriveProfile(parentPubkey) $: parentProfile = deriveProfile(parentPubkey)
$: parentProfileDisplay = deriveProfileDisplay(parentPubkey) $: parentProfileDisplay = deriveProfileDisplay(parentPubkey)
$: isPublished = findStatus($ps, [PublishStatus.Success]) $: isPublished = findStatus($ps, [PublishStatus.Success])
$: isPending = findStatus($ps, [PublishStatus.Pending]) $: isPending = findStatus($ps, [PublishStatus.Pending]) && event.created_at > now() - 30
$: failure = !isPending && !isPublished && findStatus($ps, [PublishStatus.Failure, PublishStatus.Timeout]) $: failure = !isPending && !isPublished && findStatus($ps, [PublishStatus.Failure, PublishStatus.Timeout])
</script> </script>
@@ -78,7 +90,10 @@
{/if} {/if}
<div class="-mt-1"> <div class="-mt-1">
{#if showPubkey} {#if showPubkey}
<div class="flex gap-2 items-center">
<strong class="text-sm" style="color: {colorValue}" data-color={colorName}>{$profileDisplay}</strong> <strong class="text-sm" style="color: {colorValue}" data-color={colorName}>{$profileDisplay}</strong>
<span class="opacity-50 text-xs">{formatTimestampAsTime(event.created_at)}</span>
</div>
{/if} {/if}
<p class="text-sm"> <p class="text-sm">
{event.content} {event.content}
@@ -98,6 +113,18 @@
</p> </p>
</div> </div>
</div> </div>
{#if $reactions.length > 0 || $zaps.length > 0}
<div class="text-xs ml-12">
{#each groupBy(e => e.content, $reactions).entries() as [content, events]}
<Button class="btn btn-neutral btn-xs rounded-full mr-2 flex-inline gap-1">
<span>{displayReaction(content)}</span>
{#if events.length > 1}
<span>{events.length}</span>
{/if}
</Button>
{/each}
</div>
{/if}
<div <div
class="join absolute -top-2 right-0 border border-solid border-neutral text-xs opacity-0 transition-all group-hover:opacity-100"> class="join absolute -top-2 right-0 border border-solid border-neutral text-xs opacity-0 transition-all group-hover:opacity-100">
<button class="btn join-item btn-xs"> <button class="btn join-item btn-xs">

View File

@@ -27,13 +27,10 @@
const tryJoin = async () => { const tryJoin = async () => {
for (const url of urls) { for (const url of urls) {
const {status, message} = await sendJoinRequest(nom, url) const [ok, message] = await sendJoinRequest(nom, url)
if (status !== PublishStatus.Success) { if (!ok) {
return pushToast({ return pushToast({theme: 'error', message})
theme: 'error',
message: `Failed to join relay: ${message || status}`,
})
} }
} }

View File

@@ -5,6 +5,7 @@ import {get, writable, readable, derived} from "svelte/store"
import type {Maybe} from "@welshman/lib" import type {Maybe} from "@welshman/lib"
import { import {
max, max,
first,
append, append,
between, between,
uniqBy, uniqBy,
@@ -49,7 +50,7 @@ import {decrypt, stamp, own, hash} from "@welshman/signer"
import {custom, deriveEvents, deriveEventsMapped, getter, withGetter} from "@welshman/store" import {custom, deriveEvents, deriveEventsMapped, getter, withGetter} from "@welshman/store"
import {createSearch} from "@lib/util" import {createSearch} from "@lib/util"
import type {Handle, Relay} from "@app/types" import type {Handle, Relay} from "@app/types"
import {INDEXER_RELAYS, DUFFLEPUD_URL, repository, pk, getSession, getSigner} from "@app/base" import {INDEXER_RELAYS, DUFFLEPUD_URL, repository, pk, getSession, getSigner, REACTION_KINDS} from "@app/base"
// Utils // Utils
@@ -207,6 +208,9 @@ export const load = (request: SubscribeRequest) =>
sub.emitter.on("complete", () => resolve(events)) sub.emitter.on("complete", () => resolve(events))
}) })
export const loadOne = async (request: SubscribeRequest) =>
first(await load(request))
// Publish status // Publish status
export type PublishStatusData = { export type PublishStatusData = {
@@ -685,7 +689,7 @@ export type GroupMessage = {
export const readGroupMessage = (event: TrustedEvent): Maybe<GroupMessage> => { export const readGroupMessage = (event: TrustedEvent): Maybe<GroupMessage> => {
const nom = event.tags.find(nthEq(0, "h"))?.[1] const nom = event.tags.find(nthEq(0, "h"))?.[1]
if (!nom || between(GROUP_ADD_USER - 1, GROUP_JOIN + 1, event.kind)) { if (!nom || between(GROUP_ADD_USER - 1, GROUP_JOIN + 1, event.kind) || REACTION_KINDS.includes(event.kind)) {
return undefined return undefined
} }

View File

@@ -36,7 +36,7 @@
on:click on:click
class={cx( class={cx(
$$props.class, $$props.class,
"flex items-center gap-3 transition-all hover:bg-base-100 hover:text-base-content", "flex items-center gap-3 transition-all hover:bg-base-100 hover:text-base-content text-left",
)} )}
class:text-base-content={active} class:text-base-content={active}
class:bg-base-100={active}> class:bg-base-100={active}>
@@ -48,7 +48,7 @@
on:click on:click
class={cx( class={cx(
$$props.class, $$props.class,
"flex w-full items-center gap-3 transition-all hover:bg-base-100 hover:text-base-content", "flex w-full items-center gap-3 transition-all hover:bg-base-100 hover:text-base-content text-left",
)} )}
class:text-base-content={active} class:text-base-content={active}
class:bg-base-100={active}> class:bg-base-100={active}>

View File

@@ -33,6 +33,7 @@ export const synced = <T>(key: string, defaultValue: T, delay = 300) => {
} }
export type SearchOptions<V, T> = { export type SearchOptions<V, T> = {
getValue: (item: T) => V getValue: (item: T) => V
fuseOptions?: IFuseOptions<T> fuseOptions?: IFuseOptions<T>
sortFn?: (items: FuseResult<T>) => any sortFn?: (items: FuseResult<T>) => any
@@ -95,3 +96,11 @@ export const formatTimestampAsDate = (ts: number) => {
return formatter.format(secondsToDate(ts)) return formatter.format(secondsToDate(ts))
} }
export const formatTimestampAsTime = (ts: number) => {
const formatter = new Intl.DateTimeFormat(getLocale(), {
timeStyle: "short",
})
return formatter.format(secondsToDate(ts))
}

View File

@@ -70,7 +70,9 @@
<div class="badge badge-neutral">{displayRelayUrl(url)}</div> <div class="badge badge-neutral">{displayRelayUrl(url)}</div>
{/each} {/each}
</div> </div>
{#if group.about}
<p class="py-4 text-sm">{group.about}</p> <p class="py-4 text-sm">{group.about}</p>
{/if}
</div> </div>
</a> </a>
</Masonry> </Masonry>