refactor: remove electron-related code

This commit is contained in:
codytseng
2024-12-21 23:20:30 +08:00
parent bed8df06e8
commit 2b1e6fe8f5
200 changed files with 2771 additions and 8432 deletions

69
src/lib/draft-event.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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))
}