Compare commits
No commits in common. "e740d199f1231fda97cdc919b986896061c7c7a5" and "c9bd7ca7d729181ab4091cded86eca439766616d" have entirely different histories.
e740d199f1
...
c9bd7ca7d7
58 changed files with 155 additions and 3833 deletions
2760
package-lock.json
generated
2760
package-lock.json
generated
File diff suppressed because it is too large
Load diff
12
package.json
12
package.json
|
|
@ -16,9 +16,7 @@
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview"
|
||||||
"test": "vitest run",
|
|
||||||
"test:schemas": "vitest run schemata-validation"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
|
@ -95,14 +93,11 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.17.0",
|
||||||
"@nostrability/schemata": "^0.3.2",
|
"@types/node": "^22.10.2",
|
||||||
"@types/node": "^22.19.17",
|
|
||||||
"@types/react": "^18.3.17",
|
"@types/react": "^18.3.17",
|
||||||
"@types/react-dom": "^18.3.5",
|
"@types/react-dom": "^18.3.5",
|
||||||
"@types/uri-templates": "^0.1.34",
|
"@types/uri-templates": "^0.1.34",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"ajv": "^8.18.0",
|
|
||||||
"ajv-errors": "^3.0.0",
|
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^9.17.0",
|
"eslint": "^9.17.0",
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
|
|
@ -115,7 +110,6 @@
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
"typescript-eslint": "^8.18.1",
|
"typescript-eslint": "^8.18.1",
|
||||||
"vite": "^6.0.3",
|
"vite": "^6.0.3",
|
||||||
"vite-plugin-pwa": "^0.21.1",
|
"vite-plugin-pwa": "^0.21.1"
|
||||||
"vitest": "^3.2.4"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,481 +0,0 @@
|
||||||
/**
|
|
||||||
* Schemata validation tests for Jumble's draft event builders.
|
|
||||||
*
|
|
||||||
* Validates that every create*DraftEvent function in src/lib/draft-event.ts
|
|
||||||
* produces events conforming to @nostrability/schemata JSON schemas.
|
|
||||||
*
|
|
||||||
* Covers 21 kinds: 0, 1, 3, 5, 6, 7, 16, 17, 1111, 9802,
|
|
||||||
* 10000, 10001, 10002, 10003, 10012, 10030, 10063,
|
|
||||||
* 28934, 28936, 30002, 30078
|
|
||||||
*
|
|
||||||
* Skipped:
|
|
||||||
* - kind 1018/1068 (NIP-88) — schemata e-tag schema enforces NIP-10 markers
|
|
||||||
* but NIP-88 spec uses bare e tags (https://github.com/nostrability/schemata/issues/108)
|
|
||||||
* - kind 1984 (NIP-56) — schemata schema overly strict on p tag
|
|
||||||
* (https://github.com/nostrability/schemata/issues/107)
|
|
||||||
* - kind 31987 — no schemata schema exists yet
|
|
||||||
*/
|
|
||||||
|
|
||||||
// ── Mocks (must be before any imports that use these services) ──────────
|
|
||||||
import { vi, describe, it, expect, beforeAll } from 'vitest'
|
|
||||||
|
|
||||||
vi.mock('@/services/client.service', () => ({
|
|
||||||
default: {
|
|
||||||
getEventHint: () => 'wss://relay.example.com',
|
|
||||||
getReplaeableEventFromCache: () => undefined,
|
|
||||||
fetchEvent: vi.fn().mockResolvedValue(undefined),
|
|
||||||
fetchRelayList: vi.fn().mockResolvedValue({ read: [], write: [] })
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/services/custom-emoji.service', () => ({
|
|
||||||
default: {
|
|
||||||
getEmojiById: () => undefined
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/services/media-upload.service', () => ({
|
|
||||||
default: {
|
|
||||||
getImetaTagByUrl: () => undefined
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
// ── Imports ─────────────────────────────────────────────────────────────
|
|
||||||
import { createRequire } from 'node:module'
|
|
||||||
import Ajv from 'ajv'
|
|
||||||
import ajvErrors from 'ajv-errors'
|
|
||||||
import { kinds, type Event } from 'nostr-tools'
|
|
||||||
|
|
||||||
import {
|
|
||||||
createProfileDraftEvent,
|
|
||||||
createShortTextNoteDraftEvent,
|
|
||||||
createFollowListDraftEvent,
|
|
||||||
createDeletionRequestDraftEvent,
|
|
||||||
createRepostDraftEvent,
|
|
||||||
createReactionDraftEvent,
|
|
||||||
createCommentDraftEvent,
|
|
||||||
createHighlightDraftEvent,
|
|
||||||
createMuteListDraftEvent,
|
|
||||||
createRelayListDraftEvent,
|
|
||||||
createBookmarkDraftEvent,
|
|
||||||
createPinListDraftEvent,
|
|
||||||
createFavoriteRelaysDraftEvent,
|
|
||||||
createUserEmojiListDraftEvent,
|
|
||||||
createBlossomServerListDraftEvent,
|
|
||||||
createJoinDraftEvent,
|
|
||||||
createLeaveDraftEvent,
|
|
||||||
createSeenNotificationsAtDraftEvent,
|
|
||||||
createRelaySetDraftEvent,
|
|
||||||
createExternalContentReactionDraftEvent
|
|
||||||
} from '@/lib/draft-event'
|
|
||||||
|
|
||||||
import { ExtendedKind } from '@/constants'
|
|
||||||
|
|
||||||
// ── Schema loading ──────────────────────────────────────────────────────
|
|
||||||
const require_ = createRequire(import.meta.url)
|
|
||||||
const schemataBase = require_
|
|
||||||
.resolve('@nostrability/schemata')
|
|
||||||
.replace(/\/dist\/.*/, '/dist/nips')
|
|
||||||
|
|
||||||
function loadSchema(path: string): object {
|
|
||||||
return require_(`${schemataBase}/${path}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively strip nested $schema, $id, and errorMessage fields
|
|
||||||
* that confuse AJV's strict mode.
|
|
||||||
*/
|
|
||||||
function stripSchemaFields(obj: unknown): unknown {
|
|
||||||
if (Array.isArray(obj)) {
|
|
||||||
return obj.map(stripSchemaFields)
|
|
||||||
}
|
|
||||||
if (obj !== null && typeof obj === 'object') {
|
|
||||||
const result: Record<string, unknown> = {}
|
|
||||||
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
||||||
if (key === 'errorMessage') continue
|
|
||||||
if (key === '$schema' || key === '$id') continue
|
|
||||||
result[key] = stripSchemaFields(value)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
function createValidator(schema: object): ReturnType<Ajv['compile']> {
|
|
||||||
const cleaned = stripSchemaFields(schema) as Record<string, unknown>
|
|
||||||
const ajv = new Ajv({ allErrors: true, strict: false })
|
|
||||||
ajvErrors(ajv)
|
|
||||||
return ajv.compile(cleaned)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Schema definitions (loaded at module level) ─────────────────────────
|
|
||||||
const kind0Schema = loadSchema('nip-01/kind-0/schema.json')
|
|
||||||
const kind1Schema = loadSchema('nip-01/kind-1/schema.json')
|
|
||||||
const kind3Schema = loadSchema('nip-02/kind-3/schema.json')
|
|
||||||
const kind5Schema = loadSchema('nip-09/kind-5/schema.json')
|
|
||||||
const kind6Schema = loadSchema('nip-18/kind-6/schema.json')
|
|
||||||
const kind7Schema = loadSchema('nip-25/kind-7/schema.json')
|
|
||||||
const kind16Schema = loadSchema('nip-18/kind-16/schema.json')
|
|
||||||
const kind17Schema = loadSchema('nip-25/kind-17/schema.json')
|
|
||||||
const kind1111Schema = loadSchema('nip-22/kind-1111/schema.json')
|
|
||||||
const kind9802Schema = loadSchema('nip-84/kind-9802/schema.json')
|
|
||||||
const kind10000Schema = loadSchema('nip-51/kind-10000/schema.json')
|
|
||||||
const kind10001Schema = loadSchema('nip-51/kind-10001/schema.json')
|
|
||||||
const kind10002Schema = loadSchema('nip-65/kind-10002/schema.json')
|
|
||||||
const kind10003Schema = loadSchema('nip-51/kind-10003/schema.json')
|
|
||||||
const kind10012Schema = loadSchema('nip-51/kind-10012/schema.json')
|
|
||||||
const kind10030Schema = loadSchema('nip-51/kind-10030/schema.json')
|
|
||||||
const kind10063Schema = loadSchema('nip-b7/kind-10063/schema.json')
|
|
||||||
const kind28934Schema = loadSchema('nip-43/kind-28934/schema.json')
|
|
||||||
const kind28936Schema = loadSchema('nip-43/kind-28936/schema.json')
|
|
||||||
const kind30002Schema = loadSchema('nip-51/kind-30002/schema.json')
|
|
||||||
const kind30078Schema = loadSchema('nip-78/kind-30078/schema.json')
|
|
||||||
|
|
||||||
function buildSchemaRegistry(): Map<number, object> {
|
|
||||||
const entries: [number, object][] = [
|
|
||||||
[0, kind0Schema],
|
|
||||||
[1, kind1Schema],
|
|
||||||
[3, kind3Schema],
|
|
||||||
[5, kind5Schema],
|
|
||||||
[6, kind6Schema],
|
|
||||||
[7, kind7Schema],
|
|
||||||
[16, kind16Schema],
|
|
||||||
[17, kind17Schema],
|
|
||||||
[1111, kind1111Schema],
|
|
||||||
[9802, kind9802Schema],
|
|
||||||
[10000, kind10000Schema],
|
|
||||||
[10001, kind10001Schema],
|
|
||||||
[10002, kind10002Schema],
|
|
||||||
[10003, kind10003Schema],
|
|
||||||
[10012, kind10012Schema],
|
|
||||||
[10030, kind10030Schema],
|
|
||||||
[10063, kind10063Schema],
|
|
||||||
[28934, kind28934Schema],
|
|
||||||
[28936, kind28936Schema],
|
|
||||||
[30002, kind30002Schema],
|
|
||||||
[30078, kind30078Schema]
|
|
||||||
]
|
|
||||||
return new Map(entries)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
||||||
const FAKE_PUBKEY = '0'.repeat(64)
|
|
||||||
const FAKE_PUBKEY_2 = '1'.repeat(64)
|
|
||||||
const FAKE_ID = 'a'.repeat(64)
|
|
||||||
const FAKE_SIG = 'b'.repeat(128)
|
|
||||||
|
|
||||||
type DraftEvent = { kind: number; content: string; tags: string[][]; created_at: number }
|
|
||||||
|
|
||||||
function validateDraftEvent(
|
|
||||||
draft: DraftEvent,
|
|
||||||
schemaRegistry: Map<number, object>
|
|
||||||
): { valid: boolean; errors: string[] } {
|
|
||||||
const schema = schemaRegistry.get(draft.kind)
|
|
||||||
if (!schema) {
|
|
||||||
return { valid: false, errors: [`No schema found for kind ${draft.kind}`] }
|
|
||||||
}
|
|
||||||
|
|
||||||
const signedEvent = {
|
|
||||||
...draft,
|
|
||||||
pubkey: FAKE_PUBKEY,
|
|
||||||
id: FAKE_ID,
|
|
||||||
sig: FAKE_SIG
|
|
||||||
}
|
|
||||||
|
|
||||||
const validate = createValidator(schema)
|
|
||||||
const valid = validate(signedEvent) as boolean
|
|
||||||
const errors = valid
|
|
||||||
? []
|
|
||||||
: (validate.errors ?? []).map((e) => `${e.instancePath || '/'}: ${e.message}`)
|
|
||||||
|
|
||||||
return { valid, errors }
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Build a fake signed Event for functions that require one as input */
|
|
||||||
function makeEvent(overrides: Partial<Event> = {}): Event {
|
|
||||||
return {
|
|
||||||
id: 'c'.repeat(64),
|
|
||||||
pubkey: FAKE_PUBKEY_2,
|
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
|
||||||
kind: 1,
|
|
||||||
tags: [],
|
|
||||||
content: 'hello nostr',
|
|
||||||
sig: 'e'.repeat(128),
|
|
||||||
...overrides
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Tests ───────────────────────────────────────────────────────────────
|
|
||||||
describe('Schemata Schema Validation', () => {
|
|
||||||
let schemaRegistry: Map<number, object>
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
schemaRegistry = buildSchemaRegistry()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('schema registry has 21 kinds', () => {
|
|
||||||
expect(schemaRegistry.size).toBe(21)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Kind 0 – Profile metadata
|
|
||||||
it('kind 0 (Profile) via createProfileDraftEvent', () => {
|
|
||||||
const draft = createProfileDraftEvent(
|
|
||||||
JSON.stringify({ name: 'Alice', about: 'Test profile', picture: 'https://example.com/avatar.png' })
|
|
||||||
)
|
|
||||||
expect(draft.kind).toBe(0)
|
|
||||||
const result = validateDraftEvent(draft, schemaRegistry)
|
|
||||||
expect(result.errors).toEqual([])
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Kind 1 – Short text note
|
|
||||||
it('kind 1 (Short Text Note) via createShortTextNoteDraftEvent', async () => {
|
|
||||||
const draft = await createShortTextNoteDraftEvent('Hello, Nostr!', [])
|
|
||||||
expect(draft.kind).toBe(1)
|
|
||||||
const result = validateDraftEvent(draft, schemaRegistry)
|
|
||||||
expect(result.errors).toEqual([])
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('kind 1 (Short Text Note reply) via createShortTextNoteDraftEvent', async () => {
|
|
||||||
const parent = makeEvent()
|
|
||||||
const draft = await createShortTextNoteDraftEvent('Nice post!', [], { parentEvent: parent })
|
|
||||||
expect(draft.kind).toBe(1)
|
|
||||||
const result = validateDraftEvent(draft, schemaRegistry)
|
|
||||||
expect(result.errors).toEqual([])
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Kind 3 – Follow list
|
|
||||||
it('kind 3 (Follow List) via createFollowListDraftEvent', () => {
|
|
||||||
const tags = [
|
|
||||||
['p', 'a'.repeat(64), 'wss://relay1.example.com', 'alice'],
|
|
||||||
['p', 'b'.repeat(64), 'wss://relay2.example.com', 'bob']
|
|
||||||
]
|
|
||||||
const draft = createFollowListDraftEvent(tags)
|
|
||||||
expect(draft.kind).toBe(kinds.Contacts)
|
|
||||||
const result = validateDraftEvent(draft, schemaRegistry)
|
|
||||||
expect(result.errors).toEqual([])
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Kind 5 – Deletion request
|
|
||||||
it('kind 5 (Deletion) via createDeletionRequestDraftEvent', () => {
|
|
||||||
const event = makeEvent({ kind: 1 })
|
|
||||||
const draft = createDeletionRequestDraftEvent(event)
|
|
||||||
expect(draft.kind).toBe(kinds.EventDeletion)
|
|
||||||
const result = validateDraftEvent(draft, schemaRegistry)
|
|
||||||
expect(result.errors).toEqual([])
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Kind 6 – Repost (text note)
|
|
||||||
it('kind 6 (Repost) via createRepostDraftEvent', () => {
|
|
||||||
const event = makeEvent({ kind: 1 })
|
|
||||||
const draft = createRepostDraftEvent(event)
|
|
||||||
expect(draft.kind).toBe(kinds.Repost)
|
|
||||||
const result = validateDraftEvent(draft, schemaRegistry)
|
|
||||||
expect(result.errors).toEqual([])
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Kind 7 – Reaction
|
|
||||||
it('kind 7 (Reaction: like) via createReactionDraftEvent', () => {
|
|
||||||
const event = makeEvent()
|
|
||||||
const draft = createReactionDraftEvent(event)
|
|
||||||
expect(draft.kind).toBe(kinds.Reaction)
|
|
||||||
const result = validateDraftEvent(draft, schemaRegistry)
|
|
||||||
expect(result.errors).toEqual([])
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('kind 7 (Reaction: custom emoji) via createReactionDraftEvent', () => {
|
|
||||||
const event = makeEvent()
|
|
||||||
const draft = createReactionDraftEvent(event, '🤙')
|
|
||||||
expect(draft.kind).toBe(kinds.Reaction)
|
|
||||||
const result = validateDraftEvent(draft, schemaRegistry)
|
|
||||||
expect(result.errors).toEqual([])
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Kind 16 – Generic repost (non-text)
|
|
||||||
it('kind 16 (Generic Repost) via createRepostDraftEvent', () => {
|
|
||||||
const event = makeEvent({ kind: 30023 })
|
|
||||||
const draft = createRepostDraftEvent(event)
|
|
||||||
expect(draft.kind).toBe(kinds.GenericRepost)
|
|
||||||
const result = validateDraftEvent(draft, schemaRegistry)
|
|
||||||
expect(result.errors).toEqual([])
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Kind 17 – External content reaction
|
|
||||||
it('kind 17 (External Content Reaction) via createExternalContentReactionDraftEvent', () => {
|
|
||||||
const draft = createExternalContentReactionDraftEvent('https://example.com/article')
|
|
||||||
expect(draft.kind).toBe(ExtendedKind.EXTERNAL_CONTENT_REACTION)
|
|
||||||
const result = validateDraftEvent(draft, schemaRegistry)
|
|
||||||
expect(result.errors).toEqual([])
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('kind 17 (External Content Reaction: custom) via createExternalContentReactionDraftEvent', () => {
|
|
||||||
const draft = createExternalContentReactionDraftEvent('https://example.com/article', '🔥')
|
|
||||||
expect(draft.kind).toBe(ExtendedKind.EXTERNAL_CONTENT_REACTION)
|
|
||||||
const result = validateDraftEvent(draft, schemaRegistry)
|
|
||||||
expect(result.errors).toEqual([])
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Kind 1111 – Comment
|
|
||||||
it('kind 1111 (Comment on event) via createCommentDraftEvent', async () => {
|
|
||||||
const parent = makeEvent({ kind: 1 })
|
|
||||||
const draft = await createCommentDraftEvent('Great post!', parent, [])
|
|
||||||
expect(draft.kind).toBe(ExtendedKind.COMMENT)
|
|
||||||
const result = validateDraftEvent(draft, schemaRegistry)
|
|
||||||
expect(result.errors).toEqual([])
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('kind 1111 (Comment on external content) via createCommentDraftEvent', async () => {
|
|
||||||
const draft = await createCommentDraftEvent('Interesting article', 'https://example.com/article', [])
|
|
||||||
expect(draft.kind).toBe(ExtendedKind.COMMENT)
|
|
||||||
const result = validateDraftEvent(draft, schemaRegistry)
|
|
||||||
expect(result.errors).toEqual([])
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Kind 9802 – Highlight
|
|
||||||
it('kind 9802 (Highlight from event) via createHighlightDraftEvent', () => {
|
|
||||||
const event = makeEvent({ kind: 30023, content: 'A long article with highlighted text in it.' })
|
|
||||||
const draft = createHighlightDraftEvent('highlighted text', '', event, [])
|
|
||||||
expect(draft.kind).toBe(kinds.Highlights)
|
|
||||||
const result = validateDraftEvent(draft, schemaRegistry)
|
|
||||||
expect(result.errors).toEqual([])
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Kind 10000 – Mute list
|
|
||||||
it('kind 10000 (Mute List) via createMuteListDraftEvent', () => {
|
|
||||||
const tags = [
|
|
||||||
['p', 'a'.repeat(64)],
|
|
||||||
['e', 'b'.repeat(64)],
|
|
||||||
['word', 'spam'],
|
|
||||||
['t', 'nsfw']
|
|
||||||
]
|
|
||||||
const draft = createMuteListDraftEvent(tags)
|
|
||||||
expect(draft.kind).toBe(kinds.Mutelist)
|
|
||||||
const result = validateDraftEvent(draft, schemaRegistry)
|
|
||||||
expect(result.errors).toEqual([])
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Kind 10001 – Pin list
|
|
||||||
it('kind 10001 (Pin List) via createPinListDraftEvent', () => {
|
|
||||||
const tags = [['e', 'a'.repeat(64)], ['e', 'b'.repeat(64)]]
|
|
||||||
const draft = createPinListDraftEvent(tags)
|
|
||||||
expect(draft.kind).toBe(kinds.Pinlist)
|
|
||||||
const result = validateDraftEvent(draft, schemaRegistry)
|
|
||||||
expect(result.errors).toEqual([])
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Kind 10002 – Relay list
|
|
||||||
it('kind 10002 (Relay List) via createRelayListDraftEvent', () => {
|
|
||||||
const relays = [
|
|
||||||
{ url: 'wss://relay1.example.com', scope: 'both' as const },
|
|
||||||
{ url: 'wss://relay2.example.com', scope: 'read' as const }
|
|
||||||
]
|
|
||||||
const draft = createRelayListDraftEvent(relays)
|
|
||||||
expect(draft.kind).toBe(kinds.RelayList)
|
|
||||||
const result = validateDraftEvent(draft, schemaRegistry)
|
|
||||||
expect(result.errors).toEqual([])
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Kind 10003 – Bookmark list
|
|
||||||
it('kind 10003 (Bookmarks) via createBookmarkDraftEvent', () => {
|
|
||||||
const tags = [
|
|
||||||
['e', 'a'.repeat(64)],
|
|
||||||
['a', `30023:${'b'.repeat(64)}:my-article`],
|
|
||||||
['r', 'https://example.com']
|
|
||||||
]
|
|
||||||
const draft = createBookmarkDraftEvent(tags)
|
|
||||||
expect(draft.kind).toBe(kinds.BookmarkList)
|
|
||||||
const result = validateDraftEvent(draft, schemaRegistry)
|
|
||||||
expect(result.errors).toEqual([])
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Kind 10012 – Favorite relays
|
|
||||||
it('kind 10012 (Favorite Relays) via createFavoriteRelaysDraftEvent', () => {
|
|
||||||
const draft = createFavoriteRelaysDraftEvent(
|
|
||||||
['wss://relay1.example.com', 'wss://relay2.example.com'],
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
expect(draft.kind).toBe(ExtendedKind.FAVORITE_RELAYS)
|
|
||||||
const result = validateDraftEvent(draft, schemaRegistry)
|
|
||||||
expect(result.errors).toEqual([])
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Kind 10030 – User emoji list
|
|
||||||
it('kind 10030 (User Emoji List) via createUserEmojiListDraftEvent', () => {
|
|
||||||
const tags = [['a', `30030:${'a'.repeat(64)}:my-emojis`]]
|
|
||||||
const draft = createUserEmojiListDraftEvent(tags)
|
|
||||||
expect(draft.kind).toBe(kinds.UserEmojiList)
|
|
||||||
const result = validateDraftEvent(draft, schemaRegistry)
|
|
||||||
expect(result.errors).toEqual([])
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Kind 10063 – Blossom server list
|
|
||||||
it('kind 10063 (Blossom Server List) via createBlossomServerListDraftEvent', () => {
|
|
||||||
const draft = createBlossomServerListDraftEvent([
|
|
||||||
'https://blossom1.example.com',
|
|
||||||
'https://blossom2.example.com'
|
|
||||||
])
|
|
||||||
expect(draft.kind).toBe(ExtendedKind.BLOSSOM_SERVER_LIST)
|
|
||||||
const result = validateDraftEvent(draft, schemaRegistry)
|
|
||||||
expect(result.errors).toEqual([])
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Kind 28934 – Join request (NIP-43)
|
|
||||||
it('kind 28934 (Join) via createJoinDraftEvent', () => {
|
|
||||||
const draft = createJoinDraftEvent('invite-code-123')
|
|
||||||
expect(draft.kind).toBe(28934)
|
|
||||||
const result = validateDraftEvent(draft, schemaRegistry)
|
|
||||||
expect(result.errors).toEqual([])
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Kind 28936 – Leave request (NIP-43)
|
|
||||||
it('kind 28936 (Leave) via createLeaveDraftEvent', () => {
|
|
||||||
const draft = createLeaveDraftEvent()
|
|
||||||
expect(draft.kind).toBe(28936)
|
|
||||||
const result = validateDraftEvent(draft, schemaRegistry)
|
|
||||||
expect(result.errors).toEqual([])
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Kind 30002 – Relay set
|
|
||||||
it('kind 30002 (Relay Set) via createRelaySetDraftEvent', () => {
|
|
||||||
const draft = createRelaySetDraftEvent({
|
|
||||||
id: 'my-set',
|
|
||||||
name: 'My Relay Set',
|
|
||||||
relayUrls: ['wss://relay1.example.com', 'wss://relay2.example.com']
|
|
||||||
})
|
|
||||||
expect(draft.kind).toBe(kinds.Relaysets)
|
|
||||||
const result = validateDraftEvent(draft, schemaRegistry)
|
|
||||||
expect(result.errors).toEqual([])
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Kind 30078 – Application-specific data (seen notifications)
|
|
||||||
it('kind 30078 (Application Data) via createSeenNotificationsAtDraftEvent', () => {
|
|
||||||
const draft = createSeenNotificationsAtDraftEvent()
|
|
||||||
expect(draft.kind).toBe(kinds.Application)
|
|
||||||
const result = validateDraftEvent(draft, schemaRegistry)
|
|
||||||
expect(result.errors).toEqual([])
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Slider } from '@/components/ui/slider'
|
import { Slider } from '@/components/ui/slider'
|
||||||
import { isInsecureUrl } from '@/lib/url'
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
|
|
||||||
import mediaManager from '@/services/media-manager.service'
|
import mediaManager from '@/services/media-manager.service'
|
||||||
import { Minimize2, Pause, Play, X } from 'lucide-react'
|
import { Minimize2, Pause, Play, X } from 'lucide-react'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
@ -24,12 +22,11 @@ export default function AudioPlayer({
|
||||||
className
|
className
|
||||||
}: AudioPlayerProps) {
|
}: AudioPlayerProps) {
|
||||||
const audioRef = useRef<HTMLAudioElement>(null)
|
const audioRef = useRef<HTMLAudioElement>(null)
|
||||||
const { allowInsecureConnection } = useUserPreferences()
|
|
||||||
const [isPlaying, setIsPlaying] = useState(false)
|
const [isPlaying, setIsPlaying] = useState(false)
|
||||||
const [currentTime, setCurrentTime] = useState(0)
|
const [currentTime, setCurrentTime] = useState(0)
|
||||||
const [duration, setDuration] = useState(0)
|
const [duration, setDuration] = useState(0)
|
||||||
const [error, setError] = useState(false)
|
const [error, setError] = useState(false)
|
||||||
const seekTimeoutRef = useRef<ReturnType<typeof setTimeout>>()
|
const seekTimeoutRef = useRef<NodeJS.Timeout>()
|
||||||
const isSeeking = useRef(false)
|
const isSeeking = useRef(false)
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
|
@ -124,7 +121,7 @@ export default function AudioPlayer({
|
||||||
}, 300)
|
}, 300)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error || (!allowInsecureConnection && isInsecureUrl(src))) {
|
if (error) {
|
||||||
return <ExternalLink url={src} />
|
return <ExternalLink url={src} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,10 +65,6 @@ const clients: Record<string, { name: string; getUrl: (id: string) => string }>
|
||||||
name: 'Pareto',
|
name: 'Pareto',
|
||||||
getUrl: (id: string) => `https://pareto.space/a/${id}`
|
getUrl: (id: string) => `https://pareto.space/a/${id}`
|
||||||
},
|
},
|
||||||
shosho: {
|
|
||||||
name: 'Shosho',
|
|
||||||
getUrl: (id: string) => `https://shosho.live/live/${id}`
|
|
||||||
},
|
|
||||||
njump: {
|
njump: {
|
||||||
name: 'Njump',
|
name: 'Njump',
|
||||||
getUrl: (id: string) => `https://njump.me/${id}`
|
getUrl: (id: string) => `https://njump.me/${id}`
|
||||||
|
|
@ -111,7 +107,7 @@ export default function ClientSelect({
|
||||||
case kinds.DraftLong:
|
case kinds.DraftLong:
|
||||||
return ['yakihonne', 'coracle', 'habla', 'lumilumi', 'pareto', 'njump']
|
return ['yakihonne', 'coracle', 'habla', 'lumilumi', 'pareto', 'njump']
|
||||||
case kinds.LiveEvent:
|
case kinds.LiveEvent:
|
||||||
return ['zapStream', 'shosho', 'nostrudel', 'njump']
|
return ['zapStream', 'nostrudel', 'njump']
|
||||||
case kinds.Date:
|
case kinds.Date:
|
||||||
case kinds.Time:
|
case kinds.Time:
|
||||||
return ['coracle', 'njump']
|
return ['coracle', 'njump']
|
||||||
|
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
import Image from '@/components/Image'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import { Heart } from 'lucide-react'
|
|
||||||
import { Event } from 'nostr-tools'
|
|
||||||
import { useMemo } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
export default function ReactionPreview({
|
|
||||||
event,
|
|
||||||
className
|
|
||||||
}: {
|
|
||||||
event: Event
|
|
||||||
className?: string
|
|
||||||
}) {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const reaction = useMemo(() => {
|
|
||||||
if (!event.content || event.content === '+') {
|
|
||||||
return <Heart size={14} className="inline text-red-400" />
|
|
||||||
}
|
|
||||||
|
|
||||||
const emojiName = /^:([^:]+):$/.exec(event.content)?.[1]
|
|
||||||
if (emojiName) {
|
|
||||||
const emojiTag = event.tags.find((tag) => tag[0] === 'emoji' && tag[1] === emojiName)
|
|
||||||
const emojiUrl = emojiTag?.[2]
|
|
||||||
if (emojiUrl) {
|
|
||||||
return (
|
|
||||||
<Image
|
|
||||||
image={{ url: emojiUrl, pubkey: event.pubkey }}
|
|
||||||
alt={emojiName}
|
|
||||||
className="inline-block h-4 w-4"
|
|
||||||
classNames={{ errorPlaceholder: 'bg-transparent', wrapper: 'inline-block rounded-md' }}
|
|
||||||
errorPlaceholder={<Heart size={14} className="inline text-red-400" />}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (event.content.length > 4) {
|
|
||||||
return <Heart size={14} className="inline text-red-400" />
|
|
||||||
}
|
|
||||||
return <span>{event.content}</span>
|
|
||||||
}, [event])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn('flex items-center gap-1 truncate', className)}>
|
|
||||||
<span className="truncate">[{t('Reaction')}]</span>
|
|
||||||
{reaction}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
import { ExtendedKind } from '@/constants'
|
|
||||||
import { getVideoMetadataFromEvent } from '@/lib/event-metadata'
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { Event } from 'nostr-tools'
|
import { Event } from 'nostr-tools'
|
||||||
import { useMemo } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export default function VideoNotePreview({
|
export default function VideoNotePreview({
|
||||||
|
|
@ -13,18 +10,10 @@ export default function VideoNotePreview({
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const isAddressable =
|
|
||||||
event.kind === ExtendedKind.ADDRESSABLE_NORMAL_VIDEO ||
|
|
||||||
event.kind === ExtendedKind.ADDRESSABLE_SHORT_VIDEO
|
|
||||||
const metadata = useMemo(
|
|
||||||
() => (isAddressable ? getVideoMetadataFromEvent(event) : null),
|
|
||||||
[event, isAddressable]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('pointer-events-none', className)}>
|
<div className={cn('pointer-events-none', className)}>
|
||||||
[{t('Media')}]{' '}
|
[{t('Media')}] <span className="pr-0.5 italic">{event.content}</span>
|
||||||
<span className="pr-0.5 italic">{metadata?.title || event.content}</span>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import LongFormArticlePreview from './LongFormArticlePreview'
|
||||||
import NormalContentPreview from './NormalContentPreview'
|
import NormalContentPreview from './NormalContentPreview'
|
||||||
import PictureNotePreview from './PictureNotePreview'
|
import PictureNotePreview from './PictureNotePreview'
|
||||||
import PollPreview from './PollPreview'
|
import PollPreview from './PollPreview'
|
||||||
import ReactionPreview from './ReactionPreview'
|
|
||||||
import VideoNotePreview from './VideoNotePreview'
|
import VideoNotePreview from './VideoNotePreview'
|
||||||
|
|
||||||
export default function ContentPreview({
|
export default function ContentPreview({
|
||||||
|
|
@ -83,12 +82,7 @@ export default function ContentPreview({
|
||||||
return <LongFormArticlePreview event={event} className={className} />
|
return <LongFormArticlePreview event={event} className={className} />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (event.kind === ExtendedKind.VIDEO || event.kind === ExtendedKind.SHORT_VIDEO) {
|
||||||
event.kind === ExtendedKind.VIDEO ||
|
|
||||||
event.kind === ExtendedKind.SHORT_VIDEO ||
|
|
||||||
event.kind === ExtendedKind.ADDRESSABLE_NORMAL_VIDEO ||
|
|
||||||
event.kind === ExtendedKind.ADDRESSABLE_SHORT_VIDEO
|
|
||||||
) {
|
|
||||||
return <VideoNotePreview event={event} className={className} />
|
return <VideoNotePreview event={event} className={className} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,10 +110,6 @@ export default function ContentPreview({
|
||||||
return <FollowPackPreview event={event} className={className} />
|
return <FollowPackPreview event={event} className={className} />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.kind === kinds.Reaction || event.kind === ExtendedKind.EXTERNAL_CONTENT_REACTION) {
|
|
||||||
return <ReactionPreview event={event} className={className} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
[
|
[
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import { isInsecureUrl } from '@/lib/url'
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
|
|
||||||
import { TEmoji } from '@/types'
|
import { TEmoji } from '@/types'
|
||||||
import { Heart } from 'lucide-react'
|
import { Heart } from 'lucide-react'
|
||||||
import { HTMLAttributes, useState } from 'react'
|
import { HTMLAttributes, useState } from 'react'
|
||||||
|
|
@ -15,7 +13,6 @@ export default function Emoji({
|
||||||
img?: string
|
img?: string
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
const { allowInsecureConnection } = useUserPreferences()
|
|
||||||
const [hasError, setHasError] = useState(false)
|
const [hasError, setHasError] = useState(false)
|
||||||
|
|
||||||
if (typeof emoji === 'string') {
|
if (typeof emoji === 'string') {
|
||||||
|
|
@ -26,7 +23,7 @@ export default function Emoji({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasError || (!allowInsecureConnection && isInsecureUrl(emoji.url))) {
|
if (hasError) {
|
||||||
return (
|
return (
|
||||||
<span className={cn('whitespace-nowrap', classNames?.text)}>{`:${emoji.shortcode}:`}</span>
|
<span className={cn('whitespace-nowrap', classNames?.text)}>{`:${emoji.shortcode}:`}</span>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
import { isInsecureUrl } from '@/lib/url'
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
|
|
||||||
import blossomService from '@/services/blossom.service'
|
import blossomService from '@/services/blossom.service'
|
||||||
import { TImetaInfo } from '@/types'
|
import { TImetaInfo } from '@/types'
|
||||||
import { decode } from 'blurhash'
|
import { decode } from 'blurhash'
|
||||||
|
|
@ -28,24 +26,17 @@ export default function Image({
|
||||||
hideIfError?: boolean
|
hideIfError?: boolean
|
||||||
errorPlaceholder?: React.ReactNode
|
errorPlaceholder?: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const { allowInsecureConnection } = useUserPreferences()
|
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [displaySkeleton, setDisplaySkeleton] = useState(true)
|
const [displaySkeleton, setDisplaySkeleton] = useState(true)
|
||||||
const [hasError, setHasError] = useState(false)
|
const [hasError, setHasError] = useState(false)
|
||||||
const [imageUrl, setImageUrl] = useState<string>()
|
const [imageUrl, setImageUrl] = useState<string>()
|
||||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setHasError(false)
|
setHasError(false)
|
||||||
setDisplaySkeleton(true)
|
setDisplaySkeleton(true)
|
||||||
|
|
||||||
if (!allowInsecureConnection && isInsecureUrl(url)) {
|
|
||||||
setHasError(true)
|
|
||||||
setIsLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pubkey) {
|
if (pubkey) {
|
||||||
blossomService.getValidUrl(url, pubkey).then((validUrl) => {
|
blossomService.getValidUrl(url, pubkey).then((validUrl) => {
|
||||||
setImageUrl(validUrl)
|
setImageUrl(validUrl)
|
||||||
|
|
@ -60,7 +51,7 @@ export default function Image({
|
||||||
} else {
|
} else {
|
||||||
setImageUrl(url)
|
setImageUrl(url)
|
||||||
}
|
}
|
||||||
}, [url, allowInsecureConnection])
|
}, [url])
|
||||||
|
|
||||||
if (hideIfError && hasError) return null
|
if (hideIfError && hasError) return null
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@ export default function MarkdownContent({
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3 whitespace-normal">
|
<div className="space-y-3">
|
||||||
<Markdown
|
<Markdown
|
||||||
remarkPlugins={[remarkGfm, remarkNostr, remarkInlineContent]}
|
remarkPlugins={[remarkGfm, remarkNostr, remarkInlineContent]}
|
||||||
urlTransform={(url) => {
|
urlTransform={(url) => {
|
||||||
|
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
import Emoji from '@/components/Emoji'
|
|
||||||
import { getEmojiInfosFromEmojiTags } from '@/lib/tag'
|
|
||||||
import { Event } from 'nostr-tools'
|
|
||||||
import { useMemo } from 'react'
|
|
||||||
|
|
||||||
export default function Reaction({
|
|
||||||
event,
|
|
||||||
className
|
|
||||||
}: {
|
|
||||||
event: Event
|
|
||||||
className?: string
|
|
||||||
}) {
|
|
||||||
const emoji = useMemo(() => {
|
|
||||||
const content = event.content
|
|
||||||
if (!content || content === '+') return '+'
|
|
||||||
|
|
||||||
const emojiName = /^:([^:]+):$/.exec(content)?.[1]
|
|
||||||
if (emojiName) {
|
|
||||||
const emojiInfos = getEmojiInfosFromEmojiTags(event.tags)
|
|
||||||
const emojiInfo = emojiInfos.find((e) => e.shortcode === emojiName)
|
|
||||||
if (emojiInfo) return emojiInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.length <= 4) return content
|
|
||||||
return '+'
|
|
||||||
}, [event])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
<Emoji emoji={emoji} classNames={{ text: 'text-7xl leading-none', img: 'size-20' }} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +1,15 @@
|
||||||
import { ExtendedKind } from '@/constants'
|
|
||||||
import { getImetaInfosFromEvent } from '@/lib/event'
|
import { getImetaInfosFromEvent } from '@/lib/event'
|
||||||
import { getVideoMetadataFromEvent } from '@/lib/event-metadata'
|
|
||||||
import { Event } from 'nostr-tools'
|
import { Event } from 'nostr-tools'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import Content from '../Content'
|
import Content from '../Content'
|
||||||
import { EmbeddedHashtag } from '../Embedded'
|
|
||||||
import MediaPlayer from '../MediaPlayer'
|
import MediaPlayer from '../MediaPlayer'
|
||||||
|
|
||||||
export default function VideoNote({ event, className }: { event: Event; className?: string }) {
|
export default function VideoNote({ event, className }: { event: Event; className?: string }) {
|
||||||
const videoInfos = useMemo(() => getImetaInfosFromEvent(event), [event])
|
const videoInfos = useMemo(() => getImetaInfosFromEvent(event), [event])
|
||||||
const isAddressable =
|
|
||||||
event.kind === ExtendedKind.ADDRESSABLE_NORMAL_VIDEO ||
|
|
||||||
event.kind === ExtendedKind.ADDRESSABLE_SHORT_VIDEO
|
|
||||||
const metadata = useMemo(
|
|
||||||
() => (isAddressable ? getVideoMetadataFromEvent(event) : null),
|
|
||||||
[event, isAddressable]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{metadata?.title && <div className="font-semibold">{metadata.title}</div>}
|
|
||||||
<Content event={event} />
|
<Content event={event} />
|
||||||
{metadata && metadata.tags.length > 0 && (
|
|
||||||
<div className="mt-1 flex flex-wrap gap-1">
|
|
||||||
{metadata.tags.map((tag) => (
|
|
||||||
<EmbeddedHashtag key={tag} hashtag={tag} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{videoInfos.map((video) => (
|
{videoInfos.map((video) => (
|
||||||
<MediaPlayer src={video.url} key={video.url} className="mt-2" />
|
<MediaPlayer src={video.url} key={video.url} className="mt-2" />
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,11 @@ import { useSecondaryPage } from '@/PageManager'
|
||||||
import { ExtendedKind, NSFW_DISPLAY_POLICY, SUPPORTED_KINDS } from '@/constants'
|
import { ExtendedKind, NSFW_DISPLAY_POLICY, SUPPORTED_KINDS } from '@/constants'
|
||||||
import { getParentStuff, isNsfwEvent } from '@/lib/event'
|
import { getParentStuff, isNsfwEvent } from '@/lib/event'
|
||||||
import { toExternalContent, toNote } from '@/lib/link'
|
import { toExternalContent, toNote } from '@/lib/link'
|
||||||
import { generateBech32IdFromATag, generateBech32IdFromETag, tagNameEquals } from '@/lib/tag'
|
|
||||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||||
import { useMuteList } from '@/providers/MuteListProvider'
|
import { useMuteList } from '@/providers/MuteListProvider'
|
||||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||||
import { Event, kinds } from 'nostr-tools'
|
import { Event, kinds } from 'nostr-tools'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import AudioPlayer from '../AudioPlayer'
|
import AudioPlayer from '../AudioPlayer'
|
||||||
import ClientTag from '../ClientTag'
|
import ClientTag from '../ClientTag'
|
||||||
import Content from '../Content'
|
import Content from '../Content'
|
||||||
|
|
@ -34,7 +32,6 @@ import MutedNote from './MutedNote'
|
||||||
import NsfwNote from './NsfwNote'
|
import NsfwNote from './NsfwNote'
|
||||||
import PictureNote from './PictureNote'
|
import PictureNote from './PictureNote'
|
||||||
import Poll from './Poll'
|
import Poll from './Poll'
|
||||||
import Reaction from './Reaction'
|
|
||||||
import RelayReview from './RelayReview'
|
import RelayReview from './RelayReview'
|
||||||
import UnknownNote from './UnknownNote'
|
import UnknownNote from './UnknownNote'
|
||||||
import VideoNote from './VideoNote'
|
import VideoNote from './VideoNote'
|
||||||
|
|
@ -54,21 +51,11 @@ export default function Note({
|
||||||
hideParentNotePreview?: boolean
|
hideParentNotePreview?: boolean
|
||||||
showFull?: boolean
|
showFull?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
|
||||||
const { push } = useSecondaryPage()
|
const { push } = useSecondaryPage()
|
||||||
const { isSmallScreen } = useScreenSize()
|
const { isSmallScreen } = useScreenSize()
|
||||||
const { parentEventId, parentExternalContent } = useMemo(() => {
|
const { parentEventId, parentExternalContent } = useMemo(() => {
|
||||||
return getParentStuff(event)
|
return getParentStuff(event)
|
||||||
}, [event])
|
}, [event])
|
||||||
const reactionTargetEventId = useMemo(() => {
|
|
||||||
if (event.kind !== kinds.Reaction && event.kind !== ExtendedKind.EXTERNAL_CONTENT_REACTION) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
const aTag = event.tags.findLast(tagNameEquals('a'))
|
|
||||||
if (aTag) return generateBech32IdFromATag(aTag)
|
|
||||||
const eTag = event.tags.findLast(tagNameEquals('e'))
|
|
||||||
return eTag ? generateBech32IdFromETag(eTag) : undefined
|
|
||||||
}, [event])
|
|
||||||
const { nsfwDisplayPolicy } = useContentPolicy()
|
const { nsfwDisplayPolicy } = useContentPolicy()
|
||||||
const [showNsfw, setShowNsfw] = useState(false)
|
const [showNsfw, setShowNsfw] = useState(false)
|
||||||
const { mutePubkeySet } = useMuteList()
|
const { mutePubkeySet } = useMuteList()
|
||||||
|
|
@ -130,8 +117,6 @@ export default function Note({
|
||||||
content = <EmojiPack className="mt-2" event={event} />
|
content = <EmojiPack className="mt-2" event={event} />
|
||||||
} else if (event.kind === ExtendedKind.FOLLOW_PACK) {
|
} else if (event.kind === ExtendedKind.FOLLOW_PACK) {
|
||||||
content = <FollowPack className="mt-2" event={event} />
|
content = <FollowPack className="mt-2" event={event} />
|
||||||
} else if (event.kind === kinds.Reaction) {
|
|
||||||
content = <Reaction className="mt-2" event={event} />
|
|
||||||
} else {
|
} else {
|
||||||
content = <Content className="mt-2" event={event} enableHighlight />
|
content = <Content className="mt-2" event={event} enableHighlight />
|
||||||
}
|
}
|
||||||
|
|
@ -185,17 +170,6 @@ export default function Note({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{reactionTargetEventId && (
|
|
||||||
<ParentNotePreview
|
|
||||||
eventId={reactionTargetEventId}
|
|
||||||
label={t('reacted to')}
|
|
||||||
className="mt-2"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
push(toNote(reactionTargetEventId))
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,18 +9,15 @@ export default function ParentNotePreview({
|
||||||
eventId,
|
eventId,
|
||||||
externalContent,
|
externalContent,
|
||||||
className,
|
className,
|
||||||
onClick,
|
onClick
|
||||||
label
|
|
||||||
}: {
|
}: {
|
||||||
eventId?: string
|
eventId?: string
|
||||||
externalContent?: string
|
externalContent?: string
|
||||||
className?: string
|
className?: string
|
||||||
onClick?: React.MouseEventHandler<HTMLDivElement> | undefined
|
onClick?: React.MouseEventHandler<HTMLDivElement> | undefined
|
||||||
label?: string
|
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { event, isFetching } = useFetchEvent(eventId)
|
const { event, isFetching } = useFetchEvent(eventId)
|
||||||
const displayLabel = label ?? t('reply to')
|
|
||||||
|
|
||||||
if (externalContent) {
|
if (externalContent) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -31,7 +28,7 @@ export default function ParentNotePreview({
|
||||||
)}
|
)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<div className="shrink-0">{displayLabel}</div>
|
<div className="shrink-0">{t('reply to')}</div>
|
||||||
<div className="truncate">{externalContent}</div>
|
<div className="truncate">{externalContent}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -49,7 +46,7 @@ export default function ParentNotePreview({
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="shrink-0">{displayLabel}</div>
|
<div className="shrink-0">{t('reply to')}</div>
|
||||||
<Skeleton className="h-4 w-4 rounded-full" />
|
<Skeleton className="h-4 w-4 rounded-full" />
|
||||||
<div className="flex-1 py-1">
|
<div className="flex-1 py-1">
|
||||||
<Skeleton className="h-3" />
|
<Skeleton className="h-3" />
|
||||||
|
|
@ -67,7 +64,7 @@ export default function ParentNotePreview({
|
||||||
)}
|
)}
|
||||||
onClick={event ? onClick : undefined}
|
onClick={event ? onClick : undefined}
|
||||||
>
|
>
|
||||||
<div className="shrink-0">{displayLabel}</div>
|
<div className="shrink-0">{t('reply to')}</div>
|
||||||
{event && <UserAvatar className="shrink-0" userId={event.pubkey} size="tiny" />}
|
{event && <UserAvatar className="shrink-0" userId={event.pubkey} size="tiny" />}
|
||||||
<ContentPreview className="truncate" event={event} />
|
<ContentPreview className="truncate" event={event} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { useSecondaryPage } from '@/PageManager'
|
||||||
import { SPECIAL_TRUST_SCORE_FILTER_ID } from '@/constants'
|
import { SPECIAL_TRUST_SCORE_FILTER_ID } from '@/constants'
|
||||||
import { useStuff } from '@/hooks/useStuff'
|
import { useStuff } from '@/hooks/useStuff'
|
||||||
import { useStuffStatsById } from '@/hooks/useStuffStatsById'
|
import { useStuffStatsById } from '@/hooks/useStuffStatsById'
|
||||||
import { toNote } from '@/lib/link'
|
import { toProfile } from '@/lib/link'
|
||||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||||
import { TEmoji } from '@/types'
|
import { TEmoji } from '@/types'
|
||||||
|
|
@ -27,7 +27,6 @@ export default function ReactionList({ stuff }: { stuff: Event | string }) {
|
||||||
const [filteredLikes, setFilteredLikes] = useState<
|
const [filteredLikes, setFilteredLikes] = useState<
|
||||||
Array<{
|
Array<{
|
||||||
id: string
|
id: string
|
||||||
eventId: string
|
|
||||||
pubkey: string
|
pubkey: string
|
||||||
emoji: string | TEmoji
|
emoji: string | TEmoji
|
||||||
created_at: number
|
created_at: number
|
||||||
|
|
@ -39,7 +38,6 @@ export default function ReactionList({ stuff }: { stuff: Event | string }) {
|
||||||
const likes = noteStats?.likes ?? []
|
const likes = noteStats?.likes ?? []
|
||||||
const filtered: {
|
const filtered: {
|
||||||
id: string
|
id: string
|
||||||
eventId: string
|
|
||||||
pubkey: string
|
pubkey: string
|
||||||
created_at: number
|
created_at: number
|
||||||
emoji: string | TEmoji
|
emoji: string | TEmoji
|
||||||
|
|
@ -83,7 +81,7 @@ export default function ReactionList({ stuff }: { stuff: Event | string }) {
|
||||||
<div
|
<div
|
||||||
key={like.id}
|
key={like.id}
|
||||||
className="clickable flex items-center gap-3 border-b px-4 py-3 transition-colors"
|
className="clickable flex items-center gap-3 border-b px-4 py-3 transition-colors"
|
||||||
onClick={() => push(toNote(like.eventId))}
|
onClick={() => push(toProfile(like.pubkey))}
|
||||||
>
|
>
|
||||||
<div className="flex w-6 flex-col items-center">
|
<div className="flex w-6 flex-col items-center">
|
||||||
<Emoji
|
<Emoji
|
||||||
|
|
|
||||||
|
|
@ -306,8 +306,7 @@ const UserAggregationList = forwardRef<
|
||||||
hideContentMentioningMutedUsers,
|
hideContentMentioningMutedUsers,
|
||||||
isMentioningMutedUsers,
|
isMentioningMutedUsers,
|
||||||
meetsMinTrustScore,
|
meetsMinTrustScore,
|
||||||
trustScoreThreshold,
|
trustScoreThreshold
|
||||||
since
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
|
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
|
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||||
import { useFetchProfile } from '@/hooks'
|
import { useFetchProfile } from '@/hooks'
|
||||||
import { toProfile } from '@/lib/link'
|
import { toProfile } from '@/lib/link'
|
||||||
import { generateImageByPubkey } from '@/lib/pubkey'
|
import { generateImageByPubkey } from '@/lib/pubkey'
|
||||||
import { cn, isTouchDevice } from '@/lib/utils'
|
import { cn, isTouchDevice } from '@/lib/utils'
|
||||||
import { SecondaryPageLink } from '@/PageManager'
|
import { SecondaryPageLink } from '@/PageManager'
|
||||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import Image from '../Image'
|
import Image from '../Image'
|
||||||
import ProfileCard from '../ProfileCard'
|
import ProfileCard from '../ProfileCard'
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { isInsecureUrl } from '@/lib/url'
|
|
||||||
import { cn, isInViewport } from '@/lib/utils'
|
import { cn, isInViewport } from '@/lib/utils'
|
||||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||||
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
|
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
|
||||||
|
|
@ -8,7 +7,7 @@ import ExternalLink from '../ExternalLink'
|
||||||
|
|
||||||
export default function VideoPlayer({ src, className }: { src: string; className?: string }) {
|
export default function VideoPlayer({ src, className }: { src: string; className?: string }) {
|
||||||
const { autoplay, videoLoop } = useContentPolicy()
|
const { autoplay, videoLoop } = useContentPolicy()
|
||||||
const { muteMedia, updateMuteMedia, allowInsecureConnection } = useUserPreferences()
|
const { muteMedia, updateMuteMedia } = useUserPreferences()
|
||||||
const [error, setError] = useState(false)
|
const [error, setError] = useState(false)
|
||||||
const videoRef = useRef<HTMLVideoElement>(null)
|
const videoRef = useRef<HTMLVideoElement>(null)
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
@ -70,7 +69,7 @@ export default function VideoPlayer({ src, className }: { src: string; className
|
||||||
}
|
}
|
||||||
}, [muteMedia])
|
}, [muteMedia])
|
||||||
|
|
||||||
if (error || (!allowInsecureConnection && isInsecureUrl(src))) {
|
if (error) {
|
||||||
return <ExternalLink url={src} />
|
return <ExternalLink url={src} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
import { useFetchWebMetadata } from '@/hooks/useFetchWebMetadata'
|
import { useFetchWebMetadata } from '@/hooks/useFetchWebMetadata'
|
||||||
import { isInsecureUrl } from '@/lib/url'
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||||
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
|
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import ExternalLink from '../ExternalLink'
|
|
||||||
import Image from '../Image'
|
import Image from '../Image'
|
||||||
|
import ExternalLink from '../ExternalLink'
|
||||||
|
|
||||||
export default function WebPreview({
|
export default function WebPreview({
|
||||||
url,
|
url,
|
||||||
|
|
@ -18,7 +16,6 @@ export default function WebPreview({
|
||||||
mustLoad?: boolean
|
mustLoad?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { autoLoadMedia } = useContentPolicy()
|
const { autoLoadMedia } = useContentPolicy()
|
||||||
const { allowInsecureConnection } = useUserPreferences()
|
|
||||||
const { isSmallScreen } = useScreenSize()
|
const { isSmallScreen } = useScreenSize()
|
||||||
const { title, description, image } = useFetchWebMetadata(url)
|
const { title, description, image } = useFetchWebMetadata(url)
|
||||||
|
|
||||||
|
|
@ -30,10 +27,6 @@ export default function WebPreview({
|
||||||
}
|
}
|
||||||
}, [url])
|
}, [url])
|
||||||
|
|
||||||
if (!allowInsecureConnection && isInsecureUrl(url)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!autoLoadMedia && !mustLoad) {
|
if (!autoLoadMedia && !mustLoad) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,6 @@ export const StorageKey = {
|
||||||
ENABLE_SINGLE_COLUMN_LAYOUT: 'enableSingleColumnLayout',
|
ENABLE_SINGLE_COLUMN_LAYOUT: 'enableSingleColumnLayout',
|
||||||
FAVICON_URL_TEMPLATE: 'faviconUrlTemplate',
|
FAVICON_URL_TEMPLATE: 'faviconUrlTemplate',
|
||||||
FILTER_OUT_ONION_RELAYS: 'filterOutOnionRelays',
|
FILTER_OUT_ONION_RELAYS: 'filterOutOnionRelays',
|
||||||
ALLOW_INSECURE_CONNECTION: 'allowInsecureConnection',
|
|
||||||
QUICK_REACTION: 'quickReaction',
|
QUICK_REACTION: 'quickReaction',
|
||||||
QUICK_REACTION_EMOJI: 'quickReactionEmoji',
|
QUICK_REACTION_EMOJI: 'quickReactionEmoji',
|
||||||
NSFW_DISPLAY_POLICY: 'nsfwDisplayPolicy',
|
NSFW_DISPLAY_POLICY: 'nsfwDisplayPolicy',
|
||||||
|
|
@ -125,9 +124,7 @@ export const SUPPORTED_KINDS = [
|
||||||
...ALLOWED_FILTER_KINDS,
|
...ALLOWED_FILTER_KINDS,
|
||||||
ExtendedKind.RELAY_REVIEW,
|
ExtendedKind.RELAY_REVIEW,
|
||||||
kinds.Emojisets,
|
kinds.Emojisets,
|
||||||
ExtendedKind.FOLLOW_PACK,
|
ExtendedKind.FOLLOW_PACK
|
||||||
kinds.Reaction,
|
|
||||||
ExtendedKind.EXTERNAL_CONTENT_REACTION
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export const URL_REGEX =
|
export const URL_REGEX =
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
import { isInsecureUrl } from '@/lib/url'
|
|
||||||
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
|
|
||||||
import webService from '@/services/web.service'
|
|
||||||
import { TWebMetadata } from '@/types'
|
import { TWebMetadata } from '@/types'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import webService from '@/services/web.service'
|
||||||
|
|
||||||
export function useFetchWebMetadata(url: string) {
|
export function useFetchWebMetadata(url: string) {
|
||||||
const { allowInsecureConnection } = useUserPreferences()
|
|
||||||
const [metadata, setMetadata] = useState<TWebMetadata>({})
|
const [metadata, setMetadata] = useState<TWebMetadata>({})
|
||||||
const proxyServer = import.meta.env.VITE_PROXY_SERVER
|
const proxyServer = import.meta.env.VITE_PROXY_SERVER
|
||||||
if (proxyServer) {
|
if (proxyServer) {
|
||||||
|
|
@ -13,10 +10,8 @@ export function useFetchWebMetadata(url: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!allowInsecureConnection && isInsecureUrl(url)) return
|
|
||||||
|
|
||||||
webService.fetchWebMetadata(url).then((metadata) => setMetadata(metadata))
|
webService.fetchWebMetadata(url).then((metadata) => setMetadata(metadata))
|
||||||
}, [url, allowInsecureConnection])
|
}, [url])
|
||||||
|
|
||||||
return metadata
|
return metadata
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -674,11 +674,6 @@ export default {
|
||||||
'Relays used for searching notes (NIP-50)': 'الريلايات المستخدمة للبحث عن الملاحظات (NIP-50)',
|
'Relays used for searching notes (NIP-50)': 'الريلايات المستخدمة للبحث عن الملاحظات (NIP-50)',
|
||||||
'Protected event (NIP-70)': 'حدث محمي (NIP-70)',
|
'Protected event (NIP-70)': 'حدث محمي (NIP-70)',
|
||||||
'Protected': 'محمي',
|
'Protected': 'محمي',
|
||||||
'Protected event hint': 'الأحداث المحمية (NIP-70) لا يمكن نشرها إلا من قبل المؤلف. سترفض الخوادم هذه الأحداث من أطراف ثالثة، مما يمنع الآخرين من إعادة بث محتواك. ملاحظة: لا تدعم جميع الخوادم الأحداث المحمية.',
|
'Protected event hint': 'الأحداث المحمية (NIP-70) لا يمكن نشرها إلا من قبل المؤلف. سترفض الخوادم هذه الأحداث من أطراف ثالثة، مما يمنع الآخرين من إعادة بث محتواك. ملاحظة: لا تدعم جميع الخوادم الأحداث المحمية.'
|
||||||
'Allow insecure connections': 'السماح بالاتصالات غير الآمنة',
|
|
||||||
'Allow insecure connections description':
|
|
||||||
'السماح بتحميل موارد http:// والاتصال بمرحلات ws://. قد يؤدي إلى تحذيرات المحتوى المختلط في المتصفح.',
|
|
||||||
'reacted to': 'تفاعل مع',
|
|
||||||
Reaction: 'تفاعل'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -698,11 +698,6 @@ export default {
|
||||||
'Relays used for searching notes (NIP-50)': 'Relays für die Notizsuche (NIP-50)',
|
'Relays used for searching notes (NIP-50)': 'Relays für die Notizsuche (NIP-50)',
|
||||||
'Protected event (NIP-70)': 'Geschütztes Ereignis (NIP-70)',
|
'Protected event (NIP-70)': 'Geschütztes Ereignis (NIP-70)',
|
||||||
'Protected': 'Geschützt',
|
'Protected': 'Geschützt',
|
||||||
'Protected event hint': 'Geschützte Ereignisse (NIP-70) können nur vom Autor veröffentlicht werden. Relays lehnen diese Ereignisse von Dritten ab und verhindern so, dass andere Ihre Inhalte weiterverbreiten. Hinweis: Nicht alle Relays unterstützen geschützte Ereignisse.',
|
'Protected event hint': 'Geschützte Ereignisse (NIP-70) können nur vom Autor veröffentlicht werden. Relays lehnen diese Ereignisse von Dritten ab und verhindern so, dass andere Ihre Inhalte weiterverbreiten. Hinweis: Nicht alle Relays unterstützen geschützte Ereignisse.'
|
||||||
'Allow insecure connections': 'Unsichere Verbindungen zulassen',
|
|
||||||
'Allow insecure connections description':
|
|
||||||
'Laden von http://-Ressourcen und Verbindung zu ws://-Relays erlauben. Kann Browser-Warnungen zu gemischten Inhalten auslösen.',
|
|
||||||
'reacted to': 'reagierte auf',
|
|
||||||
Reaction: 'Reaktion'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -681,11 +681,6 @@ export default {
|
||||||
'Protected event (NIP-70)': 'Protected event (NIP-70)',
|
'Protected event (NIP-70)': 'Protected event (NIP-70)',
|
||||||
'Protected': 'Protected',
|
'Protected': 'Protected',
|
||||||
'Protected event hint':
|
'Protected event hint':
|
||||||
'Protected events (NIP-70) can only be published by the author. Relays will reject these events from third parties, preventing others from rebroadcasting your content. Note: not all relays support protected events.',
|
'Protected events (NIP-70) can only be published by the author. Relays will reject these events from third parties, preventing others from rebroadcasting your content. Note: not all relays support protected events.'
|
||||||
'Allow insecure connections': 'Allow insecure connections',
|
|
||||||
'Allow insecure connections description':
|
|
||||||
'Allow loading http:// resources and connecting to ws:// relays. May trigger browser mixed content warnings.',
|
|
||||||
'reacted to': 'reacted to',
|
|
||||||
Reaction: 'Reaction'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -691,11 +691,6 @@ export default {
|
||||||
'Relays used for searching notes (NIP-50)': 'Relés utilizados para buscar notas (NIP-50)',
|
'Relays used for searching notes (NIP-50)': 'Relés utilizados para buscar notas (NIP-50)',
|
||||||
'Protected event (NIP-70)': 'Evento protegido (NIP-70)',
|
'Protected event (NIP-70)': 'Evento protegido (NIP-70)',
|
||||||
'Protected': 'Protegido',
|
'Protected': 'Protegido',
|
||||||
'Protected event hint': 'Los eventos protegidos (NIP-70) solo pueden ser publicados por el autor. Los relés rechazarán estos eventos de terceros, evitando que otros redistribuyan tu contenido. Nota: no todos los relés admiten eventos protegidos.',
|
'Protected event hint': 'Los eventos protegidos (NIP-70) solo pueden ser publicados por el autor. Los relés rechazarán estos eventos de terceros, evitando que otros redistribuyan tu contenido. Nota: no todos los relés admiten eventos protegidos.'
|
||||||
'Allow insecure connections': 'Permitir conexiones inseguras',
|
|
||||||
'Allow insecure connections description':
|
|
||||||
'Permitir cargar recursos http:// y conectar a relays ws://. Puede activar advertencias de contenido mixto del navegador.',
|
|
||||||
'reacted to': 'reaccionó a',
|
|
||||||
Reaction: 'Reacción'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -686,11 +686,6 @@ export default {
|
||||||
'Relays used for searching notes (NIP-50)': 'رلههایی که برای جستجوی یادداشتها استفاده میشوند (NIP-50)',
|
'Relays used for searching notes (NIP-50)': 'رلههایی که برای جستجوی یادداشتها استفاده میشوند (NIP-50)',
|
||||||
'Protected event (NIP-70)': 'رویداد محافظتشده (NIP-70)',
|
'Protected event (NIP-70)': 'رویداد محافظتشده (NIP-70)',
|
||||||
'Protected': 'محافظتشده',
|
'Protected': 'محافظتشده',
|
||||||
'Protected event hint': 'رویدادهای محافظتشده (NIP-70) فقط توسط نویسنده قابل انتشار هستند. رلهها این رویدادها را از اشخاص ثالث رد میکنند و از بازنشر محتوای شما توسط دیگران جلوگیری میکنند. توجه: همه رلهها از رویدادهای محافظتشده پشتیبانی نمیکنند.',
|
'Protected event hint': 'رویدادهای محافظتشده (NIP-70) فقط توسط نویسنده قابل انتشار هستند. رلهها این رویدادها را از اشخاص ثالث رد میکنند و از بازنشر محتوای شما توسط دیگران جلوگیری میکنند. توجه: همه رلهها از رویدادهای محافظتشده پشتیبانی نمیکنند.'
|
||||||
'Allow insecure connections': 'اجازه اتصالات ناامن',
|
|
||||||
'Allow insecure connections description':
|
|
||||||
'اجازه بارگذاری منابع http:// و اتصال به رلههای ws://. ممکن است هشدارهای محتوای مختلط مرورگر را فعال کند.',
|
|
||||||
'reacted to': 'واکنش نشان داد به',
|
|
||||||
Reaction: 'واکنش'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -695,11 +695,6 @@ export default {
|
||||||
'Relays used for searching notes (NIP-50)': 'Relais utilisés pour rechercher des notes (NIP-50)',
|
'Relays used for searching notes (NIP-50)': 'Relais utilisés pour rechercher des notes (NIP-50)',
|
||||||
'Protected event (NIP-70)': 'Événement protégé (NIP-70)',
|
'Protected event (NIP-70)': 'Événement protégé (NIP-70)',
|
||||||
'Protected': 'Protégé',
|
'Protected': 'Protégé',
|
||||||
'Protected event hint': 'Les événements protégés (NIP-70) ne peuvent être publiés que par l\'auteur. Les relais rejetteront ces événements provenant de tiers, empêchant les autres de rediffuser votre contenu. Remarque : tous les relais ne prennent pas en charge les événements protégés.',
|
'Protected event hint': 'Les événements protégés (NIP-70) ne peuvent être publiés que par l\'auteur. Les relais rejetteront ces événements provenant de tiers, empêchant les autres de rediffuser votre contenu. Remarque : tous les relais ne prennent pas en charge les événements protégés.'
|
||||||
'Allow insecure connections': 'Autoriser les connexions non sécurisées',
|
|
||||||
'Allow insecure connections description':
|
|
||||||
'Autoriser le chargement des ressources http:// et la connexion aux relais ws://. Peut déclencher des avertissements de contenu mixte du navigateur.',
|
|
||||||
'reacted to': 'a réagi à',
|
|
||||||
Reaction: 'Réaction'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -686,11 +686,6 @@ export default {
|
||||||
'Relays used for searching notes (NIP-50)': 'नोट्स खोजने के लिए उपयोग किए जाने वाले रिले (NIP-50)',
|
'Relays used for searching notes (NIP-50)': 'नोट्स खोजने के लिए उपयोग किए जाने वाले रिले (NIP-50)',
|
||||||
'Protected event (NIP-70)': 'संरक्षित इवेंट (NIP-70)',
|
'Protected event (NIP-70)': 'संरक्षित इवेंट (NIP-70)',
|
||||||
'Protected': 'संरक्षित',
|
'Protected': 'संरक्षित',
|
||||||
'Protected event hint': 'संरक्षित इवेंट (NIP-70) केवल लेखक द्वारा प्रकाशित किए जा सकते हैं। रिले तीसरे पक्ष से इन इवेंट को अस्वीकार कर देंगे, जिससे दूसरों को आपकी सामग्री को पुनः प्रसारित करने से रोका जा सके। नोट: सभी रिले संरक्षित इवेंट का समर्थन नहीं करते।',
|
'Protected event hint': 'संरक्षित इवेंट (NIP-70) केवल लेखक द्वारा प्रकाशित किए जा सकते हैं। रिले तीसरे पक्ष से इन इवेंट को अस्वीकार कर देंगे, जिससे दूसरों को आपकी सामग्री को पुनः प्रसारित करने से रोका जा सके। नोट: सभी रिले संरक्षित इवेंट का समर्थन नहीं करते।'
|
||||||
'Allow insecure connections': 'असुरक्षित कनेक्शन की अनुमति दें',
|
|
||||||
'Allow insecure connections description':
|
|
||||||
'http:// संसाधन लोड करने और ws:// रिले से कनेक्ट करने की अनुमति दें। ब्राउज़र मिश्रित सामग्री चेतावनियाँ ट्रिगर हो सकती हैं।',
|
|
||||||
'reacted to': 'पर प्रतिक्रिया दी',
|
|
||||||
Reaction: 'प्रतिक्रिया'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -680,11 +680,6 @@ export default {
|
||||||
'Relays used for searching notes (NIP-50)': 'Jegyzetek kereséséhez használt csomópontok (NIP-50)',
|
'Relays used for searching notes (NIP-50)': 'Jegyzetek kereséséhez használt csomópontok (NIP-50)',
|
||||||
'Protected event (NIP-70)': 'Védett esemény (NIP-70)',
|
'Protected event (NIP-70)': 'Védett esemény (NIP-70)',
|
||||||
'Protected': 'Védett',
|
'Protected': 'Védett',
|
||||||
'Protected event hint': 'A védett eseményeket (NIP-70) csak a szerző teheti közzé. A csomópontok elutasítják ezeket az eseményeket harmadik felektől, megakadályozva, hogy mások újraközvetítsék a tartalmadat. Megjegyzés: nem minden csomópont támogatja a védett eseményeket.',
|
'Protected event hint': 'A védett eseményeket (NIP-70) csak a szerző teheti közzé. A csomópontok elutasítják ezeket az eseményeket harmadik felektől, megakadályozva, hogy mások újraközvetítsék a tartalmadat. Megjegyzés: nem minden csomópont támogatja a védett eseményeket.'
|
||||||
'Allow insecure connections': 'Nem biztonságos kapcsolatok engedélyezése',
|
|
||||||
'Allow insecure connections description':
|
|
||||||
'http:// erőforrások betöltésének és ws:// relékhez való csatlakozás engedélyezése. Böngésző vegyes tartalom figyelmeztetéseket válthat ki.',
|
|
||||||
'reacted to': 'reagált erre',
|
|
||||||
Reaction: 'Reakció'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -691,11 +691,6 @@ export default {
|
||||||
'Relays used for searching notes (NIP-50)': 'Relay utilizzati per cercare le note (NIP-50)',
|
'Relays used for searching notes (NIP-50)': 'Relay utilizzati per cercare le note (NIP-50)',
|
||||||
'Protected event (NIP-70)': 'Evento protetto (NIP-70)',
|
'Protected event (NIP-70)': 'Evento protetto (NIP-70)',
|
||||||
'Protected': 'Protetto',
|
'Protected': 'Protetto',
|
||||||
'Protected event hint': 'Gli eventi protetti (NIP-70) possono essere pubblicati solo dall\'autore. I relay rifiuteranno questi eventi da terze parti, impedendo ad altri di ridiffondere i tuoi contenuti. Nota: non tutti i relay supportano gli eventi protetti.',
|
'Protected event hint': 'Gli eventi protetti (NIP-70) possono essere pubblicati solo dall\'autore. I relay rifiuteranno questi eventi da terze parti, impedendo ad altri di ridiffondere i tuoi contenuti. Nota: non tutti i relay supportano gli eventi protetti.'
|
||||||
'Allow insecure connections': 'Consenti connessioni non sicure',
|
|
||||||
'Allow insecure connections description':
|
|
||||||
'Consenti il caricamento di risorse http:// e la connessione a relay ws://. Potrebbe attivare avvisi di contenuto misto del browser.',
|
|
||||||
'reacted to': 'ha reagito a',
|
|
||||||
Reaction: 'Reazione'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -686,11 +686,6 @@ export default {
|
||||||
'Relays used for searching notes (NIP-50)': 'ノート検索に使用するリレー (NIP-50)',
|
'Relays used for searching notes (NIP-50)': 'ノート検索に使用するリレー (NIP-50)',
|
||||||
'Protected event (NIP-70)': '保護されたイベント (NIP-70)',
|
'Protected event (NIP-70)': '保護されたイベント (NIP-70)',
|
||||||
'Protected': '保護',
|
'Protected': '保護',
|
||||||
'Protected event hint': '保護されたイベント(NIP-70)は作成者のみが公開できます。リレーは第三者からのこれらのイベントを拒否し、他者によるコンテンツの再配信を防ぎます。 注意:すべてのリレーが保護されたイベントに対応しているわけではありません。',
|
'Protected event hint': '保護されたイベント(NIP-70)は作成者のみが公開できます。リレーは第三者からのこれらのイベントを拒否し、他者によるコンテンツの再配信を防ぎます。 注意:すべてのリレーが保護されたイベントに対応しているわけではありません。'
|
||||||
'Allow insecure connections': '安全でない接続を許可',
|
|
||||||
'Allow insecure connections description':
|
|
||||||
'http:// リソースの読み込みと ws:// リレーへの接続を許可します。ブラウザの混合コンテンツ警告が表示される場合があります。',
|
|
||||||
'reacted to': 'にリアクションしました',
|
|
||||||
Reaction: 'リアクション'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -680,11 +680,6 @@ export default {
|
||||||
'Relays used for searching notes (NIP-50)': '노트 검색에 사용되는 릴레이 (NIP-50)',
|
'Relays used for searching notes (NIP-50)': '노트 검색에 사용되는 릴레이 (NIP-50)',
|
||||||
'Protected event (NIP-70)': '보호된 이벤트 (NIP-70)',
|
'Protected event (NIP-70)': '보호된 이벤트 (NIP-70)',
|
||||||
'Protected': '보호됨',
|
'Protected': '보호됨',
|
||||||
'Protected event hint': '보호된 이벤트(NIP-70)는 작성자만 게시할 수 있습니다. 릴레이는 제3자의 이벤트를 거부하여 다른 사람이 콘텐츠를 재배포하는 것을 방지합니다. 참고: 모든 릴레이가 보호된 이벤트를 지원하는 것은 아닙니다.',
|
'Protected event hint': '보호된 이벤트(NIP-70)는 작성자만 게시할 수 있습니다. 릴레이는 제3자의 이벤트를 거부하여 다른 사람이 콘텐츠를 재배포하는 것을 방지합니다. 참고: 모든 릴레이가 보호된 이벤트를 지원하는 것은 아닙니다.'
|
||||||
'Allow insecure connections': '안전하지 않은 연결 허용',
|
|
||||||
'Allow insecure connections description':
|
|
||||||
'http:// 리소스 로드 및 ws:// 릴레이 연결을 허용합니다. 브라우저 혼합 콘텐츠 경고가 발생할 수 있습니다.',
|
|
||||||
'reacted to': '에 반응했습니다',
|
|
||||||
Reaction: '반응'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,7 @@ export default {
|
||||||
'Send only to r': 'Wyślij tylko do {{r}}',
|
'Send only to r': 'Wyślij tylko do {{r}}',
|
||||||
'Send only to these relays': 'Wyślij tylko do tych transmiterów',
|
'Send only to these relays': 'Wyślij tylko do tych transmiterów',
|
||||||
Explore: 'Transmitery',
|
Explore: 'Transmitery',
|
||||||
'Search relays': 'Transmitery wyszukiwania',
|
'Search relays': 'Przekaźniki wyszukiwania',
|
||||||
relayInfoBadgeAuth: '✔️',
|
relayInfoBadgeAuth: '✔️',
|
||||||
relayInfoBadgeSearch: 'Wyszukiwarka',
|
relayInfoBadgeSearch: 'Wyszukiwarka',
|
||||||
relayInfoBadgePayment: 'Płatności',
|
relayInfoBadgePayment: 'Płatności',
|
||||||
|
|
@ -671,32 +671,27 @@ export default {
|
||||||
'Auto-load profile pictures': 'Automatyczne ładowanie zdjęć profilowych',
|
'Auto-load profile pictures': 'Automatyczne ładowanie zdjęć profilowych',
|
||||||
'Disable live feed': 'Wyłącz kanał na żywo',
|
'Disable live feed': 'Wyłącz kanał na żywo',
|
||||||
'Enable live feed': 'Włącz kanał na żywo',
|
'Enable live feed': 'Włącz kanał na żywo',
|
||||||
'Default relays': 'Domyślne transmitery',
|
'Default relays': 'Domyślne przekaźniki',
|
||||||
'Reset to default': 'Przywróć domyślne',
|
'Reset to default': 'Przywróć domyślne',
|
||||||
'Default relays description':
|
'Default relays description':
|
||||||
'Używane do odpytywania konfiguracji transmiterów innych użytkowników i jako rozwiązanie awaryjne, gdy użytkownicy nie mają skonfigurowanych transmiterów.',
|
'Używane do odpytywania konfiguracji przekaźników innych użytkowników i jako rozwiązanie awaryjne, gdy użytkownicy nie mają skonfigurowanych przekaźników.',
|
||||||
'Default relays warning':
|
'Default relays warning':
|
||||||
'Ostrzeżenie: Nie modyfikuj tych ustawień pochopnie, może to wpłynąć na komfort użytkowania.',
|
'Ostrzeżenie: Nie modyfikuj tych ustawień pochopnie, może to wpłynąć na podstawowe doświadczenie.',
|
||||||
'Invalid relay URL': 'Nieprawidłowy adres URL transmitera',
|
'Invalid relay URL': 'Nieprawidłowy adres URL przekaźnika',
|
||||||
'Muted words': 'Wyciszone słowa',
|
'Muted words': 'Wyciszone słowa',
|
||||||
'Add muted word': 'Dodaj wyciszone słowo',
|
'Add muted word': 'Dodaj wyciszone słowo',
|
||||||
'Zap Details': 'Szczegóły zapu',
|
'Zap Details': 'Szczegóły zapu',
|
||||||
'Default trust score filter threshold ({{n}}%)':
|
'Default trust score filter threshold ({{n}}%)':
|
||||||
'Domyślny próg filtra wyniku zaufania ({{n}}%)',
|
'Domyślny próg filtra wyniku zaufania ({{n}}%)',
|
||||||
'No notes found': 'Nie znaleziono wpisów',
|
'No notes found': 'Nie znaleziono notatek',
|
||||||
'Try again later or check your connection': 'Spróbuj ponownie później lub sprawdź połączenie',
|
'Try again later or check your connection': 'Spróbuj ponownie później lub sprawdź połączenie',
|
||||||
'Hide indirect': 'Ukryj pośrednie',
|
'Hide indirect': 'Ukryj pośrednie',
|
||||||
'Copy note content': 'Kopiuj treść wpisu',
|
'Copy note content': 'Kopiuj treść notatki',
|
||||||
'Video loop': 'Zapętlanie wideo',
|
'Video loop': 'Zapętlanie wideo',
|
||||||
'Automatically replay videos when they end': 'Automatycznie powtarzaj filmy po zakończeniu',
|
'Automatically replay videos when they end': 'Automatycznie powtarzaj filmy po zakończeniu',
|
||||||
'Relays used for searching notes (NIP-50)': 'Transmitery używane do wyszukiwania wpisów (NIP-50)',
|
'Relays used for searching notes (NIP-50)': 'Przekaźniki używane do wyszukiwania notatek (NIP-50)',
|
||||||
'Protected event (NIP-70)': 'Chronione zdarzenie (NIP-70)',
|
'Protected event (NIP-70)': 'Chronione zdarzenie (NIP-70)',
|
||||||
'Protected': 'Chronione',
|
'Protected': 'Chronione',
|
||||||
'Protected event hint': 'Chronione zdarzenia (NIP-70) mogą być publikowane tylko przez autora. Transmitery odrzucą publikację tych zdarzeń przez osoby trzecie, uniemożliwiając innym retransmisję Twoich treści. Uwaga: nie wszystkie transmitery obsługują ochronę zdarzeń.',
|
'Protected event hint': 'Chronione zdarzenia (NIP-70) mogą być publikowane tylko przez autora. Przekaźniki odrzucą te zdarzenia od osób trzecich, uniemożliwiając innym retransmisję Twoich treści. Uwaga: nie wszystkie przekaźniki obsługują chronione zdarzenia.'
|
||||||
'Allow insecure connections': 'Zezwól na niezabezpieczone połączenia',
|
|
||||||
'Allow insecure connections description':
|
|
||||||
'Zezwól na ładowanie zasobów http:// i łączenie z transmiterami ws://. Może to wywołać ostrzeżenia przeglądarki o mieszanej zawartości.',
|
|
||||||
'reacted to': 'zareagował na',
|
|
||||||
Reaction: 'Reakcja'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -689,11 +689,6 @@ export default {
|
||||||
'Relays used for searching notes (NIP-50)': 'Relays usados para buscar notas (NIP-50)',
|
'Relays used for searching notes (NIP-50)': 'Relays usados para buscar notas (NIP-50)',
|
||||||
'Protected event (NIP-70)': 'Evento protegido (NIP-70)',
|
'Protected event (NIP-70)': 'Evento protegido (NIP-70)',
|
||||||
'Protected': 'Protegido',
|
'Protected': 'Protegido',
|
||||||
'Protected event hint': 'Eventos protegidos (NIP-70) só podem ser publicados pelo autor. Os relays rejeitarão esses eventos de terceiros, impedindo que outros retransmitam seu conteúdo. Nota: nem todos os relays suportam eventos protegidos.',
|
'Protected event hint': 'Eventos protegidos (NIP-70) só podem ser publicados pelo autor. Os relays rejeitarão esses eventos de terceiros, impedindo que outros retransmitam seu conteúdo. Nota: nem todos os relays suportam eventos protegidos.'
|
||||||
'Allow insecure connections': 'Permitir conexões inseguras',
|
|
||||||
'Allow insecure connections description':
|
|
||||||
'Permitir carregar recursos http:// e conectar a relays ws://. Pode acionar avisos de conteúdo misto do navegador.',
|
|
||||||
'reacted to': 'reagiu a',
|
|
||||||
Reaction: 'Reação'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -692,11 +692,6 @@ export default {
|
||||||
'Relays used for searching notes (NIP-50)': 'Relés usados para pesquisar notas (NIP-50)',
|
'Relays used for searching notes (NIP-50)': 'Relés usados para pesquisar notas (NIP-50)',
|
||||||
'Protected event (NIP-70)': 'Evento protegido (NIP-70)',
|
'Protected event (NIP-70)': 'Evento protegido (NIP-70)',
|
||||||
'Protected': 'Protegido',
|
'Protected': 'Protegido',
|
||||||
'Protected event hint': 'Eventos protegidos (NIP-70) só podem ser publicados pelo autor. Os relés rejeitarão estes eventos de terceiros, impedindo que outros retransmitam o seu conteúdo. Nota: nem todos os relés suportam eventos protegidos.',
|
'Protected event hint': 'Eventos protegidos (NIP-70) só podem ser publicados pelo autor. Os relés rejeitarão estes eventos de terceiros, impedindo que outros retransmitam o seu conteúdo. Nota: nem todos os relés suportam eventos protegidos.'
|
||||||
'Allow insecure connections': 'Permitir ligações inseguras',
|
|
||||||
'Allow insecure connections description':
|
|
||||||
'Permitir carregar recursos http:// e ligar a relays ws://. Pode acionar avisos de conteúdo misto do navegador.',
|
|
||||||
'reacted to': 'reagiu a',
|
|
||||||
Reaction: 'Reação'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -691,11 +691,6 @@ export default {
|
||||||
'Relays used for searching notes (NIP-50)': 'Ретрансляторы для поиска заметок (NIP-50)',
|
'Relays used for searching notes (NIP-50)': 'Ретрансляторы для поиска заметок (NIP-50)',
|
||||||
'Protected event (NIP-70)': 'Защищённое событие (NIP-70)',
|
'Protected event (NIP-70)': 'Защищённое событие (NIP-70)',
|
||||||
'Protected': 'Защищённый',
|
'Protected': 'Защищённый',
|
||||||
'Protected event hint': 'Защищённые события (NIP-70) могут быть опубликованы только автором. Ретрансляторы отклонят эти события от третьих лиц, предотвращая повторную трансляцию вашего контента. Примечание: не все ретрансляторы поддерживают защищённые события.',
|
'Protected event hint': 'Защищённые события (NIP-70) могут быть опубликованы только автором. Ретрансляторы отклонят эти события от третьих лиц, предотвращая повторную трансляцию вашего контента. Примечание: не все ретрансляторы поддерживают защищённые события.'
|
||||||
'Allow insecure connections': 'Разрешить небезопасные соединения',
|
|
||||||
'Allow insecure connections description':
|
|
||||||
'Разрешить загрузку ресурсов http:// и подключение к реле ws://. Может вызвать предупреждения браузера о смешанном содержимом.',
|
|
||||||
'reacted to': 'отреагировал на',
|
|
||||||
Reaction: 'Реакция'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -676,11 +676,6 @@ export default {
|
||||||
'Relays used for searching notes (NIP-50)': 'รีเลย์ที่ใช้สำหรับค้นหาโน้ต (NIP-50)',
|
'Relays used for searching notes (NIP-50)': 'รีเลย์ที่ใช้สำหรับค้นหาโน้ต (NIP-50)',
|
||||||
'Protected event (NIP-70)': 'เหตุการณ์ที่ได้รับการป้องกัน (NIP-70)',
|
'Protected event (NIP-70)': 'เหตุการณ์ที่ได้รับการป้องกัน (NIP-70)',
|
||||||
'Protected': 'ป้องกัน',
|
'Protected': 'ป้องกัน',
|
||||||
'Protected event hint': 'เหตุการณ์ที่ได้รับการป้องกัน (NIP-70) สามารถเผยแพร่ได้โดยผู้เขียนเท่านั้น รีเลย์จะปฏิเสธเหตุการณ์เหล่านี้จากบุคคลที่สาม ป้องกันไม่ให้ผู้อื่นเผยแพร่เนื้อหาของคุณซ้ำ หมายเหตุ: รีเลย์บางแห่งไม่รองรับเหตุการณ์ที่ได้รับการป้องกัน',
|
'Protected event hint': 'เหตุการณ์ที่ได้รับการป้องกัน (NIP-70) สามารถเผยแพร่ได้โดยผู้เขียนเท่านั้น รีเลย์จะปฏิเสธเหตุการณ์เหล่านี้จากบุคคลที่สาม ป้องกันไม่ให้ผู้อื่นเผยแพร่เนื้อหาของคุณซ้ำ หมายเหตุ: รีเลย์บางแห่งไม่รองรับเหตุการณ์ที่ได้รับการป้องกัน'
|
||||||
'Allow insecure connections': 'อนุญาตการเชื่อมต่อที่ไม่ปลอดภัย',
|
|
||||||
'Allow insecure connections description':
|
|
||||||
'อนุญาตให้โหลดทรัพยากร http:// และเชื่อมต่อกับรีเลย์ ws:// อาจทำให้เบราว์เซอร์แสดงคำเตือนเนื้อหาแบบผสม',
|
|
||||||
'reacted to': 'รีแอคชันไปที่',
|
|
||||||
Reaction: 'รีแอคชัน'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -658,11 +658,6 @@ export default {
|
||||||
'Relays used for searching notes (NIP-50)': '用於搜尋筆記的伺服器 (NIP-50)',
|
'Relays used for searching notes (NIP-50)': '用於搜尋筆記的伺服器 (NIP-50)',
|
||||||
'Protected event (NIP-70)': '受保護的事件 (NIP-70)',
|
'Protected event (NIP-70)': '受保護的事件 (NIP-70)',
|
||||||
'Protected': '受保護',
|
'Protected': '受保護',
|
||||||
'Protected event hint': '受保護的事件(NIP-70)只能由作者發布。伺服器將拒絕來自第三方的這些事件,防止他人轉播你的內容。 注意:並非所有伺服器都支持受保護的事件。',
|
'Protected event hint': '受保護的事件(NIP-70)只能由作者發布。伺服器將拒絕來自第三方的這些事件,防止他人轉播你的內容。 注意:並非所有伺服器都支持受保護的事件。'
|
||||||
'Allow insecure connections': '允許不安全的連線',
|
|
||||||
'Allow insecure connections description':
|
|
||||||
'允許載入 http:// 資源和連線 ws:// relay。可能會觸發瀏覽器混合內容警告。',
|
|
||||||
'reacted to': '回應了',
|
|
||||||
Reaction: '回應'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -663,11 +663,6 @@ export default {
|
||||||
'Relays used for searching notes (NIP-50)': '用于搜索笔记的服务器 (NIP-50)',
|
'Relays used for searching notes (NIP-50)': '用于搜索笔记的服务器 (NIP-50)',
|
||||||
'Protected event (NIP-70)': '受保护的事件 (NIP-70)',
|
'Protected event (NIP-70)': '受保护的事件 (NIP-70)',
|
||||||
'Protected': '受保护',
|
'Protected': '受保护',
|
||||||
'Protected event hint': '受保护的事件(NIP-70)只能由作者发布。服务器将拒绝来自第三方的这些事件,防止他人转播你的内容。 注意:并非所有服务器都支持受保护的事件。',
|
'Protected event hint': '受保护的事件(NIP-70)只能由作者发布。服务器将拒绝来自第三方的这些事件,防止他人转播你的内容。 注意:并非所有服务器都支持受保护的事件。'
|
||||||
'Allow insecure connections': '允许不安全的连接',
|
|
||||||
'Allow insecure connections description':
|
|
||||||
'允许加载 http:// 资源和连接 ws:// relay。可能会触发浏览器混合内容警告。',
|
|
||||||
'reacted to': '回应了',
|
|
||||||
Reaction: '回应'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -387,25 +387,6 @@ export function getEmojisFromEvent(event: Event): TEmoji[] {
|
||||||
return info.emojis
|
return info.emojis
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getVideoMetadataFromEvent(event: Event) {
|
|
||||||
let title: string | undefined
|
|
||||||
const tags = new Set<string>()
|
|
||||||
|
|
||||||
event.tags.forEach(([tagName, tagValue]) => {
|
|
||||||
if (tagName === 'title') {
|
|
||||||
title = tagValue
|
|
||||||
} else if (tagName === 't' && tagValue && tags.size < 6) {
|
|
||||||
tags.add(tagValue.toLocaleLowerCase())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!title) {
|
|
||||||
title = event.tags.find(tagNameEquals('d'))?.[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
return { title, tags: Array.from(tags) }
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getStarsFromRelayReviewEvent(event: Event): number {
|
export function getStarsFromRelayReviewEvent(event: Event): number {
|
||||||
const ratingTag = event.tags.find((t) => t[0] === 'rating')
|
const ratingTag = event.tags.find((t) => t[0] === 'rating')
|
||||||
if (ratingTag) {
|
if (ratingTag) {
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,11 @@ export function containsMarkdown(content: string): boolean {
|
||||||
|
|
||||||
let matchCount = 0
|
let matchCount = 0
|
||||||
for (const pattern of mediumPatterns) {
|
for (const pattern of mediumPatterns) {
|
||||||
const globalPattern = new RegExp(pattern.source, pattern.flags.includes('m') ? 'gm' : 'g')
|
if (pattern.test(cleaned)) {
|
||||||
const occurrences = (cleaned.match(globalPattern) || []).length
|
matchCount++
|
||||||
matchCount += occurrences
|
|
||||||
if (matchCount >= 2) return true
|
if (matchCount >= 2) return true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import storage from '@/services/local-storage.service'
|
|
||||||
import { SimplePool } from 'nostr-tools'
|
import { SimplePool } from 'nostr-tools'
|
||||||
import { AbstractRelay } from 'nostr-tools/abstract-relay'
|
import { AbstractRelay } from 'nostr-tools/abstract-relay'
|
||||||
import { isInsecureUrl } from './url'
|
|
||||||
|
|
||||||
const DEFAULT_CONNECTION_TIMEOUT = 10 * 1000 // 10 seconds
|
const DEFAULT_CONNECTION_TIMEOUT = 10 * 1000 // 10 seconds
|
||||||
const CLEANUP_THRESHOLD = 15 // number of relays to trigger cleanup
|
const CLEANUP_THRESHOLD = 15 // number of relays to trigger cleanup
|
||||||
|
|
@ -19,9 +17,6 @@ export class SmartPool extends SimplePool {
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureRelay(url: string): Promise<AbstractRelay> {
|
ensureRelay(url: string): Promise<AbstractRelay> {
|
||||||
if (!storage.getAllowInsecureConnection() && isInsecureUrl(url)) {
|
|
||||||
return Promise.reject(new Error(`Insecure relay connection blocked: ${url}`))
|
|
||||||
}
|
|
||||||
// If relay is new and we have many relays, trigger cleanup
|
// If relay is new and we have many relays, trigger cleanup
|
||||||
if (!this.relayIdleTracker.has(url) && this.relayIdleTracker.size > CLEANUP_THRESHOLD) {
|
if (!this.relayIdleTracker.has(url) && this.relayIdleTracker.size > CLEANUP_THRESHOLD) {
|
||||||
this.cleanIdleRelays()
|
this.cleanIdleRelays()
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,6 @@ export function isWebsocketUrl(url: string): boolean {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isInsecureUrl(url: string): boolean {
|
|
||||||
try {
|
|
||||||
const protocol = new URL(url).protocol
|
|
||||||
return protocol === 'ws:' || protocol === 'http:'
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isOnionUrl(url: string): boolean {
|
export function isOnionUrl(url: string): boolean {
|
||||||
try {
|
try {
|
||||||
const hostname = new URL(url).hostname
|
const hostname = new URL(url).hostname
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import { Switch } from '@/components/ui/switch'
|
||||||
import { DEFAULT_FAVICON_URL_TEMPLATE } from '@/constants'
|
import { DEFAULT_FAVICON_URL_TEMPLATE } from '@/constants'
|
||||||
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
||||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||||
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
|
|
||||||
import storage from '@/services/local-storage.service'
|
import storage from '@/services/local-storage.service'
|
||||||
import { forwardRef, useState } from 'react'
|
import { forwardRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
@ -14,7 +13,6 @@ import { useTranslation } from 'react-i18next'
|
||||||
const SystemSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
|
const SystemSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { faviconUrlTemplate, setFaviconUrlTemplate } = useContentPolicy()
|
const { faviconUrlTemplate, setFaviconUrlTemplate } = useContentPolicy()
|
||||||
const { allowInsecureConnection, updateAllowInsecureConnection } = useUserPreferences()
|
|
||||||
const [filterOutOnionRelays, setFilterOutOnionRelays] = useState(
|
const [filterOutOnionRelays, setFilterOutOnionRelays] = useState(
|
||||||
storage.getFilterOutOnionRelays()
|
storage.getFilterOutOnionRelays()
|
||||||
)
|
)
|
||||||
|
|
@ -47,19 +45,6 @@ const SystemSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-h-9 items-center justify-between px-4">
|
|
||||||
<Label htmlFor="allow-insecure-connection" className="text-base font-normal">
|
|
||||||
<div>{t('Allow insecure connections')}</div>
|
|
||||||
<div className="text-muted-foreground">
|
|
||||||
{t('Allow insecure connections description')}
|
|
||||||
</div>
|
|
||||||
</Label>
|
|
||||||
<Switch
|
|
||||||
id="allow-insecure-connection"
|
|
||||||
checked={allowInsecureConnection}
|
|
||||||
onCheckedChange={updateAllowInsecureConnection}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 px-4">
|
<div className="space-y-2 px-4">
|
||||||
<DefaultRelaysSetting />
|
<DefaultRelaysSetting />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,7 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
|
||||||
publish,
|
publish,
|
||||||
updateMuteListEvent,
|
updateMuteListEvent,
|
||||||
nip04Decrypt,
|
nip04Decrypt,
|
||||||
nip44Encrypt,
|
nip04Encrypt
|
||||||
nip44Decrypt
|
|
||||||
} = useNostr()
|
} = useNostr()
|
||||||
const [tags, setTags] = useState<string[][]>([])
|
const [tags, setTags] = useState<string[][]>([])
|
||||||
const [privateTags, setPrivateTags] = useState<string[][]>([])
|
const [privateTags, setPrivateTags] = useState<string[][]>([])
|
||||||
|
|
@ -57,53 +56,28 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [changing, setChanging] = useState(false)
|
const [changing, setChanging] = useState(false)
|
||||||
|
|
||||||
const getPrivateTags = useCallback(
|
const getPrivateTags = useCallback(
|
||||||
async (
|
async (muteListEvent: Event) => {
|
||||||
muteListEvent: Event
|
if (!muteListEvent.content) return []
|
||||||
): Promise<{ privateTags: string[][]; wasNip04: boolean }> => {
|
|
||||||
if (!muteListEvent.content) return { privateTags: [], wasNip04: false }
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const wasNip04 = muteListEvent.content.includes('?iv=')
|
|
||||||
const storedPlainText = await indexedDb.getDecryptedContent(muteListEvent.id)
|
const storedPlainText = await indexedDb.getDecryptedContent(muteListEvent.id)
|
||||||
|
|
||||||
let plainText: string
|
let plainText: string
|
||||||
if (storedPlainText) {
|
if (storedPlainText) {
|
||||||
console.log('[MuteList] Using cached decrypted content for event', muteListEvent.id)
|
|
||||||
plainText = storedPlainText
|
plainText = storedPlainText
|
||||||
} else {
|
} else {
|
||||||
console.log('[MuteList] Decrypting content with', wasNip04 ? 'NIP-04' : 'NIP-44', 'for event', muteListEvent.id)
|
plainText = await nip04Decrypt(muteListEvent.pubkey, muteListEvent.content)
|
||||||
plainText = wasNip04
|
|
||||||
? await nip04Decrypt(muteListEvent.pubkey, muteListEvent.content)
|
|
||||||
: await nip44Decrypt(muteListEvent.pubkey, muteListEvent.content)
|
|
||||||
await indexedDb.putDecryptedContent(muteListEvent.id, plainText)
|
await indexedDb.putDecryptedContent(muteListEvent.id, plainText)
|
||||||
}
|
}
|
||||||
|
|
||||||
const privateTags = z.array(z.array(z.string())).parse(JSON.parse(plainText))
|
const privateTags = z.array(z.array(z.string())).parse(JSON.parse(plainText))
|
||||||
console.log('[MuteList] Decrypted privateTags count:', privateTags.length, 'wasNip04:', wasNip04)
|
return privateTags
|
||||||
return { privateTags, wasNip04 }
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to decrypt mute list content', error)
|
console.error('Failed to decrypt mute list content', error)
|
||||||
return { privateTags: [], wasNip04: false }
|
return []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[nip04Decrypt, nip44Decrypt]
|
[nip04Decrypt]
|
||||||
)
|
|
||||||
|
|
||||||
const migrateToNip44 = useCallback(
|
|
||||||
async (muteListEvent: Event, privateTags: string[][]) => {
|
|
||||||
if (!accountPubkey) return
|
|
||||||
console.log('[MuteList] Migrating from NIP-04 to NIP-44, privateTags count:', privateTags.length)
|
|
||||||
try {
|
|
||||||
const cipherText = await nip44Encrypt(accountPubkey, JSON.stringify(privateTags))
|
|
||||||
const newMuteListDraftEvent = createMuteListDraftEvent(muteListEvent.tags, cipherText)
|
|
||||||
const event = await publish(newMuteListDraftEvent)
|
|
||||||
console.log('[MuteList] Migration successful, new event id:', event.id)
|
|
||||||
await updateMuteListEvent(event, privateTags)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[MuteList] Failed to migrate to NIP-44', error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[accountPubkey, nip44Encrypt, publish, updateMuteListEvent]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -114,16 +88,11 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { privateTags, wasNip04 } = await getPrivateTags(muteListEvent).catch(() => ({
|
const privateTags = await getPrivateTags(muteListEvent).catch(() => {
|
||||||
privateTags: [] as string[][],
|
return []
|
||||||
wasNip04: false
|
})
|
||||||
}))
|
|
||||||
setPrivateTags(privateTags)
|
setPrivateTags(privateTags)
|
||||||
setTags(muteListEvent.tags)
|
setTags(muteListEvent.tags)
|
||||||
|
|
||||||
if (wasNip04 && privateTags.length > 0) {
|
|
||||||
migrateToNip44(muteListEvent, privateTags)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
updateMuteTags()
|
updateMuteTags()
|
||||||
}, [muteListEvent])
|
}, [muteListEvent])
|
||||||
|
|
@ -174,14 +143,8 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const newTags = (muteListEvent?.tags ?? []).concat([['p', pubkey]])
|
const newTags = (muteListEvent?.tags ?? []).concat([['p', pubkey]])
|
||||||
const { privateTags } = muteListEvent
|
const newMuteListEvent = await publishNewMuteListEvent(newTags, muteListEvent?.content)
|
||||||
? await getPrivateTags(muteListEvent)
|
const privateTags = await getPrivateTags(newMuteListEvent)
|
||||||
: { privateTags: [] }
|
|
||||||
const cipherText =
|
|
||||||
privateTags.length > 0
|
|
||||||
? await nip44Encrypt(accountPubkey, JSON.stringify(privateTags))
|
|
||||||
: ''
|
|
||||||
const newMuteListEvent = await publishNewMuteListEvent(newTags, cipherText)
|
|
||||||
await updateMuteListEvent(newMuteListEvent, privateTags)
|
await updateMuteListEvent(newMuteListEvent, privateTags)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errors = formatError(error)
|
const errors = formatError(error)
|
||||||
|
|
@ -200,15 +163,13 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
|
||||||
try {
|
try {
|
||||||
const muteListEvent = await client.fetchMuteListEvent(accountPubkey)
|
const muteListEvent = await client.fetchMuteListEvent(accountPubkey)
|
||||||
checkMuteListEvent(muteListEvent)
|
checkMuteListEvent(muteListEvent)
|
||||||
const { privateTags } = muteListEvent
|
const privateTags = muteListEvent ? await getPrivateTags(muteListEvent) : []
|
||||||
? await getPrivateTags(muteListEvent)
|
|
||||||
: { privateTags: [] as string[][] }
|
|
||||||
if (privateTags.some(([tagName, tagValue]) => tagName === 'p' && tagValue === pubkey)) {
|
if (privateTags.some(([tagName, tagValue]) => tagName === 'p' && tagValue === pubkey)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPrivateTags = privateTags.concat([['p', pubkey]])
|
const newPrivateTags = privateTags.concat([['p', pubkey]])
|
||||||
const cipherText = await nip44Encrypt(accountPubkey, JSON.stringify(newPrivateTags))
|
const cipherText = await nip04Encrypt(accountPubkey, JSON.stringify(newPrivateTags))
|
||||||
const newMuteListEvent = await publishNewMuteListEvent(muteListEvent?.tags ?? [], cipherText)
|
const newMuteListEvent = await publishNewMuteListEvent(muteListEvent?.tags ?? [], cipherText)
|
||||||
await updateMuteListEvent(newMuteListEvent, newPrivateTags)
|
await updateMuteListEvent(newMuteListEvent, newPrivateTags)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -229,11 +190,11 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
|
||||||
const muteListEvent = await client.fetchMuteListEvent(accountPubkey)
|
const muteListEvent = await client.fetchMuteListEvent(accountPubkey)
|
||||||
if (!muteListEvent) return
|
if (!muteListEvent) return
|
||||||
|
|
||||||
const { privateTags } = await getPrivateTags(muteListEvent)
|
const privateTags = await getPrivateTags(muteListEvent)
|
||||||
const newPrivateTags = privateTags.filter((tag) => tag[0] !== 'p' || tag[1] !== pubkey)
|
const newPrivateTags = privateTags.filter((tag) => tag[0] !== 'p' || tag[1] !== pubkey)
|
||||||
let cipherText = muteListEvent.content
|
let cipherText = muteListEvent.content
|
||||||
if (newPrivateTags.length !== privateTags.length) {
|
if (newPrivateTags.length !== privateTags.length) {
|
||||||
cipherText = await nip44Encrypt(accountPubkey, JSON.stringify(newPrivateTags))
|
cipherText = await nip04Encrypt(accountPubkey, JSON.stringify(newPrivateTags))
|
||||||
}
|
}
|
||||||
|
|
||||||
const newMuteListEvent = await publishNewMuteListEvent(
|
const newMuteListEvent = await publishNewMuteListEvent(
|
||||||
|
|
@ -259,13 +220,13 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
|
||||||
const muteListEvent = await client.fetchMuteListEvent(accountPubkey)
|
const muteListEvent = await client.fetchMuteListEvent(accountPubkey)
|
||||||
if (!muteListEvent) return
|
if (!muteListEvent) return
|
||||||
|
|
||||||
const { privateTags } = await getPrivateTags(muteListEvent)
|
const privateTags = await getPrivateTags(muteListEvent)
|
||||||
const newPrivateTags = privateTags.filter((tag) => tag[0] !== 'p' || tag[1] !== pubkey)
|
const newPrivateTags = privateTags.filter((tag) => tag[0] !== 'p' || tag[1] !== pubkey)
|
||||||
if (newPrivateTags.length === privateTags.length) {
|
if (newPrivateTags.length === privateTags.length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const cipherText = await nip44Encrypt(accountPubkey, JSON.stringify(newPrivateTags))
|
const cipherText = await nip04Encrypt(accountPubkey, JSON.stringify(newPrivateTags))
|
||||||
const newMuteListEvent = await publishNewMuteListEvent(
|
const newMuteListEvent = await publishNewMuteListEvent(
|
||||||
muteListEvent.tags
|
muteListEvent.tags
|
||||||
.filter((tag) => tag[0] !== 'p' || tag[1] !== pubkey)
|
.filter((tag) => tag[0] !== 'p' || tag[1] !== pubkey)
|
||||||
|
|
@ -296,11 +257,11 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { privateTags } = await getPrivateTags(muteListEvent)
|
const privateTags = await getPrivateTags(muteListEvent)
|
||||||
const newPrivateTags = privateTags
|
const newPrivateTags = privateTags
|
||||||
.filter((tag) => tag[0] !== 'p' || tag[1] !== pubkey)
|
.filter((tag) => tag[0] !== 'p' || tag[1] !== pubkey)
|
||||||
.concat([['p', pubkey]])
|
.concat([['p', pubkey]])
|
||||||
const cipherText = await nip44Encrypt(accountPubkey, JSON.stringify(newPrivateTags))
|
const cipherText = await nip04Encrypt(accountPubkey, JSON.stringify(newPrivateTags))
|
||||||
const newMuteListEvent = await publishNewMuteListEvent(newTags, cipherText)
|
const newMuteListEvent = await publishNewMuteListEvent(newTags, cipherText)
|
||||||
await updateMuteListEvent(newMuteListEvent, newPrivateTags)
|
await updateMuteListEvent(newMuteListEvent, newPrivateTags)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -68,20 +68,6 @@ export class BunkerSigner implements ISigner {
|
||||||
return await this.signer.nip04Decrypt(pubkey, cipherText)
|
return await this.signer.nip04Decrypt(pubkey, cipherText)
|
||||||
}
|
}
|
||||||
|
|
||||||
async nip44Encrypt(pubkey: string, plainText: string) {
|
|
||||||
if (!this.signer) {
|
|
||||||
throw new Error('Not logged in')
|
|
||||||
}
|
|
||||||
return await this.signer.nip44Encrypt(pubkey, plainText)
|
|
||||||
}
|
|
||||||
|
|
||||||
async nip44Decrypt(pubkey: string, cipherText: string) {
|
|
||||||
if (!this.signer) {
|
|
||||||
throw new Error('Not logged in')
|
|
||||||
}
|
|
||||||
return await this.signer.nip44Decrypt(pubkey, cipherText)
|
|
||||||
}
|
|
||||||
|
|
||||||
getClientSecretKey() {
|
getClientSecretKey() {
|
||||||
return bytesToHex(this.clientSecretKey)
|
return bytesToHex(this.clientSecretKey)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,8 +81,6 @@ type TNostrContext = {
|
||||||
signEvent: (draftEvent: TDraftEvent) => Promise<VerifiedEvent>
|
signEvent: (draftEvent: TDraftEvent) => Promise<VerifiedEvent>
|
||||||
nip04Encrypt: (pubkey: string, plainText: string) => Promise<string>
|
nip04Encrypt: (pubkey: string, plainText: string) => Promise<string>
|
||||||
nip04Decrypt: (pubkey: string, cipherText: string) => Promise<string>
|
nip04Decrypt: (pubkey: string, cipherText: string) => Promise<string>
|
||||||
nip44Encrypt: (pubkey: string, plainText: string) => Promise<string>
|
|
||||||
nip44Decrypt: (pubkey: string, cipherText: string) => Promise<string>
|
|
||||||
startLogin: () => void
|
startLogin: () => void
|
||||||
checkLogin: <T>(cb?: () => T) => Promise<T | void>
|
checkLogin: <T>(cb?: () => T) => Promise<T | void>
|
||||||
updateRelayListEvent: (relayListEvent: Event) => Promise<void>
|
updateRelayListEvent: (relayListEvent: Event) => Promise<void>
|
||||||
|
|
@ -732,14 +730,6 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||||
return signer?.nip04Decrypt(pubkey, cipherText) ?? ''
|
return signer?.nip04Decrypt(pubkey, cipherText) ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const nip44Encrypt = async (pubkey: string, plainText: string) => {
|
|
||||||
return signer?.nip44Encrypt(pubkey, plainText) ?? ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const nip44Decrypt = async (pubkey: string, cipherText: string) => {
|
|
||||||
return signer?.nip44Decrypt(pubkey, cipherText) ?? ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkLogin = async <T,>(cb?: () => T): Promise<T | void> => {
|
const checkLogin = async <T,>(cb?: () => T): Promise<T | void> => {
|
||||||
if (signer) {
|
if (signer) {
|
||||||
return cb && cb()
|
return cb && cb()
|
||||||
|
|
@ -867,8 +857,6 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||||
signHttpAuth,
|
signHttpAuth,
|
||||||
nip04Encrypt,
|
nip04Encrypt,
|
||||||
nip04Decrypt,
|
nip04Decrypt,
|
||||||
nip44Encrypt,
|
|
||||||
nip44Decrypt,
|
|
||||||
startLogin: () => setOpenLoginDialog(true),
|
startLogin: () => setOpenLoginDialog(true),
|
||||||
checkLogin,
|
checkLogin,
|
||||||
signEvent,
|
signEvent,
|
||||||
|
|
|
||||||
|
|
@ -57,24 +57,4 @@ export class Nip07Signer implements ISigner {
|
||||||
}
|
}
|
||||||
return await this.signer.nip04.decrypt(pubkey, cipherText)
|
return await this.signer.nip04.decrypt(pubkey, cipherText)
|
||||||
}
|
}
|
||||||
|
|
||||||
async nip44Encrypt(pubkey: string, plainText: string) {
|
|
||||||
if (!this.signer) {
|
|
||||||
throw new Error('Should call init() first')
|
|
||||||
}
|
|
||||||
if (!this.signer.nip44?.encrypt) {
|
|
||||||
throw new Error('The extension you are using does not support nip44 encryption')
|
|
||||||
}
|
|
||||||
return await this.signer.nip44.encrypt(pubkey, plainText)
|
|
||||||
}
|
|
||||||
|
|
||||||
async nip44Decrypt(pubkey: string, cipherText: string) {
|
|
||||||
if (!this.signer) {
|
|
||||||
throw new Error('Should call init() first')
|
|
||||||
}
|
|
||||||
if (!this.signer.nip44?.decrypt) {
|
|
||||||
throw new Error('The extension you are using does not support nip44 decryption')
|
|
||||||
}
|
|
||||||
return await this.signer.nip44.decrypt(pubkey, cipherText)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,20 +66,6 @@ export class NostrConnectionSigner implements ISigner {
|
||||||
return await this.signer.nip04Decrypt(pubkey, cipherText)
|
return await this.signer.nip04Decrypt(pubkey, cipherText)
|
||||||
}
|
}
|
||||||
|
|
||||||
async nip44Encrypt(pubkey: string, plainText: string) {
|
|
||||||
if (!this.signer) {
|
|
||||||
throw new Error('Not logged in')
|
|
||||||
}
|
|
||||||
return await this.signer.nip44Encrypt(pubkey, plainText)
|
|
||||||
}
|
|
||||||
|
|
||||||
async nip44Decrypt(pubkey: string, cipherText: string) {
|
|
||||||
if (!this.signer) {
|
|
||||||
throw new Error('Not logged in')
|
|
||||||
}
|
|
||||||
return await this.signer.nip44Decrypt(pubkey, cipherText)
|
|
||||||
}
|
|
||||||
|
|
||||||
getClientSecretKey() {
|
getClientSecretKey() {
|
||||||
return bytesToHex(this.clientSecretKey)
|
return bytesToHex(this.clientSecretKey)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,12 +31,4 @@ export class NpubSigner implements ISigner {
|
||||||
async nip04Decrypt(): Promise<any> {
|
async nip04Decrypt(): Promise<any> {
|
||||||
throw new Error('Not logged in')
|
throw new Error('Not logged in')
|
||||||
}
|
}
|
||||||
|
|
||||||
async nip44Encrypt(): Promise<any> {
|
|
||||||
throw new Error('Not logged in')
|
|
||||||
}
|
|
||||||
|
|
||||||
async nip44Decrypt(): Promise<any> {
|
|
||||||
throw new Error('Not logged in')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { ISigner, TDraftEvent } from '@/types'
|
import { ISigner, TDraftEvent } from '@/types'
|
||||||
import { finalizeEvent, getPublicKey as nGetPublicKey, nip04, nip19 } from 'nostr-tools'
|
import { finalizeEvent, getPublicKey as nGetPublicKey, nip04, nip19 } from 'nostr-tools'
|
||||||
import { v2 as nip44 } from 'nostr-tools/nip44'
|
|
||||||
|
|
||||||
export class NsecSigner implements ISigner {
|
export class NsecSigner implements ISigner {
|
||||||
private privkey: Uint8Array | null = null
|
private privkey: Uint8Array | null = null
|
||||||
|
|
@ -51,20 +50,4 @@ export class NsecSigner implements ISigner {
|
||||||
}
|
}
|
||||||
return nip04.decrypt(this.privkey, pubkey, cipherText)
|
return nip04.decrypt(this.privkey, pubkey, cipherText)
|
||||||
}
|
}
|
||||||
|
|
||||||
async nip44Encrypt(pubkey: string, plainText: string) {
|
|
||||||
if (!this.privkey) {
|
|
||||||
throw new Error('Not logged in')
|
|
||||||
}
|
|
||||||
const conversationKey = nip44.utils.getConversationKey(this.privkey, pubkey)
|
|
||||||
return nip44.encrypt(plainText, conversationKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
async nip44Decrypt(pubkey: string, cipherText: string) {
|
|
||||||
if (!this.privkey) {
|
|
||||||
throw new Error('Not logged in')
|
|
||||||
}
|
|
||||||
const conversationKey = nip44.utils.getConversationKey(this.privkey, pubkey)
|
|
||||||
return nip44.decrypt(cipherText, conversationKey)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,7 @@ export function PinnedUsersProvider({ children }: { children: React.ReactNode })
|
||||||
updatePinnedUsersEvent,
|
updatePinnedUsersEvent,
|
||||||
publish,
|
publish,
|
||||||
nip04Decrypt,
|
nip04Decrypt,
|
||||||
nip44Encrypt,
|
nip04Encrypt
|
||||||
nip44Decrypt
|
|
||||||
} = useNostr()
|
} = useNostr()
|
||||||
const [privateTags, setPrivateTags] = useState<string[][]>([])
|
const [privateTags, setPrivateTags] = useState<string[][]>([])
|
||||||
const pinnedPubkeySet = useMemo(() => {
|
const pinnedPubkeySet = useMemo(() => {
|
||||||
|
|
@ -51,23 +50,6 @@ export function PinnedUsersProvider({ children }: { children: React.ReactNode })
|
||||||
return new Set(getPubkeysFromPTags(pinnedUsersEvent.tags.concat(privateTags)))
|
return new Set(getPubkeysFromPTags(pinnedUsersEvent.tags.concat(privateTags)))
|
||||||
}, [pinnedUsersEvent, privateTags])
|
}, [pinnedUsersEvent, privateTags])
|
||||||
|
|
||||||
const migrateToNip44 = useCallback(
|
|
||||||
async (event: Event, privateTags: string[][]) => {
|
|
||||||
if (!accountPubkey) return
|
|
||||||
console.log('[PinnedUsers] Migrating from NIP-04 to NIP-44, privateTags count:', privateTags.length)
|
|
||||||
try {
|
|
||||||
const cipherText = await nip44Encrypt(accountPubkey, JSON.stringify(privateTags))
|
|
||||||
const draftEvent = createPinnedUsersListDraftEvent(event.tags, cipherText)
|
|
||||||
const newEvent = await publish(draftEvent)
|
|
||||||
console.log('[PinnedUsers] Migration successful, new event id:', newEvent.id)
|
|
||||||
await updatePinnedUsersEvent(newEvent, privateTags)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[PinnedUsers] Failed to migrate to NIP-44', error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[accountPubkey, nip44Encrypt, publish, updatePinnedUsersEvent]
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updatePrivateTags = async () => {
|
const updatePrivateTags = async () => {
|
||||||
if (!pinnedUsersEvent) {
|
if (!pinnedUsersEvent) {
|
||||||
|
|
@ -75,48 +57,37 @@ export function PinnedUsersProvider({ children }: { children: React.ReactNode })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { privateTags, wasNip04 } = await getPrivateTags(pinnedUsersEvent).catch(() => ({
|
const privateTags = await getPrivateTags(pinnedUsersEvent).catch(() => {
|
||||||
privateTags: [] as string[][],
|
return []
|
||||||
wasNip04: false
|
})
|
||||||
}))
|
|
||||||
setPrivateTags(privateTags)
|
setPrivateTags(privateTags)
|
||||||
|
|
||||||
if (wasNip04 && privateTags.length > 0) {
|
|
||||||
migrateToNip44(pinnedUsersEvent, privateTags)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
updatePrivateTags()
|
updatePrivateTags()
|
||||||
}, [pinnedUsersEvent])
|
}, [pinnedUsersEvent])
|
||||||
|
|
||||||
const getPrivateTags = useCallback(
|
const getPrivateTags = useCallback(
|
||||||
async (event: Event): Promise<{ privateTags: string[][]; wasNip04: boolean }> => {
|
async (event: Event) => {
|
||||||
if (!event.content) return { privateTags: [], wasNip04: false }
|
if (!event.content) return []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const wasNip04 = event.content.includes('?iv=')
|
|
||||||
const storedPlainText = await indexedDb.getDecryptedContent(event.id)
|
const storedPlainText = await indexedDb.getDecryptedContent(event.id)
|
||||||
|
|
||||||
let plainText: string
|
let plainText: string
|
||||||
if (storedPlainText) {
|
if (storedPlainText) {
|
||||||
console.log('[PinnedUsers] Using cached decrypted content for event', event.id)
|
|
||||||
plainText = storedPlainText
|
plainText = storedPlainText
|
||||||
} else {
|
} else {
|
||||||
console.log('[PinnedUsers] Decrypting content with', wasNip04 ? 'NIP-04' : 'NIP-44', 'for event', event.id)
|
plainText = await nip04Decrypt(event.pubkey, event.content)
|
||||||
plainText = wasNip04
|
|
||||||
? await nip04Decrypt(event.pubkey, event.content)
|
|
||||||
: await nip44Decrypt(event.pubkey, event.content)
|
|
||||||
await indexedDb.putDecryptedContent(event.id, plainText)
|
await indexedDb.putDecryptedContent(event.id, plainText)
|
||||||
}
|
}
|
||||||
|
|
||||||
const privateTags = z.array(z.array(z.string())).parse(JSON.parse(plainText))
|
const privateTags = z.array(z.array(z.string())).parse(JSON.parse(plainText))
|
||||||
console.log('[PinnedUsers] Decrypted privateTags count:', privateTags.length, 'wasNip04:', wasNip04)
|
return privateTags
|
||||||
return { privateTags, wasNip04 }
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to decrypt pinned users content', error)
|
console.error('Failed to decrypt pinned users content', error)
|
||||||
return { privateTags: [], wasNip04: false }
|
return []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[nip04Decrypt, nip44Decrypt]
|
[nip04Decrypt]
|
||||||
)
|
)
|
||||||
|
|
||||||
const isPinned = useCallback(
|
const isPinned = useCallback(
|
||||||
|
|
@ -158,7 +129,7 @@ export function PinnedUsersProvider({ children }: { children: React.ReactNode })
|
||||||
)
|
)
|
||||||
let newContent = pinnedUsersEvent.content
|
let newContent = pinnedUsersEvent.content
|
||||||
if (newPrivateTags.length !== privateTags.length) {
|
if (newPrivateTags.length !== privateTags.length) {
|
||||||
newContent = await nip44Encrypt(pinnedUsersEvent.pubkey, JSON.stringify(newPrivateTags))
|
newContent = await nip04Encrypt(pinnedUsersEvent.pubkey, JSON.stringify(newPrivateTags))
|
||||||
}
|
}
|
||||||
const draftEvent = createPinnedUsersListDraftEvent(newTags, newContent)
|
const draftEvent = createPinnedUsersListDraftEvent(newTags, newContent)
|
||||||
const newEvent = await publish(draftEvent)
|
const newEvent = await publish(draftEvent)
|
||||||
|
|
@ -177,7 +148,7 @@ export function PinnedUsersProvider({ children }: { children: React.ReactNode })
|
||||||
publish,
|
publish,
|
||||||
updatePinnedUsersEvent,
|
updatePinnedUsersEvent,
|
||||||
privateTags,
|
privateTags,
|
||||||
nip44Encrypt
|
nip04Encrypt
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,6 @@ type TUserPreferencesContext = {
|
||||||
|
|
||||||
quickReactionEmoji: string | TEmoji
|
quickReactionEmoji: string | TEmoji
|
||||||
updateQuickReactionEmoji: (emoji: string | TEmoji) => void
|
updateQuickReactionEmoji: (emoji: string | TEmoji) => void
|
||||||
|
|
||||||
allowInsecureConnection: boolean
|
|
||||||
updateAllowInsecureConnection: (allow: boolean) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserPreferencesContext = createContext<TUserPreferencesContext | undefined>(undefined)
|
const UserPreferencesContext = createContext<TUserPreferencesContext | undefined>(undefined)
|
||||||
|
|
@ -49,10 +46,6 @@ export function UserPreferencesProvider({ children }: { children: React.ReactNod
|
||||||
const [quickReaction, setQuickReaction] = useState(storage.getQuickReaction())
|
const [quickReaction, setQuickReaction] = useState(storage.getQuickReaction())
|
||||||
const [quickReactionEmoji, setQuickReactionEmoji] = useState(storage.getQuickReactionEmoji())
|
const [quickReactionEmoji, setQuickReactionEmoji] = useState(storage.getQuickReactionEmoji())
|
||||||
|
|
||||||
const [allowInsecureConnection, setAllowInsecureConnection] = useState(
|
|
||||||
storage.getAllowInsecureConnection()
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isSmallScreen && enableSingleColumnLayout) {
|
if (!isSmallScreen && enableSingleColumnLayout) {
|
||||||
document.documentElement.style.setProperty('overflow-y', 'scroll')
|
document.documentElement.style.setProperty('overflow-y', 'scroll')
|
||||||
|
|
@ -86,11 +79,6 @@ export function UserPreferencesProvider({ children }: { children: React.ReactNod
|
||||||
storage.setQuickReactionEmoji(emoji)
|
storage.setQuickReactionEmoji(emoji)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateAllowInsecureConnection = (allow: boolean) => {
|
|
||||||
setAllowInsecureConnection(allow)
|
|
||||||
storage.setAllowInsecureConnection(allow)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserPreferencesContext.Provider
|
<UserPreferencesContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
|
@ -105,9 +93,7 @@ export function UserPreferencesProvider({ children }: { children: React.ReactNod
|
||||||
quickReaction,
|
quickReaction,
|
||||||
updateQuickReaction,
|
updateQuickReaction,
|
||||||
quickReactionEmoji,
|
quickReactionEmoji,
|
||||||
updateQuickReactionEmoji,
|
updateQuickReactionEmoji
|
||||||
allowInsecureConnection,
|
|
||||||
updateAllowInsecureConnection
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,6 @@ class LocalStorageService {
|
||||||
private enableSingleColumnLayout: boolean = true
|
private enableSingleColumnLayout: boolean = true
|
||||||
private faviconUrlTemplate: string = DEFAULT_FAVICON_URL_TEMPLATE
|
private faviconUrlTemplate: string = DEFAULT_FAVICON_URL_TEMPLATE
|
||||||
private filterOutOnionRelays: boolean = !isTorBrowser()
|
private filterOutOnionRelays: boolean = !isTorBrowser()
|
||||||
private allowInsecureConnection: boolean = false
|
|
||||||
private quickReaction: boolean = false
|
private quickReaction: boolean = false
|
||||||
private quickReactionEmoji: string | TEmoji = '+'
|
private quickReactionEmoji: string | TEmoji = '+'
|
||||||
private nsfwDisplayPolicy: TNsfwDisplayPolicy = NSFW_DISPLAY_POLICY.HIDE_CONTENT
|
private nsfwDisplayPolicy: TNsfwDisplayPolicy = NSFW_DISPLAY_POLICY.HIDE_CONTENT
|
||||||
|
|
@ -254,9 +253,6 @@ class LocalStorageService {
|
||||||
this.filterOutOnionRelays = filterOutOnionRelaysStr !== 'false'
|
this.filterOutOnionRelays = filterOutOnionRelaysStr !== 'false'
|
||||||
}
|
}
|
||||||
|
|
||||||
this.allowInsecureConnection =
|
|
||||||
window.localStorage.getItem(StorageKey.ALLOW_INSECURE_CONNECTION) === 'true'
|
|
||||||
|
|
||||||
this.quickReaction = window.localStorage.getItem(StorageKey.QUICK_REACTION) === 'true'
|
this.quickReaction = window.localStorage.getItem(StorageKey.QUICK_REACTION) === 'true'
|
||||||
const quickReactionEmojiStr =
|
const quickReactionEmojiStr =
|
||||||
window.localStorage.getItem(StorageKey.QUICK_REACTION_EMOJI) ?? '+'
|
window.localStorage.getItem(StorageKey.QUICK_REACTION_EMOJI) ?? '+'
|
||||||
|
|
@ -654,15 +650,6 @@ class LocalStorageService {
|
||||||
window.localStorage.setItem(StorageKey.FILTER_OUT_ONION_RELAYS, filterOut.toString())
|
window.localStorage.setItem(StorageKey.FILTER_OUT_ONION_RELAYS, filterOut.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllowInsecureConnection() {
|
|
||||||
return this.allowInsecureConnection
|
|
||||||
}
|
|
||||||
|
|
||||||
setAllowInsecureConnection(allow: boolean) {
|
|
||||||
this.allowInsecureConnection = allow
|
|
||||||
window.localStorage.setItem(StorageKey.ALLOW_INSECURE_CONNECTION, allow.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
getQuickReaction() {
|
getQuickReaction() {
|
||||||
return this.quickReaction
|
return this.quickReaction
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,5 @@
|
||||||
import { ExtendedKind } from '@/constants'
|
import { ExtendedKind } from '@/constants'
|
||||||
import {
|
import { getEventKey, getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event'
|
||||||
getEventKey,
|
|
||||||
getNoteBech32Id,
|
|
||||||
getReplaceableCoordinateFromEvent,
|
|
||||||
isReplaceableEvent
|
|
||||||
} from '@/lib/event'
|
|
||||||
import { getZapInfoFromEvent } from '@/lib/event-metadata'
|
import { getZapInfoFromEvent } from '@/lib/event-metadata'
|
||||||
import { getDefaultRelayUrls } from '@/lib/relay'
|
import { getDefaultRelayUrls } from '@/lib/relay'
|
||||||
import { getEmojiInfosFromEmojiTags, tagNameEquals } from '@/lib/tag'
|
import { getEmojiInfosFromEmojiTags, tagNameEquals } from '@/lib/tag'
|
||||||
|
|
@ -15,13 +10,7 @@ import { Event, Filter, kinds } from 'nostr-tools'
|
||||||
|
|
||||||
export type TStuffStats = {
|
export type TStuffStats = {
|
||||||
likeIdSet: Set<string>
|
likeIdSet: Set<string>
|
||||||
likes: {
|
likes: { id: string; pubkey: string; created_at: number; emoji: TEmoji | string }[]
|
||||||
id: string
|
|
||||||
eventId: string
|
|
||||||
pubkey: string
|
|
||||||
created_at: number
|
|
||||||
emoji: TEmoji | string
|
|
||||||
}[]
|
|
||||||
repostPubkeySet: Set<string>
|
repostPubkeySet: Set<string>
|
||||||
reposts: { id: string; pubkey: string; created_at: number }[]
|
reposts: { id: string; pubkey: string; created_at: number }[]
|
||||||
zapPrSet: Set<string>
|
zapPrSet: Set<string>
|
||||||
|
|
@ -280,13 +269,7 @@ class StuffStatsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
likeIdSet.add(evt.id)
|
likeIdSet.add(evt.id)
|
||||||
likes.push({
|
likes.push({ id: evt.id, pubkey: evt.pubkey, created_at: evt.created_at, emoji })
|
||||||
id: evt.id,
|
|
||||||
eventId: getNoteBech32Id(evt),
|
|
||||||
pubkey: evt.pubkey,
|
|
||||||
created_at: evt.created_at,
|
|
||||||
emoji
|
|
||||||
})
|
|
||||||
this.stuffStatsMap.set(targetEventKey, { ...old, likeIdSet, likes })
|
this.stuffStatsMap.set(targetEventKey, { ...old, likeIdSet, likes })
|
||||||
return targetEventKey
|
return targetEventKey
|
||||||
}
|
}
|
||||||
|
|
@ -315,13 +298,7 @@ class StuffStatsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
likeIdSet.add(evt.id)
|
likeIdSet.add(evt.id)
|
||||||
likes.push({
|
likes.push({ id: evt.id, pubkey: evt.pubkey, created_at: evt.created_at, emoji })
|
||||||
id: evt.id,
|
|
||||||
eventId: getNoteBech32Id(evt),
|
|
||||||
pubkey: evt.pubkey,
|
|
||||||
created_at: evt.created_at,
|
|
||||||
emoji
|
|
||||||
})
|
|
||||||
this.stuffStatsMap.set(target, { ...old, likeIdSet, likes })
|
this.stuffStatsMap.set(target, { ...old, likeIdSet, likes })
|
||||||
return target
|
return target
|
||||||
}
|
}
|
||||||
|
|
|
||||||
6
src/types/index.d.ts
vendored
6
src/types/index.d.ts
vendored
|
|
@ -91,10 +91,6 @@ export type TNip07 = {
|
||||||
encrypt?: (pubkey: string, plainText: string) => Promise<string>
|
encrypt?: (pubkey: string, plainText: string) => Promise<string>
|
||||||
decrypt?: (pubkey: string, cipherText: string) => Promise<string>
|
decrypt?: (pubkey: string, cipherText: string) => Promise<string>
|
||||||
}
|
}
|
||||||
nip44?: {
|
|
||||||
encrypt?: (pubkey: string, plainText: string) => Promise<string>
|
|
||||||
decrypt?: (pubkey: string, cipherText: string) => Promise<string>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISigner {
|
export interface ISigner {
|
||||||
|
|
@ -102,8 +98,6 @@ export interface ISigner {
|
||||||
signEvent: (draftEvent: TDraftEvent) => Promise<VerifiedEvent>
|
signEvent: (draftEvent: TDraftEvent) => Promise<VerifiedEvent>
|
||||||
nip04Encrypt: (pubkey: string, plainText: string) => Promise<string>
|
nip04Encrypt: (pubkey: string, plainText: string) => Promise<string>
|
||||||
nip04Decrypt: (pubkey: string, cipherText: string) => Promise<string>
|
nip04Decrypt: (pubkey: string, cipherText: string) => Promise<string>
|
||||||
nip44Encrypt: (pubkey: string, plainText: string) => Promise<string>
|
|
||||||
nip44Decrypt: (pubkey: string, cipherText: string) => Promise<string>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TSignerType = 'nsec' | 'nip-07' | 'bunker' | 'browser-nsec' | 'ncryptsec' | 'npub'
|
export type TSignerType = 'nsec' | 'nip-07' | 'bunker' | 'browser-nsec' | 'ncryptsec' | 'npub'
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import path from 'path'
|
|
||||||
import { defineConfig } from 'vitest/config'
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
define: {
|
|
||||||
'import.meta.env.GIT_COMMIT': '"test"',
|
|
||||||
'import.meta.env.APP_VERSION': '"0.0.0"',
|
|
||||||
'import.meta.env.VITE_COMMUNITY_RELAY_SETS': '[]',
|
|
||||||
'import.meta.env.VITE_COMMUNITY_RELAYS': '[]'
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': path.resolve(__dirname, './src')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
test: {
|
|
||||||
include: ['src/**/*.spec.ts']
|
|
||||||
}
|
|
||||||
})
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue