push(toNoteList({ externalContentId: value }))}
+ onClick={() => push(toExternalContent(value))}
>
{value}
@@ -184,5 +184,5 @@ function isConsecutive(rootEvent?: Event, parentEvent?: Event) {
const tag = getParentTag(parentEvent)
if (!tag) return false
- return getEventKey(rootEvent) === getEventKeyFromTag(tag.tag)
+ return getEventKey(rootEvent) === getKeyFromTag(tag.tag)
}
diff --git a/src/providers/NostrProvider/index.tsx b/src/providers/NostrProvider/index.tsx
index 47bf336..6407f0a 100644
--- a/src/providers/NostrProvider/index.tsx
+++ b/src/providers/NostrProvider/index.tsx
@@ -19,7 +19,7 @@ import client from '@/services/client.service'
import customEmojiService from '@/services/custom-emoji.service'
import indexedDb from '@/services/indexed-db.service'
import storage from '@/services/local-storage.service'
-import noteStatsService from '@/services/note-stats.service'
+import stuffStatsService from '@/services/stuff-stats.service'
import {
ISigner,
TAccount,
@@ -369,7 +369,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
limit: 100
}
])
- noteStatsService.updateNoteStatsByEvents(events)
+ stuffStatsService.updateStuffStatsByEvents(events)
}
initInteractions()
}, [account])
diff --git a/src/providers/ReplyProvider.tsx b/src/providers/ReplyProvider.tsx
index f20ec85..16eede9 100644
--- a/src/providers/ReplyProvider.tsx
+++ b/src/providers/ReplyProvider.tsx
@@ -1,4 +1,4 @@
-import { getEventKey, getEventKeyFromTag, getParentTag } from '@/lib/event'
+import { getEventKey, getKeyFromTag, getParentTag } from '@/lib/event'
import { Event } from 'nostr-tools'
import { createContext, useCallback, useContext, useState } from 'react'
@@ -32,7 +32,7 @@ export function ReplyProvider({ children }: { children: React.ReactNode }) {
const parentTag = getParentTag(reply)
if (parentTag) {
- const parentKey = getEventKeyFromTag(parentTag.tag)
+ const parentKey = getKeyFromTag(parentTag.tag)
if (parentKey) {
newReplyEventMap.set(parentKey, [...(newReplyEventMap.get(parentKey) || []), reply])
}
diff --git a/src/providers/ThemeProvider.tsx b/src/providers/ThemeProvider.tsx
index 4efbba9..a74b879 100644
--- a/src/providers/ThemeProvider.tsx
+++ b/src/providers/ThemeProvider.tsx
@@ -4,6 +4,7 @@ import { TTheme, TThemeSetting } from '@/types'
import { createContext, useContext, useEffect, useState } from 'react'
type ThemeProviderState = {
+ theme: TTheme
themeSetting: TThemeSetting
setThemeSetting: (themeSetting: TThemeSetting) => void
primaryColor: TPrimaryColor
@@ -83,6 +84,7 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
return (
},
{ path: '/relays/:url/reviews', element:
},
{ path: '/search', element:
},
+ { path: '/external-content', element:
},
{ path: '/settings', element:
},
{ path: '/settings/relays', element:
},
{ path: '/settings/wallet', element:
},
diff --git a/src/services/client.service.ts b/src/services/client.service.ts
index 6be6796..868da3f 100644
--- a/src/services/client.service.ts
+++ b/src/services/client.service.ts
@@ -98,11 +98,11 @@ class ClientService extends EventTarget {
}
}
- let relays: string[]
+ const relaySet = new Set
()
if (specifiedRelayUrls?.length) {
- relays = specifiedRelayUrls
+ specifiedRelayUrls.forEach((url) => relaySet.add(url))
} else {
- const _additionalRelayUrls: string[] = additionalRelayUrls ?? []
+ additionalRelayUrls?.forEach((url) => relaySet.add(url))
if (!specifiedRelayUrls?.length && ![kinds.Contacts, kinds.Mutelist].includes(event.kind)) {
const mentions: string[] = []
event.tags.forEach(([tagName, tagValue]) => {
@@ -118,10 +118,14 @@ class ClientService extends EventTarget {
if (mentions.length > 0) {
const relayLists = await this.fetchRelayLists(mentions)
relayLists.forEach((relayList) => {
- _additionalRelayUrls.push(...relayList.read.slice(0, 4))
+ relayList.read.slice(0, 4).forEach((url) => relaySet.add(url))
})
}
}
+
+ const relayList = await this.fetchRelayList(event.pubkey)
+ relayList.write.forEach((url) => relaySet.add(url))
+
if (
[
kinds.RelayList,
@@ -131,20 +135,23 @@ class ClientService extends EventTarget {
ExtendedKind.RELAY_REVIEW
].includes(event.kind)
) {
- _additionalRelayUrls.push(...BIG_RELAY_URLS)
+ BIG_RELAY_URLS.forEach((url) => relaySet.add(url))
}
- const relayList = await this.fetchRelayList(event.pubkey)
- relays = (relayList?.write.slice(0, 10) ?? []).concat(
- Array.from(new Set(_additionalRelayUrls)) ?? []
- )
+ if (event.kind === ExtendedKind.COMMENT) {
+ const rootITag = event.tags.find(tagNameEquals('I'))
+ if (rootITag) {
+ // For external content comments, always publish to big relays
+ BIG_RELAY_URLS.forEach((url) => relaySet.add(url))
+ }
+ }
}
- if (!relays.length) {
- relays.push(...BIG_RELAY_URLS)
+ if (!relaySet.size) {
+ BIG_RELAY_URLS.forEach((url) => relaySet.add(url))
}
- return relays
+ return Array.from(relaySet)
}
async publishEvent(relayUrls: string[], event: NEvent) {
diff --git a/src/services/note-stats.service.ts b/src/services/note-stats.service.ts
deleted file mode 100644
index 9403d52..0000000
--- a/src/services/note-stats.service.ts
+++ /dev/null
@@ -1,273 +0,0 @@
-import { BIG_RELAY_URLS } from '@/constants'
-import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event'
-import { getZapInfoFromEvent } from '@/lib/event-metadata'
-import { getEmojiInfosFromEmojiTags, tagNameEquals } from '@/lib/tag'
-import client from '@/services/client.service'
-import { TEmoji } from '@/types'
-import dayjs from 'dayjs'
-import { Event, Filter, kinds } from 'nostr-tools'
-
-export type TNoteStats = {
- likeIdSet: Set
- likes: { id: string; pubkey: string; created_at: number; emoji: TEmoji | string }[]
- repostPubkeySet: Set
- reposts: { id: string; pubkey: string; created_at: number }[]
- zapPrSet: Set
- zaps: { pr: string; pubkey: string; amount: number; created_at: number; comment?: string }[]
- updatedAt?: number
-}
-
-class NoteStatsService {
- static instance: NoteStatsService
- private noteStatsMap: Map> = new Map()
- private noteStatsSubscribers = new Map void>>()
-
- constructor() {
- if (!NoteStatsService.instance) {
- NoteStatsService.instance = this
- }
- return NoteStatsService.instance
- }
-
- async fetchNoteStats(event: Event, pubkey?: string | null) {
- const oldStats = this.noteStatsMap.get(event.id)
- let since: number | undefined
- if (oldStats?.updatedAt) {
- since = oldStats.updatedAt
- }
- const [relayList, authorProfile] = await Promise.all([
- client.fetchRelayList(event.pubkey),
- client.fetchProfile(event.pubkey)
- ])
-
- const replaceableCoordinate = isReplaceableEvent(event.kind)
- ? getReplaceableCoordinateFromEvent(event)
- : undefined
-
- const filters: Filter[] = [
- {
- '#e': [event.id],
- kinds: [kinds.Reaction],
- limit: 500
- },
- {
- '#e': [event.id],
- kinds: [kinds.Repost],
- limit: 100
- }
- ]
-
- if (replaceableCoordinate) {
- filters.push(
- {
- '#a': [replaceableCoordinate],
- kinds: [kinds.Reaction],
- limit: 500
- },
- {
- '#a': [replaceableCoordinate],
- kinds: [kinds.Repost],
- limit: 100
- }
- )
- }
-
- if (authorProfile?.lightningAddress) {
- filters.push({
- '#e': [event.id],
- kinds: [kinds.Zap],
- limit: 500
- })
-
- if (replaceableCoordinate) {
- filters.push({
- '#a': [replaceableCoordinate],
- kinds: [kinds.Zap],
- limit: 500
- })
- }
- }
-
- if (pubkey) {
- filters.push({
- '#e': [event.id],
- authors: [pubkey],
- kinds: [kinds.Reaction, kinds.Repost]
- })
-
- if (replaceableCoordinate) {
- filters.push({
- '#a': [replaceableCoordinate],
- authors: [pubkey],
- kinds: [kinds.Reaction, kinds.Repost]
- })
- }
-
- if (authorProfile?.lightningAddress) {
- filters.push({
- '#e': [event.id],
- '#P': [pubkey],
- kinds: [kinds.Zap]
- })
-
- if (replaceableCoordinate) {
- filters.push({
- '#a': [replaceableCoordinate],
- '#P': [pubkey],
- kinds: [kinds.Zap]
- })
- }
- }
- }
-
- if (since) {
- filters.forEach((filter) => {
- filter.since = since
- })
- }
- const events: Event[] = []
- await client.fetchEvents(relayList.read.concat(BIG_RELAY_URLS).slice(0, 5), filters, {
- onevent: (evt) => {
- this.updateNoteStatsByEvents([evt])
- events.push(evt)
- }
- })
- this.noteStatsMap.set(event.id, {
- ...(this.noteStatsMap.get(event.id) ?? {}),
- updatedAt: dayjs().unix()
- })
- return this.noteStatsMap.get(event.id) ?? {}
- }
-
- subscribeNoteStats(noteId: string, callback: () => void) {
- let set = this.noteStatsSubscribers.get(noteId)
- if (!set) {
- set = new Set()
- this.noteStatsSubscribers.set(noteId, set)
- }
- set.add(callback)
- return () => {
- set?.delete(callback)
- if (set?.size === 0) this.noteStatsSubscribers.delete(noteId)
- }
- }
-
- private notifyNoteStats(noteId: string) {
- const set = this.noteStatsSubscribers.get(noteId)
- if (set) {
- set.forEach((cb) => cb())
- }
- }
-
- getNoteStats(id: string): Partial | undefined {
- return this.noteStatsMap.get(id)
- }
-
- addZap(
- pubkey: string,
- eventId: string,
- pr: string,
- amount: number,
- comment?: string,
- created_at: number = dayjs().unix(),
- notify: boolean = true
- ) {
- const old = this.noteStatsMap.get(eventId) || {}
- const zapPrSet = old.zapPrSet || new Set()
- const zaps = old.zaps || []
- if (zapPrSet.has(pr)) return
-
- zapPrSet.add(pr)
- zaps.push({ pr, pubkey, amount, comment, created_at })
- this.noteStatsMap.set(eventId, { ...old, zapPrSet, zaps })
- if (notify) {
- this.notifyNoteStats(eventId)
- }
- return eventId
- }
-
- updateNoteStatsByEvents(events: Event[]) {
- const updatedEventIdSet = new Set()
- events.forEach((evt) => {
- let updatedEventId: string | undefined
- if (evt.kind === kinds.Reaction) {
- updatedEventId = this.addLikeByEvent(evt)
- } else if (evt.kind === kinds.Repost) {
- updatedEventId = this.addRepostByEvent(evt)
- } else if (evt.kind === kinds.Zap) {
- updatedEventId = this.addZapByEvent(evt)
- }
- if (updatedEventId) {
- updatedEventIdSet.add(updatedEventId)
- }
- })
- updatedEventIdSet.forEach((eventId) => {
- this.notifyNoteStats(eventId)
- })
- }
-
- private addLikeByEvent(evt: Event) {
- const targetEventId = evt.tags.findLast(tagNameEquals('e'))?.[1]
- if (!targetEventId) return
-
- const old = this.noteStatsMap.get(targetEventId) || {}
- const likeIdSet = old.likeIdSet || new Set()
- const likes = old.likes || []
- if (likeIdSet.has(evt.id)) return
-
- let emoji: TEmoji | string = evt.content.trim()
- if (!emoji) return
-
- if (emoji.startsWith(':') && emoji.endsWith(':')) {
- const emojiInfos = getEmojiInfosFromEmojiTags(evt.tags)
- const shortcode = emoji.split(':')[1]
- const emojiInfo = emojiInfos.find((info) => info.shortcode === shortcode)
- if (emojiInfo) {
- emoji = emojiInfo
- } else {
- emoji = '+'
- }
- }
-
- likeIdSet.add(evt.id)
- likes.push({ id: evt.id, pubkey: evt.pubkey, created_at: evt.created_at, emoji })
- this.noteStatsMap.set(targetEventId, { ...old, likeIdSet, likes })
- return targetEventId
- }
-
- private addRepostByEvent(evt: Event) {
- const eventId = evt.tags.find(tagNameEquals('e'))?.[1]
- if (!eventId) return
-
- const old = this.noteStatsMap.get(eventId) || {}
- const repostPubkeySet = old.repostPubkeySet || new Set()
- const reposts = old.reposts || []
- if (repostPubkeySet.has(evt.pubkey)) return
-
- repostPubkeySet.add(evt.pubkey)
- reposts.push({ id: evt.id, pubkey: evt.pubkey, created_at: evt.created_at })
- this.noteStatsMap.set(eventId, { ...old, repostPubkeySet, reposts })
- return eventId
- }
-
- private addZapByEvent(evt: Event) {
- const info = getZapInfoFromEvent(evt)
- if (!info) return
- const { originalEventId, senderPubkey, invoice, amount, comment } = info
- if (!originalEventId || !senderPubkey) return
-
- return this.addZap(
- senderPubkey,
- originalEventId,
- invoice,
- amount,
- comment,
- evt.created_at,
- false
- )
- }
-}
-
-const instance = new NoteStatsService()
-
-export default instance
diff --git a/src/services/post-editor-cache.service.ts b/src/services/post-editor-cache.service.ts
index f2ac915..515180b 100644
--- a/src/services/post-editor-cache.service.ts
+++ b/src/services/post-editor-cache.service.ts
@@ -24,49 +24,53 @@ class PostEditorCacheService {
getPostContentCache({
defaultContent,
- parentEvent
- }: { defaultContent?: string; parentEvent?: Event } = {}) {
+ parentStuff
+ }: { defaultContent?: string; parentStuff?: Event | string } = {}) {
return (
- this.postContentCache.get(this.generateCacheKey(defaultContent, parentEvent)) ??
+ this.postContentCache.get(this.generateCacheKey(defaultContent, parentStuff)) ??
defaultContent
)
}
setPostContentCache(
- { defaultContent, parentEvent }: { defaultContent?: string; parentEvent?: Event },
+ { defaultContent, parentStuff }: { defaultContent?: string; parentStuff?: Event | string },
content: Content
) {
- this.postContentCache.set(this.generateCacheKey(defaultContent, parentEvent), content)
+ this.postContentCache.set(this.generateCacheKey(defaultContent, parentStuff), content)
}
getPostSettingsCache({
defaultContent,
- parentEvent
- }: { defaultContent?: string; parentEvent?: Event } = {}): TPostSettings | undefined {
- return this.postSettingsCache.get(this.generateCacheKey(defaultContent, parentEvent))
+ parentStuff
+ }: { defaultContent?: string; parentStuff?: Event | string } = {}): TPostSettings | undefined {
+ return this.postSettingsCache.get(this.generateCacheKey(defaultContent, parentStuff))
}
setPostSettingsCache(
- { defaultContent, parentEvent }: { defaultContent?: string; parentEvent?: Event },
+ { defaultContent, parentStuff }: { defaultContent?: string; parentStuff?: Event | string },
settings: TPostSettings
) {
- this.postSettingsCache.set(this.generateCacheKey(defaultContent, parentEvent), settings)
+ this.postSettingsCache.set(this.generateCacheKey(defaultContent, parentStuff), settings)
}
clearPostCache({
defaultContent,
- parentEvent
+ parentStuff
}: {
defaultContent?: string
- parentEvent?: Event
+ parentStuff?: Event | string
}) {
- const cacheKey = this.generateCacheKey(defaultContent, parentEvent)
+ const cacheKey = this.generateCacheKey(defaultContent, parentStuff)
this.postContentCache.delete(cacheKey)
this.postSettingsCache.delete(cacheKey)
}
- generateCacheKey(defaultContent: string = '', parentEvent?: Event): string {
- return parentEvent ? parentEvent.id : defaultContent
+ generateCacheKey(defaultContent: string = '', parentStuff?: Event | string): string {
+ return parentStuff
+ ? typeof parentStuff === 'string'
+ ? parentStuff
+ : parentStuff.id
+ : defaultContent
}
}
diff --git a/src/services/stuff-stats.service.ts b/src/services/stuff-stats.service.ts
new file mode 100644
index 0000000..9a057e2
--- /dev/null
+++ b/src/services/stuff-stats.service.ts
@@ -0,0 +1,328 @@
+import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
+import { getEventKey, getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event'
+import { getZapInfoFromEvent } from '@/lib/event-metadata'
+import { getEmojiInfosFromEmojiTags, tagNameEquals } from '@/lib/tag'
+import client from '@/services/client.service'
+import { TEmoji } from '@/types'
+import dayjs from 'dayjs'
+import { Event, Filter, kinds } from 'nostr-tools'
+
+export type TStuffStats = {
+ likeIdSet: Set
+ likes: { id: string; pubkey: string; created_at: number; emoji: TEmoji | string }[]
+ repostPubkeySet: Set
+ reposts: { id: string; pubkey: string; created_at: number }[]
+ zapPrSet: Set
+ zaps: { pr: string; pubkey: string; amount: number; created_at: number; comment?: string }[]
+ updatedAt?: number
+}
+
+class StuffStatsService {
+ static instance: StuffStatsService
+ private stuffStatsMap: Map> = new Map()
+ private stuffStatsSubscribers = new Map void>>()
+
+ constructor() {
+ if (!StuffStatsService.instance) {
+ StuffStatsService.instance = this
+ }
+ return StuffStatsService.instance
+ }
+
+ async fetchStuffStats(stuff: Event | string, pubkey?: string | null) {
+ const { event, externalContent } =
+ typeof stuff === 'string'
+ ? { event: undefined, externalContent: stuff }
+ : { event: stuff, externalContent: undefined }
+ const key = event ? getEventKey(event) : externalContent
+ const oldStats = this.stuffStatsMap.get(key)
+ let since: number | undefined
+ if (oldStats?.updatedAt) {
+ since = oldStats.updatedAt
+ }
+ const [relayList, authorProfile] = event
+ ? await Promise.all([client.fetchRelayList(event.pubkey), client.fetchProfile(event.pubkey)])
+ : []
+
+ const replaceableCoordinate =
+ event && isReplaceableEvent(event.kind) ? getReplaceableCoordinateFromEvent(event) : undefined
+
+ const filters: Filter[] = []
+
+ if (event) {
+ filters.push(
+ {
+ '#e': [event.id],
+ kinds: [kinds.Reaction],
+ limit: 500
+ },
+ {
+ '#e': [event.id],
+ kinds: [kinds.Repost],
+ limit: 100
+ }
+ )
+ } else {
+ filters.push({
+ '#i': [externalContent],
+ kinds: [ExtendedKind.EXTERNAL_CONTENT_REACTION],
+ limit: 500
+ })
+ }
+
+ if (replaceableCoordinate) {
+ filters.push(
+ {
+ '#a': [replaceableCoordinate],
+ kinds: [kinds.Reaction],
+ limit: 500
+ },
+ {
+ '#a': [replaceableCoordinate],
+ kinds: [kinds.Repost],
+ limit: 100
+ }
+ )
+ }
+
+ if (event && authorProfile?.lightningAddress) {
+ filters.push({
+ '#e': [event.id],
+ kinds: [kinds.Zap],
+ limit: 500
+ })
+
+ if (replaceableCoordinate) {
+ filters.push({
+ '#a': [replaceableCoordinate],
+ kinds: [kinds.Zap],
+ limit: 500
+ })
+ }
+ }
+
+ if (pubkey) {
+ filters.push(
+ event
+ ? {
+ '#e': [event.id],
+ authors: [pubkey],
+ kinds: [kinds.Reaction, kinds.Repost]
+ }
+ : {
+ '#i': [externalContent],
+ authors: [pubkey],
+ kinds: [ExtendedKind.EXTERNAL_CONTENT_REACTION]
+ }
+ )
+
+ if (replaceableCoordinate) {
+ filters.push({
+ '#a': [replaceableCoordinate],
+ authors: [pubkey],
+ kinds: [kinds.Reaction, kinds.Repost]
+ })
+ }
+
+ if (event && authorProfile?.lightningAddress) {
+ filters.push({
+ '#e': [event.id],
+ '#P': [pubkey],
+ kinds: [kinds.Zap]
+ })
+
+ if (replaceableCoordinate) {
+ filters.push({
+ '#a': [replaceableCoordinate],
+ '#P': [pubkey],
+ kinds: [kinds.Zap]
+ })
+ }
+ }
+ }
+
+ if (since) {
+ filters.forEach((filter) => {
+ filter.since = since
+ })
+ }
+
+ const relays = relayList ? relayList.read.concat(BIG_RELAY_URLS).slice(0, 5) : BIG_RELAY_URLS
+
+ const events: Event[] = []
+ await client.fetchEvents(relays, filters, {
+ onevent: (evt) => {
+ this.updateStuffStatsByEvents([evt])
+ events.push(evt)
+ }
+ })
+ this.stuffStatsMap.set(key, {
+ ...(this.stuffStatsMap.get(key) ?? {}),
+ updatedAt: dayjs().unix()
+ })
+ return this.stuffStatsMap.get(key) ?? {}
+ }
+
+ subscribeStuffStats(stuffKey: string, callback: () => void) {
+ let set = this.stuffStatsSubscribers.get(stuffKey)
+ if (!set) {
+ set = new Set()
+ this.stuffStatsSubscribers.set(stuffKey, set)
+ }
+ set.add(callback)
+ return () => {
+ set?.delete(callback)
+ if (set?.size === 0) this.stuffStatsSubscribers.delete(stuffKey)
+ }
+ }
+
+ private notifyStuffStats(stuffKey: string) {
+ const set = this.stuffStatsSubscribers.get(stuffKey)
+ if (set) {
+ set.forEach((cb) => cb())
+ }
+ }
+
+ getStuffStats(stuffKey: string): Partial | undefined {
+ return this.stuffStatsMap.get(stuffKey)
+ }
+
+ addZap(
+ pubkey: string,
+ eventId: string,
+ pr: string,
+ amount: number,
+ comment?: string,
+ created_at: number = dayjs().unix(),
+ notify: boolean = true
+ ) {
+ const old = this.stuffStatsMap.get(eventId) || {}
+ const zapPrSet = old.zapPrSet || new Set()
+ const zaps = old.zaps || []
+ if (zapPrSet.has(pr)) return
+
+ zapPrSet.add(pr)
+ zaps.push({ pr, pubkey, amount, comment, created_at })
+ this.stuffStatsMap.set(eventId, { ...old, zapPrSet, zaps })
+ if (notify) {
+ this.notifyStuffStats(eventId)
+ }
+ return eventId
+ }
+
+ updateStuffStatsByEvents(events: Event[]) {
+ const targetKeySet = new Set()
+ events.forEach((evt) => {
+ let targetKey: string | undefined
+ if (evt.kind === kinds.Reaction) {
+ targetKey = this.addLikeByEvent(evt)
+ } else if (evt.kind === ExtendedKind.EXTERNAL_CONTENT_REACTION) {
+ targetKey = this.addExternalContentLikeByEvent(evt)
+ } else if (evt.kind === kinds.Repost) {
+ targetKey = this.addRepostByEvent(evt)
+ } else if (evt.kind === kinds.Zap) {
+ targetKey = this.addZapByEvent(evt)
+ }
+ if (targetKey) {
+ targetKeySet.add(targetKey)
+ }
+ })
+ targetKeySet.forEach((targetKey) => {
+ this.notifyStuffStats(targetKey)
+ })
+ }
+
+ private addLikeByEvent(evt: Event) {
+ const targetEventId = evt.tags.findLast(tagNameEquals('e'))?.[1]
+ if (!targetEventId) return
+
+ const old = this.stuffStatsMap.get(targetEventId) || {}
+ const likeIdSet = old.likeIdSet || new Set()
+ const likes = old.likes || []
+ if (likeIdSet.has(evt.id)) return
+
+ let emoji: TEmoji | string = evt.content.trim()
+ if (!emoji) return
+
+ if (emoji.startsWith(':') && emoji.endsWith(':')) {
+ const emojiInfos = getEmojiInfosFromEmojiTags(evt.tags)
+ const shortcode = emoji.split(':')[1]
+ const emojiInfo = emojiInfos.find((info) => info.shortcode === shortcode)
+ if (emojiInfo) {
+ emoji = emojiInfo
+ } else {
+ emoji = '+'
+ }
+ }
+
+ likeIdSet.add(evt.id)
+ likes.push({ id: evt.id, pubkey: evt.pubkey, created_at: evt.created_at, emoji })
+ this.stuffStatsMap.set(targetEventId, { ...old, likeIdSet, likes })
+ return targetEventId
+ }
+
+ private addExternalContentLikeByEvent(evt: Event) {
+ const target = evt.tags.findLast(tagNameEquals('i'))?.[1]
+ if (!target) return
+
+ const old = this.stuffStatsMap.get(target) || {}
+ const likeIdSet = old.likeIdSet || new Set()
+ const likes = old.likes || []
+ if (likeIdSet.has(evt.id)) return
+
+ let emoji: TEmoji | string = evt.content.trim()
+ if (!emoji) return
+
+ if (emoji.startsWith(':') && emoji.endsWith(':')) {
+ const emojiInfos = getEmojiInfosFromEmojiTags(evt.tags)
+ const shortcode = emoji.split(':')[1]
+ const emojiInfo = emojiInfos.find((info) => info.shortcode === shortcode)
+ if (emojiInfo) {
+ emoji = emojiInfo
+ } else {
+ emoji = '+'
+ }
+ }
+
+ likeIdSet.add(evt.id)
+ likes.push({ id: evt.id, pubkey: evt.pubkey, created_at: evt.created_at, emoji })
+ this.stuffStatsMap.set(target, { ...old, likeIdSet, likes })
+ return target
+ }
+
+ private addRepostByEvent(evt: Event) {
+ const eventId = evt.tags.find(tagNameEquals('e'))?.[1]
+ if (!eventId) return
+
+ const old = this.stuffStatsMap.get(eventId) || {}
+ const repostPubkeySet = old.repostPubkeySet || new Set()
+ const reposts = old.reposts || []
+ if (repostPubkeySet.has(evt.pubkey)) return
+
+ repostPubkeySet.add(evt.pubkey)
+ reposts.push({ id: evt.id, pubkey: evt.pubkey, created_at: evt.created_at })
+ this.stuffStatsMap.set(eventId, { ...old, repostPubkeySet, reposts })
+ return eventId
+ }
+
+ private addZapByEvent(evt: Event) {
+ const info = getZapInfoFromEvent(evt)
+ if (!info) return
+ const { originalEventId, senderPubkey, invoice, amount, comment } = info
+ if (!originalEventId || !senderPubkey) return
+
+ return this.addZap(
+ senderPubkey,
+ originalEventId,
+ invoice,
+ amount,
+ comment,
+ evt.created_at,
+ false
+ )
+ }
+}
+
+const instance = new StuffStatsService()
+
+export default instance
diff --git a/src/types/index.d.ts b/src/types/index.d.ts
index bef490e..418a9fc 100644
--- a/src/types/index.d.ts
+++ b/src/types/index.d.ts
@@ -170,7 +170,14 @@ export type TPollCreateData = {
endsAt?: number
}
-export type TSearchType = 'profile' | 'profiles' | 'notes' | 'note' | 'hashtag' | 'relay'
+export type TSearchType =
+ | 'profile'
+ | 'profiles'
+ | 'notes'
+ | 'note'
+ | 'hashtag'
+ | 'relay'
+ | 'externalContent'
export type TSearchParams = {
type: TSearchType
diff --git a/src/types/twitter.d.ts b/src/types/twitter.d.ts
new file mode 100644
index 0000000..6db3a93
--- /dev/null
+++ b/src/types/twitter.d.ts
@@ -0,0 +1,19 @@
+declare global {
+ interface Window {
+ twttr?: {
+ widgets: {
+ createTweet: (
+ tweetId: string,
+ container: HTMLElement,
+ options?: {
+ theme?: 'light' | 'dark'
+ dnt?: boolean
+ conversation?: 'none' | 'all'
+ }
+ ) => Promise
+ }
+ }
+ }
+}
+
+export {}