mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-18 22:44:26 +01:00
feat: parse textnote and include metadata as tags
This commit is contained in:
@@ -21,11 +21,12 @@ import UserNameDisplay from '@/components/UserDisplayName';
|
||||
import eventWrapper from '@/core/event';
|
||||
|
||||
import useConfig from '@/nostr/useConfig';
|
||||
import useCommands from '@/nostr/useCommands';
|
||||
import useCommands, { PublishTextNoteParams } from '@/nostr/useCommands';
|
||||
import usePubkey from '@/nostr/usePubkey';
|
||||
import { useHandleCommand } from '@/hooks/useCommandBus';
|
||||
|
||||
import { uploadNostrBuild, uploadFiles } from '@/utils/imageUpload';
|
||||
import parseTextNote from '@/core/parseTextNote';
|
||||
|
||||
type NotePostFormProps = {
|
||||
replyTo?: NostrEvent;
|
||||
@@ -45,6 +46,36 @@ const placeholder = (mode: NotePostFormProps['mode']) => {
|
||||
}
|
||||
};
|
||||
|
||||
const parseAndExtract = (content: string) => {
|
||||
const parsed = parseTextNote(content);
|
||||
|
||||
const hashtags: string[] = [];
|
||||
const pubkeyReferences: string[] = [];
|
||||
const eventReferences: string[] = [];
|
||||
const urlReferences: string[] = [];
|
||||
|
||||
parsed.forEach((node) => {
|
||||
if (node.type === 'HashTag') {
|
||||
hashtags.push(node.tagName);
|
||||
} else if (node.type === 'URL') {
|
||||
urlReferences.push(node.content);
|
||||
} else if (node.type === 'Bech32Entity') {
|
||||
if (node.data.type === 'npub') {
|
||||
pubkeyReferences.push(node.data.data);
|
||||
} else if (node.data.type === 'note') {
|
||||
eventReferences.push(node.data.data);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
hashtags,
|
||||
pubkeyReferences,
|
||||
eventReferences,
|
||||
urlReferences,
|
||||
};
|
||||
};
|
||||
|
||||
const NotePostForm: Component<NotePostFormProps> = (props) => {
|
||||
let textAreaRef: HTMLTextAreaElement | undefined;
|
||||
let fileInputRef: HTMLInputElement | undefined;
|
||||
@@ -113,9 +144,19 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
||||
const mentionedPubkeys: Accessor<string[]> = createMemo(
|
||||
() => replyTo()?.mentionedPubkeysWithoutAuthor() ?? [],
|
||||
);
|
||||
const notifyPubkeys = (pubkey: string): string[] | undefined => {
|
||||
if (props.replyTo === undefined) return undefined;
|
||||
return uniq([props.replyTo.pubkey, ...mentionedPubkeys(), pubkey]);
|
||||
|
||||
const notifyPubkeys = (pubkey: string, pubkeyReferences: string[]): string[] => {
|
||||
if (props.replyTo == null) return pubkeyReferences;
|
||||
return uniq([
|
||||
// 返信先を先頭に
|
||||
props.replyTo.pubkey,
|
||||
// 自分も通知欄に表示するために表示(他アプリとの互換性)
|
||||
pubkey,
|
||||
// その他の返信先
|
||||
...mentionedPubkeys(),
|
||||
// 本文中の公開鍵(npub)
|
||||
...pubkeyReferences,
|
||||
]);
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
@@ -127,15 +168,23 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
||||
console.error('pubkey is not available');
|
||||
return;
|
||||
}
|
||||
let textNote: Parameters<typeof commands.publishTextNote>[0] = {
|
||||
|
||||
const { hashtags, pubkeyReferences, eventReferences, urlReferences } = parseAndExtract(text());
|
||||
|
||||
let textNote: PublishTextNoteParams = {
|
||||
relayUrls: config().relayUrls,
|
||||
pubkey,
|
||||
content: text(),
|
||||
notifyPubkeys: pubkeyReferences,
|
||||
mentionEventIds: eventReferences,
|
||||
hashtags,
|
||||
urls: urlReferences,
|
||||
};
|
||||
|
||||
if (replyTo() != null) {
|
||||
textNote = {
|
||||
...textNote,
|
||||
notifyPubkeys: notifyPubkeys(pubkey),
|
||||
notifyPubkeys: notifyPubkeys(pubkey, pubkeyReferences),
|
||||
rootEventId: replyTo()?.rootEvent()?.id ?? replyTo()?.id,
|
||||
replyEventId: replyTo()?.id,
|
||||
};
|
||||
|
||||
@@ -118,6 +118,15 @@ describe('parseTextNote', () => {
|
||||
assert.deepStrictEqual(parsed, expected);
|
||||
});
|
||||
|
||||
it('should parse text note which includes only a URL', () => {
|
||||
const parsed = parseTextNote('https://syusui-s.github.io/rabbit/');
|
||||
const expected: ParsedTextNoteNode[] = [
|
||||
{ type: 'URL', content: 'https://syusui-s.github.io/rabbit/' },
|
||||
];
|
||||
|
||||
assert.deepStrictEqual(parsed, expected);
|
||||
});
|
||||
|
||||
it('should ignore invalid URL', () => {
|
||||
const parsed = parseTextNote('ws://localhost:port');
|
||||
|
||||
|
||||
@@ -120,6 +120,9 @@ setInterval(() => {
|
||||
setActiveBatchSubscriptions(count);
|
||||
}, 1000);
|
||||
|
||||
const EmptyBatchedEvents = Object.freeze({ events: Object.freeze([]), completed: true });
|
||||
const emptyBatchedEvents = () => EmptyBatchedEvents;
|
||||
|
||||
const { exec } = useBatch<TaskArg, TaskRes>(() => ({
|
||||
interval: 2000,
|
||||
batchSize: 150,
|
||||
@@ -190,8 +193,6 @@ const { exec } = useBatch<TaskArg, TaskRes>(() => ({
|
||||
});
|
||||
};
|
||||
|
||||
const emptyBatchedEvents = () => ({ events: [], completed: true });
|
||||
|
||||
const finalizeTasks = () => {
|
||||
tasks.forEach((task) => {
|
||||
const signal = signals.get(task.id);
|
||||
|
||||
21
src/nostr/useCommands.test.ts
Normal file
21
src/nostr/useCommands.test.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import assert from 'assert';
|
||||
import { describe, it } from 'vitest';
|
||||
import { buildTags } from './useCommands';
|
||||
|
||||
describe('buildTags', () => {
|
||||
it('should place a reply tag as first one if it is an only element', () => {
|
||||
const replyEventId = '6b280916873768d752cb95a0d2787a184926db8b717394c66ae255b221e607a8a';
|
||||
const actual = buildTags({ replyEventId });
|
||||
const expect = [['e', replyEventId, '', 'reply']];
|
||||
|
||||
assert.deepStrictEqual(actual, expect);
|
||||
});
|
||||
|
||||
it("should add a url as a 'r' tag", () => {
|
||||
const urls = ['https://syusui-s.github.io/rabbit/'];
|
||||
const actual = buildTags({ urls });
|
||||
const expect = [['r', 'https://syusui-s.github.io/rabbit/']];
|
||||
|
||||
assert.deepStrictEqual(actual, expect);
|
||||
});
|
||||
});
|
||||
@@ -1,28 +1,27 @@
|
||||
import {
|
||||
getEventHash,
|
||||
type UnsignedEvent,
|
||||
type Event as NostrEvent,
|
||||
type Pub,
|
||||
type Kind,
|
||||
} from 'nostr-tools';
|
||||
import { getEventHash, Kind, type UnsignedEvent, type Pub } from 'nostr-tools';
|
||||
|
||||
import '@/types/nostr.d';
|
||||
// import '@/types/nostr.d';
|
||||
import usePool from '@/nostr/usePool';
|
||||
|
||||
import epoch from '@/utils/epoch';
|
||||
|
||||
export type PublishTextNoteParams = {
|
||||
relayUrls: string[];
|
||||
pubkey: string;
|
||||
content: string;
|
||||
export type TagParams = {
|
||||
tags?: string[][];
|
||||
notifyPubkeys?: string[];
|
||||
rootEventId?: string;
|
||||
mentionEventIds?: string[];
|
||||
replyEventId?: string;
|
||||
hashtags?: string[];
|
||||
urls?: string[];
|
||||
contentWarning?: string;
|
||||
};
|
||||
|
||||
export type PublishTextNoteParams = {
|
||||
relayUrls: string[];
|
||||
pubkey: string;
|
||||
content: string;
|
||||
} & TagParams;
|
||||
|
||||
// NIP-20: Command Result
|
||||
const waitCommandResult = (pub: Pub, relayUrl: string): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -43,8 +42,10 @@ export const buildTags = ({
|
||||
mentionEventIds,
|
||||
replyEventId,
|
||||
contentWarning,
|
||||
hashtags,
|
||||
urls,
|
||||
tags,
|
||||
}: PublishTextNoteParams): string[][] => {
|
||||
}: TagParams): string[][] => {
|
||||
// NIP-10
|
||||
const eTags = [];
|
||||
const pTags = notifyPubkeys?.map((p) => ['p', p]) ?? [];
|
||||
@@ -61,6 +62,14 @@ export const buildTags = ({
|
||||
eTags.push(['e', replyEventId, '', 'reply']);
|
||||
}
|
||||
|
||||
if (hashtags != null) {
|
||||
hashtags.forEach((tag) => otherTags.push(['t', tag]));
|
||||
}
|
||||
|
||||
if (urls != null) {
|
||||
urls.forEach((url) => otherTags.push(['r', url]));
|
||||
}
|
||||
|
||||
if (contentWarning != null) {
|
||||
otherTags.push(['content-warning', contentWarning]);
|
||||
}
|
||||
@@ -162,6 +171,31 @@ const useCommands = () => {
|
||||
};
|
||||
return publishEvent(relayUrls, preSignedEvent);
|
||||
},
|
||||
// useFollowingsのisFetchedが呼ばれたとしても全てのリレーから取得できたとは限らない
|
||||
// 半数以上、あるいは5秒待ってみて応答があればそれを利用するみたいな仕組みが必要か?
|
||||
updateContacts({
|
||||
relayUrls,
|
||||
pubkey,
|
||||
followingPubkeys,
|
||||
content,
|
||||
}: {
|
||||
relayUrls: string[];
|
||||
pubkey: string;
|
||||
followingPubkeys: string[];
|
||||
content: string;
|
||||
}): Promise<Promise<void>[]> {
|
||||
const pTags = followingPubkeys.map((key) => ['p', key]);
|
||||
|
||||
const preSignedEvent: UnsignedEvent = {
|
||||
kind: Kind.Contacts,
|
||||
pubkey,
|
||||
created_at: epoch(),
|
||||
tags: pTags,
|
||||
content,
|
||||
};
|
||||
return publishEvent(relayUrls, preSignedEvent);
|
||||
// TODO publishできたら、invalidateをしないといけない
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
11
src/utils/imageUrl.test.ts
Normal file
11
src/utils/imageUrl.test.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import assert from 'assert';
|
||||
import { describe, it } from 'vitest';
|
||||
import { fixUrl } from './imageUrl';
|
||||
|
||||
describe('fixUrl', () => {
|
||||
it('should return an image url for a given imgur.com URL with additional path', () => {
|
||||
const actual = fixUrl('https://imgur.com/uBf5Qts.jpeg');
|
||||
const expected = 'https://i.imgur.com/uBf5Qtsl.webp';
|
||||
assert.deepStrictEqual(actual, expected);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,12 @@
|
||||
import path from 'path';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user