mirror of
https://github.com/coracle-social/flotilla.git
synced 2025-12-10 02:47:06 +00:00
recognize reactions
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import {derived} from "svelte/store"
|
||||
import {memoize, assoc} from "@welshman/lib"
|
||||
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 {NetworkContext, Tracker} from "@welshman/net"
|
||||
import {Nip46Broker, Nip46Signer, Nip07Signer, Nip01Signer} from "@welshman/signer"
|
||||
@@ -12,12 +12,15 @@ export const DEFAULT_RELAYS = [
|
||||
"wss://groups.fiatjaf.com/",
|
||||
"wss://relay29.galaxoidlabs.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 DUFFLEPUD_URL = "https://dufflepud.onrender.com"
|
||||
|
||||
export const REACTION_KINDS = [REACTION, ZAP_RESPONSE]
|
||||
|
||||
export const repository = new Repository<HashedEvent>()
|
||||
|
||||
export const relay = new Relay(repository)
|
||||
|
||||
@@ -12,8 +12,11 @@ import {
|
||||
createEvent,
|
||||
displayProfile,
|
||||
} from "@welshman/util"
|
||||
import {PublishStatus} from "@welshman/net"
|
||||
import {pk, signer, repository, INDEXER_RELAYS} from "@app/base"
|
||||
import {
|
||||
loadOne,
|
||||
subscribe,
|
||||
getWriteRelayUrls,
|
||||
loadGroup,
|
||||
loadGroupMembership,
|
||||
@@ -98,9 +101,17 @@ export const addGroupMemberships = (newTags: string[][]) =>
|
||||
export const removeGroupMemberships = (noms: string[]) =>
|
||||
updateList(GROUPS, (tags: string[][]) => tags.filter(t => !noms.includes(t[1])))
|
||||
|
||||
export const sendJoinRequest = async (nom: string, url: string) => {
|
||||
const event = createEvent(GROUP_JOIN, {tags: [["h", nom]]})
|
||||
const result = await publishThunk(makeThunk({event, relays: [url]}))
|
||||
export const sendJoinRequest = async (nom: string, url: string): Promise<[boolean, string]> => {
|
||||
const 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."]
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
<script lang="ts">
|
||||
import twColors from "tailwindcss/colors"
|
||||
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 {deriveEvents} from "@welshman/store"
|
||||
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 {formatTimestampAsTime} from '@lib/util'
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
import {repository} from '@app/base'
|
||||
import type {PublishStatusData} from "@app/state"
|
||||
import {deriveProfile, deriveProfileDisplay, deriveEvent, publishStatusData} from "@app/state"
|
||||
|
||||
@@ -38,6 +42,8 @@
|
||||
|
||||
const profile = deriveProfile(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 parentId = replies[0]?.[1]
|
||||
const parentHints = [replies[0]?.[2]].filter(Boolean)
|
||||
@@ -45,6 +51,12 @@
|
||||
const [colorName, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
|
||||
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[]) =>
|
||||
$ps.find(({status}) => statuses.includes(status))
|
||||
|
||||
@@ -52,7 +64,7 @@
|
||||
$: parentProfile = deriveProfile(parentPubkey)
|
||||
$: parentProfileDisplay = deriveProfileDisplay(parentPubkey)
|
||||
$: 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])
|
||||
</script>
|
||||
|
||||
@@ -78,7 +90,10 @@
|
||||
{/if}
|
||||
<div class="-mt-1">
|
||||
{#if showPubkey}
|
||||
<div class="flex gap-2 items-center">
|
||||
<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}
|
||||
<p class="text-sm">
|
||||
{event.content}
|
||||
@@ -98,6 +113,18 @@
|
||||
</p>
|
||||
</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
|
||||
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">
|
||||
|
||||
@@ -27,13 +27,10 @@
|
||||
|
||||
const tryJoin = async () => {
|
||||
for (const url of urls) {
|
||||
const {status, message} = await sendJoinRequest(nom, url)
|
||||
const [ok, message] = await sendJoinRequest(nom, url)
|
||||
|
||||
if (status !== PublishStatus.Success) {
|
||||
return pushToast({
|
||||
theme: 'error',
|
||||
message: `Failed to join relay: ${message || status}`,
|
||||
})
|
||||
if (!ok) {
|
||||
return pushToast({theme: 'error', message})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {get, writable, readable, derived} from "svelte/store"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import {
|
||||
max,
|
||||
first,
|
||||
append,
|
||||
between,
|
||||
uniqBy,
|
||||
@@ -49,7 +50,7 @@ import {decrypt, stamp, own, hash} from "@welshman/signer"
|
||||
import {custom, deriveEvents, deriveEventsMapped, getter, withGetter} from "@welshman/store"
|
||||
import {createSearch} from "@lib/util"
|
||||
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
|
||||
|
||||
@@ -207,6 +208,9 @@ export const load = (request: SubscribeRequest) =>
|
||||
sub.emitter.on("complete", () => resolve(events))
|
||||
})
|
||||
|
||||
export const loadOne = async (request: SubscribeRequest) =>
|
||||
first(await load(request))
|
||||
|
||||
// Publish status
|
||||
|
||||
export type PublishStatusData = {
|
||||
@@ -685,7 +689,7 @@ export type GroupMessage = {
|
||||
export const readGroupMessage = (event: TrustedEvent): Maybe<GroupMessage> => {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
on:click
|
||||
class={cx(
|
||||
$$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:bg-base-100={active}>
|
||||
@@ -48,7 +48,7 @@
|
||||
on:click
|
||||
class={cx(
|
||||
$$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:bg-base-100={active}>
|
||||
|
||||
@@ -33,6 +33,7 @@ export const synced = <T>(key: string, defaultValue: T, delay = 300) => {
|
||||
}
|
||||
|
||||
export type SearchOptions<V, T> = {
|
||||
|
||||
getValue: (item: T) => V
|
||||
fuseOptions?: IFuseOptions<T>
|
||||
sortFn?: (items: FuseResult<T>) => any
|
||||
@@ -95,3 +96,11 @@ export const formatTimestampAsDate = (ts: number) => {
|
||||
|
||||
return formatter.format(secondsToDate(ts))
|
||||
}
|
||||
|
||||
export const formatTimestampAsTime = (ts: number) => {
|
||||
const formatter = new Intl.DateTimeFormat(getLocale(), {
|
||||
timeStyle: "short",
|
||||
})
|
||||
|
||||
return formatter.format(secondsToDate(ts))
|
||||
}
|
||||
|
||||
@@ -70,7 +70,9 @@
|
||||
<div class="badge badge-neutral">{displayRelayUrl(url)}</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if group.about}
|
||||
<p class="py-4 text-sm">{group.about}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
</Masonry>
|
||||
|
||||
Reference in New Issue
Block a user