refactor: remove electron-related code
This commit is contained in:
69
src/lib/draft-event.ts
Normal file
69
src/lib/draft-event.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { TDraftEvent } from '@/types'
|
||||
import dayjs from 'dayjs'
|
||||
import { Event, kinds } from 'nostr-tools'
|
||||
import { extractHashtags, extractMentions, getEventCoordinate, isReplaceable } from './event'
|
||||
|
||||
// https://github.com/nostr-protocol/nips/blob/master/25.md
|
||||
export function createReactionDraftEvent(event: Event): TDraftEvent {
|
||||
const tags = event.tags.filter((tag) => tag.length >= 2 && ['e', 'p'].includes(tag[0]))
|
||||
tags.push(['e', event.id])
|
||||
tags.push(['p', event.pubkey])
|
||||
tags.push(['k', event.kind.toString()])
|
||||
|
||||
if (isReplaceable(event.kind)) {
|
||||
tags.push(['a', getEventCoordinate(event)])
|
||||
}
|
||||
|
||||
return {
|
||||
kind: kinds.Reaction,
|
||||
content: '+',
|
||||
tags,
|
||||
created_at: dayjs().unix()
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/nostr-protocol/nips/blob/master/18.md
|
||||
export function createRepostDraftEvent(event: Event): TDraftEvent {
|
||||
const tags = [
|
||||
['e', event.id], // TODO: url
|
||||
['p', event.pubkey]
|
||||
]
|
||||
|
||||
return {
|
||||
kind: kinds.Repost,
|
||||
content: JSON.stringify(event),
|
||||
tags,
|
||||
created_at: dayjs().unix()
|
||||
}
|
||||
}
|
||||
|
||||
export async function createShortTextNoteDraftEvent(
|
||||
content: string,
|
||||
parentEvent?: Event
|
||||
): Promise<TDraftEvent> {
|
||||
const { pubkeys, otherRelatedEventIds, quoteEventIds, rootEventId, parentEventId } =
|
||||
await extractMentions(content, parentEvent)
|
||||
const hashtags = extractHashtags(content)
|
||||
|
||||
const tags = pubkeys
|
||||
.map((pubkey) => ['p', pubkey])
|
||||
.concat(otherRelatedEventIds.map((eventId) => ['e', eventId]))
|
||||
.concat(quoteEventIds.map((eventId) => ['q', eventId]))
|
||||
.concat(hashtags.map((hashtag) => ['t', hashtag]))
|
||||
.concat([['client', 'jumble']])
|
||||
|
||||
if (rootEventId) {
|
||||
tags.push(['e', rootEventId, '', 'root'])
|
||||
}
|
||||
|
||||
if (parentEventId) {
|
||||
tags.push(['e', parentEventId, '', 'reply'])
|
||||
}
|
||||
|
||||
return {
|
||||
kind: kinds.ShortTextNote,
|
||||
content,
|
||||
tags,
|
||||
created_at: dayjs().unix()
|
||||
}
|
||||
}
|
||||
125
src/lib/event.ts
Normal file
125
src/lib/event.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import client from '@/services/client.service'
|
||||
import { Event, kinds, nip19 } from 'nostr-tools'
|
||||
import { isReplyETag, isRootETag, tagNameEquals } from './tag'
|
||||
|
||||
export function isNsfwEvent(event: Event) {
|
||||
return event.tags.some(
|
||||
([tagName, tagValue]) =>
|
||||
tagName === 'content-warning' || (tagName === 't' && tagValue.toLowerCase() === 'nsfw')
|
||||
)
|
||||
}
|
||||
|
||||
export function isReplyNoteEvent(event: Event) {
|
||||
if (event.kind !== kinds.ShortTextNote) return false
|
||||
|
||||
let hasETag = false
|
||||
let hasMarker = false
|
||||
for (const [tagName, , , marker] of event.tags) {
|
||||
if (tagName !== 'e') continue
|
||||
hasETag = true
|
||||
|
||||
if (!marker) continue
|
||||
hasMarker = true
|
||||
|
||||
if (['root', 'reply'].includes(marker)) return true
|
||||
}
|
||||
return hasETag && !hasMarker
|
||||
}
|
||||
|
||||
export function getParentEventId(event?: Event) {
|
||||
return event?.tags.find(isReplyETag)?.[1]
|
||||
}
|
||||
|
||||
export function getRootEventId(event?: Event) {
|
||||
return event?.tags.find(isRootETag)?.[1]
|
||||
}
|
||||
|
||||
export function isReplaceable(kind: number) {
|
||||
return kinds.isReplaceableKind(kind) || kinds.isParameterizedReplaceableKind(kind)
|
||||
}
|
||||
|
||||
export function getEventCoordinate(event: Event) {
|
||||
const d = event.tags.find(tagNameEquals('d'))?.[1]
|
||||
return d ? `${event.kind}:${event.pubkey}:${d}` : `${event.kind}:${event.pubkey}`
|
||||
}
|
||||
|
||||
export function getSharableEventId(event: Event) {
|
||||
if (isReplaceable(event.kind)) {
|
||||
const identifier = event.tags.find(tagNameEquals('d'))?.[1] ?? ''
|
||||
return nip19.naddrEncode({ pubkey: event.pubkey, kind: event.kind, identifier })
|
||||
}
|
||||
return nip19.neventEncode({ id: event.id, author: event.pubkey, kind: event.kind })
|
||||
}
|
||||
|
||||
export async function extractMentions(content: string, parentEvent?: Event) {
|
||||
const pubkeySet = new Set<string>()
|
||||
const relatedEventIdSet = new Set<string>()
|
||||
const quoteEventIdSet = new Set<string>()
|
||||
let rootEventId: string | undefined
|
||||
let parentEventId: string | undefined
|
||||
const matches = content.match(
|
||||
/nostr:(npub1[a-z0-9]{58}|nprofile1[a-z0-9]+|note1[a-z0-9]{58}|nevent1[a-z0-9]+)/g
|
||||
)
|
||||
|
||||
for (const m of matches || []) {
|
||||
try {
|
||||
const id = m.split(':')[1]
|
||||
const { type, data } = nip19.decode(id)
|
||||
if (type === 'nprofile') {
|
||||
pubkeySet.add(data.pubkey)
|
||||
} else if (type === 'npub') {
|
||||
pubkeySet.add(data)
|
||||
} else if (['nevent', 'note', 'naddr'].includes(type)) {
|
||||
const event = await client.fetchEvent(id)
|
||||
if (event) {
|
||||
pubkeySet.add(event.pubkey)
|
||||
quoteEventIdSet.add(event.id)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
if (parentEvent) {
|
||||
relatedEventIdSet.add(parentEvent.id)
|
||||
pubkeySet.add(parentEvent.pubkey)
|
||||
parentEvent.tags.forEach((tag) => {
|
||||
if (tagNameEquals('p')(tag)) {
|
||||
pubkeySet.add(tag[1])
|
||||
} else if (isRootETag(tag)) {
|
||||
rootEventId = tag[1]
|
||||
} else if (tagNameEquals('e')(tag)) {
|
||||
relatedEventIdSet.add(tag[1])
|
||||
}
|
||||
})
|
||||
if (rootEventId || isReplyNoteEvent(parentEvent)) {
|
||||
parentEventId = parentEvent.id
|
||||
} else {
|
||||
rootEventId = parentEvent.id
|
||||
}
|
||||
}
|
||||
|
||||
if (rootEventId) relatedEventIdSet.delete(rootEventId)
|
||||
if (parentEventId) relatedEventIdSet.delete(parentEventId)
|
||||
|
||||
return {
|
||||
pubkeys: Array.from(pubkeySet),
|
||||
otherRelatedEventIds: Array.from(relatedEventIdSet),
|
||||
quoteEventIds: Array.from(quoteEventIdSet),
|
||||
rootEventId,
|
||||
parentEventId
|
||||
}
|
||||
}
|
||||
|
||||
export function extractHashtags(content: string) {
|
||||
const hashtags: string[] = []
|
||||
const matches = content.match(/#[\p{L}\p{N}\p{M}]+/gu)
|
||||
matches?.forEach((m) => {
|
||||
const hashtag = m.slice(1).toLowerCase()
|
||||
if (hashtag) {
|
||||
hashtags.push(hashtag)
|
||||
}
|
||||
})
|
||||
return hashtags
|
||||
}
|
||||
45
src/lib/link.ts
Normal file
45
src/lib/link.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Event, nip19 } from 'nostr-tools'
|
||||
|
||||
export const toHome = () => '/'
|
||||
export const toNote = (eventOrId: Event | string) => {
|
||||
if (typeof eventOrId === 'string') return `/notes/${eventOrId}`
|
||||
const nevent = nip19.neventEncode({ id: eventOrId.id, author: eventOrId.pubkey })
|
||||
return `/notes/${nevent}`
|
||||
}
|
||||
export const toNoteList = ({
|
||||
hashtag,
|
||||
search,
|
||||
relay
|
||||
}: {
|
||||
hashtag?: string
|
||||
search?: string
|
||||
relay?: string
|
||||
}) => {
|
||||
const path = '/notes'
|
||||
const query = new URLSearchParams()
|
||||
if (hashtag) query.set('t', hashtag.toLowerCase())
|
||||
if (search) query.set('s', search)
|
||||
if (relay) query.set('relay', relay)
|
||||
return `${path}?${query.toString()}`
|
||||
}
|
||||
export const toProfile = (pubkey: string) => {
|
||||
const npub = nip19.npubEncode(pubkey)
|
||||
return `/users/${npub}`
|
||||
}
|
||||
export const toProfileList = ({ search }: { search?: string }) => {
|
||||
const path = '/users'
|
||||
const query = new URLSearchParams()
|
||||
if (search) query.set('s', search)
|
||||
return `${path}?${query.toString()}`
|
||||
}
|
||||
export const toFollowingList = (pubkey: string) => {
|
||||
const npub = nip19.npubEncode(pubkey)
|
||||
return `/users/${npub}/following`
|
||||
}
|
||||
export const toRelaySettings = () => '/relay-settings'
|
||||
export const toNotifications = () => '/notifications'
|
||||
|
||||
export const toNoStrudelProfile = (id: string) => `https://nostrudel.ninja/#/u/${id}`
|
||||
export const toNoStrudelNote = (id: string) => `https://nostrudel.ninja/#/n/${id}`
|
||||
export const toNoStrudelArticle = (id: string) => `https://nostrudel.ninja/#/articles/${id}`
|
||||
export const toNoStrudelStream = (id: string) => `https://nostrudel.ninja/#/streams/${id}`
|
||||
41
src/lib/nip05.ts
Normal file
41
src/lib/nip05.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { LRUCache } from 'lru-cache'
|
||||
|
||||
type TVerifyNip05Result = {
|
||||
isVerified: boolean
|
||||
nip05Name: string
|
||||
nip05Domain: string
|
||||
}
|
||||
|
||||
const verifyNip05ResultCache = new LRUCache<string, TVerifyNip05Result>({
|
||||
max: 1000,
|
||||
fetchMethod: (key) => {
|
||||
const { nip05, pubkey } = JSON.parse(key)
|
||||
return _verifyNip05(nip05, pubkey)
|
||||
}
|
||||
})
|
||||
|
||||
async function _verifyNip05(nip05: string, pubkey: string): Promise<TVerifyNip05Result> {
|
||||
const [nip05Name, nip05Domain] = nip05?.split('@') || [undefined, undefined]
|
||||
const result = { isVerified: false, nip05Name, nip05Domain }
|
||||
if (!nip05Name || !nip05Domain || !pubkey) return result
|
||||
|
||||
try {
|
||||
const res = await fetch(`https://${nip05Domain}/.well-known/nostr.json?name=${nip05Name}`)
|
||||
const json = await res.json()
|
||||
if (json.names?.[nip05Name] === pubkey) {
|
||||
return { ...result, isVerified: true }
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export async function verifyNip05(nip05: string, pubkey: string): Promise<TVerifyNip05Result> {
|
||||
const result = await verifyNip05ResultCache.fetch(JSON.stringify({ nip05, pubkey }))
|
||||
if (result) {
|
||||
return result
|
||||
}
|
||||
const [nip05Name, nip05Domain] = nip05?.split('@') || [undefined, undefined]
|
||||
return { isVerified: false, nip05Name, nip05Domain }
|
||||
}
|
||||
91
src/lib/pubkey.ts
Normal file
91
src/lib/pubkey.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { LRUCache } from 'lru-cache'
|
||||
import { nip19 } from 'nostr-tools'
|
||||
|
||||
export function formatPubkey(pubkey: string) {
|
||||
const npub = pubkeyToNpub(pubkey)
|
||||
if (npub) {
|
||||
return formatNpub(npub)
|
||||
}
|
||||
return pubkey.slice(0, 4) + '...' + pubkey.slice(-4)
|
||||
}
|
||||
|
||||
export function formatNpub(npub: string, length = 12) {
|
||||
if (length < 12) {
|
||||
length = 12
|
||||
}
|
||||
|
||||
if (length >= 63) {
|
||||
return npub
|
||||
}
|
||||
|
||||
const prefixLength = Math.floor((length - 5) / 2) + 5
|
||||
const suffixLength = length - prefixLength
|
||||
return npub.slice(0, prefixLength) + '...' + npub.slice(-suffixLength)
|
||||
}
|
||||
|
||||
export function pubkeyToNpub(pubkey: string) {
|
||||
try {
|
||||
return nip19.npubEncode(pubkey)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function userIdToPubkey(userId: string) {
|
||||
if (userId.startsWith('npub1')) {
|
||||
const { data } = nip19.decode(userId as `npub1${string}`)
|
||||
return data
|
||||
}
|
||||
return userId
|
||||
}
|
||||
|
||||
const pubkeyImageCache = new LRUCache<string, string>({ max: 1000 })
|
||||
export function generateImageByPubkey(pubkey: string): string {
|
||||
if (pubkeyImageCache.has(pubkey)) {
|
||||
return pubkeyImageCache.get(pubkey)!
|
||||
}
|
||||
|
||||
const paddedPubkey = pubkey.padEnd(2, '0')
|
||||
|
||||
// Split into 3 parts for colors and the rest for control points
|
||||
const colors: string[] = []
|
||||
const controlPoints: string[] = []
|
||||
for (let i = 0; i < 11; i++) {
|
||||
const part = paddedPubkey.slice(i * 6, (i + 1) * 6)
|
||||
if (i < 3) {
|
||||
colors.push(`#${part}`)
|
||||
} else {
|
||||
controlPoints.push(part)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate SVG with multiple radial gradients
|
||||
const gradients = controlPoints
|
||||
.map((point, index) => {
|
||||
const cx = parseInt(point.slice(0, 2), 16) % 100
|
||||
const cy = parseInt(point.slice(2, 4), 16) % 100
|
||||
const r = (parseInt(point.slice(4, 6), 16) % 35) + 30
|
||||
const c = colors[index % (colors.length - 1)]
|
||||
|
||||
return `
|
||||
<radialGradient id="grad${index}-${pubkey}" cx="${cx}%" cy="${cy}%" r="${r}%">
|
||||
<stop offset="0%" style="stop-color:${c};stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:${c};stop-opacity:0" />
|
||||
</radialGradient>
|
||||
<rect width="100%" height="100%" fill="url(#grad${index}-${pubkey})" />
|
||||
`
|
||||
})
|
||||
.join('')
|
||||
|
||||
const image = `
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="100%" height="100%" fill="${colors[2]}" fill-opacity="0.3" />
|
||||
${gradients}
|
||||
</svg>
|
||||
`
|
||||
const imageData = `data:image/svg+xml;base64,${btoa(image)}`
|
||||
|
||||
pubkeyImageCache.set(pubkey, imageData)
|
||||
|
||||
return imageData
|
||||
}
|
||||
9
src/lib/relay.ts
Normal file
9
src/lib/relay.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { TRelayInfo } from '@/types'
|
||||
|
||||
export function checkAlgoRelay(relayInfo: TRelayInfo | undefined) {
|
||||
return relayInfo?.software === 'https://github.com/bitvora/algo-relay' // hardcode for now
|
||||
}
|
||||
|
||||
export function checkSearchRelay(relayInfo: TRelayInfo | undefined) {
|
||||
return relayInfo?.supported_nips?.includes(50)
|
||||
}
|
||||
15
src/lib/tag.ts
Normal file
15
src/lib/tag.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export function tagNameEquals(tagName: string) {
|
||||
return (tag: string[]) => tag[0] === tagName
|
||||
}
|
||||
|
||||
export function isReplyETag([tagName, , , marker]: string[]) {
|
||||
return tagName === 'e' && marker === 'reply'
|
||||
}
|
||||
|
||||
export function isRootETag([tagName, , , marker]: string[]) {
|
||||
return tagName === 'e' && marker === 'root'
|
||||
}
|
||||
|
||||
export function isMentionETag([tagName, , , marker]: string[]) {
|
||||
return tagName === 'e' && marker === 'mention'
|
||||
}
|
||||
16
src/lib/url.ts
Normal file
16
src/lib/url.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export function isWebsocketUrl(url: string): boolean {
|
||||
return /^wss?:\/\/.+$/.test(url)
|
||||
}
|
||||
|
||||
// copy from nostr-tools/utils
|
||||
export function normalizeUrl(url: string): string {
|
||||
if (url.indexOf('://') === -1) url = 'wss://' + url
|
||||
const p = new URL(url)
|
||||
p.pathname = p.pathname.replace(/\/+/g, '/')
|
||||
if (p.pathname.endsWith('/')) p.pathname = p.pathname.slice(0, -1)
|
||||
if ((p.port === '80' && p.protocol === 'ws:') || (p.port === '443' && p.protocol === 'wss:'))
|
||||
p.port = ''
|
||||
p.searchParams.sort()
|
||||
p.hash = ''
|
||||
return p.toString()
|
||||
}
|
||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
Reference in New Issue
Block a user