mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-17 22:14:26 +01:00
update
This commit is contained in:
13
.eslintrc.js
13
.eslintrc.js
@@ -63,7 +63,18 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
tailwindcss: {
|
tailwindcss: {
|
||||||
whitelist: ['form-input'],
|
whitelist: [
|
||||||
|
'form-input',
|
||||||
|
// rabbit parts
|
||||||
|
'nostr-textnote',
|
||||||
|
'author',
|
||||||
|
'author-icon',
|
||||||
|
'author-name',
|
||||||
|
'author-username',
|
||||||
|
'created-at',
|
||||||
|
'actions',
|
||||||
|
'content',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
|
|||||||
11
src/components/EventLink.tsx
Normal file
11
src/components/EventLink.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Component } from 'solid-js';
|
||||||
|
|
||||||
|
type EventLinkProps = {
|
||||||
|
eventId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EventLink: Component<EventLinkProps> = (props) => (
|
||||||
|
<span class="text-blue-500 underline">{props.eventId}</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default EventLink;
|
||||||
@@ -44,7 +44,7 @@ const SideBar: Component = () => {
|
|||||||
content,
|
content,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('ok');
|
console.log('succeeded to post');
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error('error', err);
|
console.error('error', err);
|
||||||
|
|||||||
0
src/components/textNote/Bech32EntityDisplay.tsx
Normal file
0
src/components/textNote/Bech32EntityDisplay.tsx
Normal file
41
src/components/textNote/ContentWarningDisplay.tsx
Normal file
41
src/components/textNote/ContentWarningDisplay.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { createSignal, type Component, type JSX, Show } from 'solid-js';
|
||||||
|
import { ContentWarning } from '@/core/event';
|
||||||
|
|
||||||
|
export type ContentWarningDisplayProps = {
|
||||||
|
contentWarning: ContentWarning;
|
||||||
|
children: JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ContentWarningDisplay: Component<ContentWarningDisplayProps> = (props) => {
|
||||||
|
const [showContentWarning, setShowContentWarning] = createSignal(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show
|
||||||
|
when={!props.contentWarning.contentWarning || showContentWarning()}
|
||||||
|
fallback={
|
||||||
|
<button
|
||||||
|
class="mt-2 w-full rounded border p-2 text-center text-xs text-stone-600 shadow-sm hover:shadow"
|
||||||
|
onClick={() => setShowContentWarning(true)}
|
||||||
|
>
|
||||||
|
表示するにはクリック
|
||||||
|
<Show when={props.contentWarning.reason != null}>
|
||||||
|
<br />
|
||||||
|
<span>理由: {props.contentWarning.reason}</span>
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div>{props.children}</div>
|
||||||
|
<Show when={props.contentWarning.contentWarning}>
|
||||||
|
<button
|
||||||
|
class="text-xs text-stone-600 hover:text-stone-800"
|
||||||
|
onClick={() => setShowContentWarning(false)}
|
||||||
|
>
|
||||||
|
隠す
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContentWarningDisplay;
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Component } from 'solid-js';
|
import { Component, createSignal, Show } from 'solid-js';
|
||||||
|
import { ContentWarning } from '@/core/event';
|
||||||
|
|
||||||
type ImageDisplayProps = {
|
type ImageDisplayProps = {
|
||||||
url: string;
|
url: string;
|
||||||
|
contentWarning: ContentWarning;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fixUrl = (url: URL): string => {
|
const fixUrl = (url: URL): string => {
|
||||||
@@ -20,16 +22,29 @@ const fixUrl = (url: URL): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ImageDisplay: Component<ImageDisplayProps> = (props) => {
|
const ImageDisplay: Component<ImageDisplayProps> = (props) => {
|
||||||
|
const [hidden, setHidden] = createSignal(props.contentWarning.contentWarning);
|
||||||
const url = () => new URL(props.url);
|
const url = () => new URL(props.url);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a class="my-2 block" href={props.url} target="_blank" rel="noopener noreferrer">
|
<Show
|
||||||
<img
|
when={!hidden()}
|
||||||
class="inline-block max-h-64 max-w-full rounded object-contain shadow hover:shadow-md"
|
fallback={
|
||||||
src={fixUrl(url())}
|
<button
|
||||||
alt={props.url}
|
class="rounded bg-stone-300 p-3 text-xs text-stone-600 hover:shadow"
|
||||||
/>
|
onClick={() => setHidden(false)}
|
||||||
</a>
|
>
|
||||||
|
画像を展開する
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<a class="my-2 block" href={props.url} target="_blank" rel="noopener noreferrer">
|
||||||
|
<img
|
||||||
|
class="inline-block max-h-64 max-w-full rounded object-contain shadow hover:shadow-md"
|
||||||
|
src={fixUrl(url())}
|
||||||
|
alt={props.url}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</Show>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Show } from 'solid-js';
|
import { Show } from 'solid-js';
|
||||||
import TextNoteDisplayById from '@/components/textNote/TextNoteDisplayById';
|
import TextNoteDisplayById from '@/components/textNote/TextNoteDisplayById';
|
||||||
import { type MentionedEvent } from '@/core/parseTextNote';
|
import { type MentionedEvent } from '@/core/parseTextNote';
|
||||||
|
import EventLink from '../EventLink';
|
||||||
|
|
||||||
export type MentionedEventDisplayProps = {
|
export type MentionedEventDisplayProps = {
|
||||||
mentionedEvent: MentionedEvent;
|
mentionedEvent: MentionedEvent;
|
||||||
@@ -9,8 +10,15 @@ export type MentionedEventDisplayProps = {
|
|||||||
const MentionedEventDisplay = (props: MentionedEventDisplayProps) => {
|
const MentionedEventDisplay = (props: MentionedEventDisplayProps) => {
|
||||||
return (
|
return (
|
||||||
<Show
|
<Show
|
||||||
when={props.mentionedEvent.marker == null || props.mentionedEvent.marker === 'mention'}
|
when={
|
||||||
fallback={<span class="text-blue-500 underline">{props.mentionedEvent.eventId}</span>}
|
props.mentionedEvent.marker == null ||
|
||||||
|
props.mentionedEvent.marker.length === 0 ||
|
||||||
|
props.mentionedEvent.marker === 'mention'
|
||||||
|
}
|
||||||
|
fallback={() => {
|
||||||
|
console.log(props.mentionedEvent);
|
||||||
|
return <EventLink eventId={props.mentionedEvent.eventId} />;
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div class="my-1 rounded border p-1">
|
<div class="my-1 rounded border p-1">
|
||||||
<TextNoteDisplayById
|
<TextNoteDisplayById
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import PlainTextDisplay from '@/components/textNote/PlainTextDisplay';
|
|||||||
import MentionedUserDisplay from '@/components/textNote/MentionedUserDisplay';
|
import MentionedUserDisplay from '@/components/textNote/MentionedUserDisplay';
|
||||||
import MentionedEventDisplay from '@/components/textNote/MentionedEventDisplay';
|
import MentionedEventDisplay from '@/components/textNote/MentionedEventDisplay';
|
||||||
import ImageDisplay from '@/components/textNote/ImageDisplay';
|
import ImageDisplay from '@/components/textNote/ImageDisplay';
|
||||||
|
import eventWrapper from '@/core/event';
|
||||||
|
import EventLink from '../EventLink';
|
||||||
|
import TextNoteDisplayById from './TextNoteDisplayById';
|
||||||
|
|
||||||
export type TextNoteContentDisplayProps = {
|
export type TextNoteContentDisplayProps = {
|
||||||
event: NostrEvent;
|
event: NostrEvent;
|
||||||
@@ -12,6 +15,7 @@ export type TextNoteContentDisplayProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
|
const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
|
||||||
|
const event = () => eventWrapper(props.event);
|
||||||
return (
|
return (
|
||||||
<For each={parseTextNote(props.event)}>
|
<For each={parseTextNote(props.event)}>
|
||||||
{(item: ParsedTextNoteNode) => {
|
{(item: ParsedTextNoteNode) => {
|
||||||
@@ -25,17 +29,24 @@ const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
|
|||||||
if (props.embedding) {
|
if (props.embedding) {
|
||||||
return <MentionedEventDisplay mentionedEvent={item} />;
|
return <MentionedEventDisplay mentionedEvent={item} />;
|
||||||
}
|
}
|
||||||
return <div>mention</div>;
|
return <EventLink eventId={item.eventId} />;
|
||||||
|
}
|
||||||
|
if (item.type === 'Bech32Entity') {
|
||||||
|
if (item.data.type === 'note' && props.embedding) {
|
||||||
|
return (
|
||||||
|
<div class="my-1 rounded border p-1">
|
||||||
|
<TextNoteDisplayById eventId={item.data.data} actions={false} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <span class="text-blue-500 underline">{item.content}</span>;
|
||||||
}
|
}
|
||||||
if (item.type === 'HashTag') {
|
if (item.type === 'HashTag') {
|
||||||
return <span class="text-blue-500 underline">{item.content}</span>;
|
return <span class="text-blue-500 underline">{item.content}</span>;
|
||||||
}
|
}
|
||||||
if (item.type === 'URL') {
|
if (item.type === 'URL') {
|
||||||
if (item.content.match(/\.(jpeg|jpg|png|gif|webp)$/i)) {
|
if (item.content.match(/\.(jpeg|jpg|png|gif|webp)$/i) && props.embedding) {
|
||||||
if (props.embedding) {
|
return <ImageDisplay url={item.content} contentWarning={event().contentWarning()} />;
|
||||||
return <ImageDisplay url={item.content} />;
|
|
||||||
}
|
|
||||||
return <div>image</div>;
|
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import useDeprecatedReposts from '@/nostr/useDeprecatedReposts';
|
|||||||
import useFormatDate from '@/hooks/useFormatDate';
|
import useFormatDate from '@/hooks/useFormatDate';
|
||||||
|
|
||||||
import ensureNonNull from '@/utils/ensureNonNull';
|
import ensureNonNull from '@/utils/ensureNonNull';
|
||||||
|
import ContentWarningDisplay from './ContentWarningDisplay';
|
||||||
|
|
||||||
export type TextNoteDisplayProps = {
|
export type TextNoteDisplayProps = {
|
||||||
event: NostrEvent;
|
event: NostrEvent;
|
||||||
@@ -31,12 +32,15 @@ export type TextNoteDisplayProps = {
|
|||||||
actions?: boolean;
|
actions?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ContentWarning = (props) => {};
|
||||||
|
|
||||||
const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||||
const { config } = useConfig();
|
const { config } = useConfig();
|
||||||
const formatDate = useFormatDate();
|
const formatDate = useFormatDate();
|
||||||
const commands = useCommands();
|
const commands = useCommands();
|
||||||
const pubkey = usePubkey();
|
const pubkey = usePubkey();
|
||||||
const [showReplyForm, setShowReplyForm] = createSignal(false);
|
const [showReplyForm, setShowReplyForm] = createSignal(false);
|
||||||
|
const [showContentWarning, setShowContentWarning] = createSignal(false);
|
||||||
|
|
||||||
const event = createMemo(() => eventWrapper(props.event));
|
const event = createMemo(() => eventWrapper(props.event));
|
||||||
|
|
||||||
@@ -93,7 +97,8 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
eventId: eventIdNonNull,
|
eventId: eventIdNonNull,
|
||||||
notifyPubkey: props.event.pubkey,
|
notifyPubkey: props.event.pubkey,
|
||||||
})
|
})
|
||||||
.then(() => invalidateDeprecatedReposts());
|
.then(() => invalidateDeprecatedReposts())
|
||||||
|
.catch((err) => console.error('failed to repost: ', err));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -113,12 +118,13 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
eventId: eventIdNonNull,
|
eventId: eventIdNonNull,
|
||||||
notifyPubkey: props.event.pubkey,
|
notifyPubkey: props.event.pubkey,
|
||||||
})
|
})
|
||||||
.then(() => invalidateReactions());
|
.then(() => invalidateReactions())
|
||||||
|
.catch((err) => console.error('failed to publish reaction: ', err));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col">
|
<div class="nostr-textnote flex flex-col">
|
||||||
<div class="flex w-full gap-1">
|
<div class="flex w-full gap-1">
|
||||||
<div class="author-icon h-10 w-10 shrink-0">
|
<div class="author-icon h-10 w-10 shrink-0">
|
||||||
<Show when={author()?.picture}>
|
<Show when={author()?.picture}>
|
||||||
@@ -158,11 +164,13 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class="content whitespace-pre-wrap break-all">
|
<ContentWarningDisplay contentWarning={event().contentWarning()}>
|
||||||
<TextNoteContentDisplay event={props.event} embedding={embedding()} />
|
<div class="content whitespace-pre-wrap break-all">
|
||||||
</div>
|
<TextNoteContentDisplay event={props.event} embedding={embedding()} />
|
||||||
|
</div>
|
||||||
|
</ContentWarningDisplay>
|
||||||
<Show when={actions()}>
|
<Show when={actions()}>
|
||||||
<div class="flex w-48 items-center justify-between gap-8 pt-1">
|
<div class="actions flex w-48 items-center justify-between gap-8 pt-1">
|
||||||
<button
|
<button
|
||||||
class="h-4 w-4 text-zinc-400"
|
class="h-4 w-4 text-zinc-400"
|
||||||
onClick={() => setShowReplyForm((current) => !current)}
|
onClick={() => setShowReplyForm((current) => !current)}
|
||||||
|
|||||||
@@ -2,12 +2,18 @@ import type { Event as NostrEvent } from 'nostr-tools';
|
|||||||
import uniq from 'lodash/uniq';
|
import uniq from 'lodash/uniq';
|
||||||
|
|
||||||
export type EventMarker = 'reply' | 'root' | 'mention';
|
export type EventMarker = 'reply' | 'root' | 'mention';
|
||||||
|
|
||||||
export type TaggedEvent = {
|
export type TaggedEvent = {
|
||||||
id: string;
|
id: string;
|
||||||
relayUrl?: string;
|
relayUrl?: string;
|
||||||
marker: EventMarker;
|
marker: EventMarker;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ContentWarning = {
|
||||||
|
contentWarning: boolean;
|
||||||
|
reason?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const eventWrapper = (event: NostrEvent) => {
|
const eventWrapper = (event: NostrEvent) => {
|
||||||
return {
|
return {
|
||||||
get rawEvent(): NostrEvent {
|
get rawEvent(): NostrEvent {
|
||||||
@@ -58,7 +64,7 @@ const eventWrapper = (event: NostrEvent) => {
|
|||||||
return events.map(([, eventId, relayUrl, marker], index) => ({
|
return events.map(([, eventId, relayUrl, marker], index) => ({
|
||||||
id: eventId,
|
id: eventId,
|
||||||
relayUrl,
|
relayUrl,
|
||||||
marker: (marker as EventMarker) ?? positionToMarker(index),
|
marker: (marker as EventMarker | undefined) ?? positionToMarker(index),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
replyingToEvent(): TaggedEvent | undefined {
|
replyingToEvent(): TaggedEvent | undefined {
|
||||||
@@ -73,6 +79,13 @@ const eventWrapper = (event: NostrEvent) => {
|
|||||||
mentionedPubkeys(): string[] {
|
mentionedPubkeys(): string[] {
|
||||||
return uniq(event.tags.filter(([tagName]) => tagName === 'p').map((e) => e[1]));
|
return uniq(event.tags.filter(([tagName]) => tagName === 'p').map((e) => e[1]));
|
||||||
},
|
},
|
||||||
|
contentWarning(): ContentWarning {
|
||||||
|
const tag = event.tags.find(([tagName]) => tagName === 'content-warning');
|
||||||
|
if (tag == null) return { contentWarning: false };
|
||||||
|
|
||||||
|
const reason = (tag[1]?.length ?? 0) > 0 ? tag[1] : undefined;
|
||||||
|
return { contentWarning: true, reason };
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Event as NostrEvent } from 'nostr-tools';
|
import type { Event as NostrEvent } from 'nostr-tools';
|
||||||
|
import { decode, type ProfilePointer, type EventPointer } from 'nostr-tools/nip19';
|
||||||
|
|
||||||
export type PlainText = {
|
export type PlainText = {
|
||||||
type: 'PlainText';
|
type: 'PlainText';
|
||||||
@@ -7,19 +8,29 @@ export type PlainText = {
|
|||||||
|
|
||||||
export type MentionedEvent = {
|
export type MentionedEvent = {
|
||||||
type: 'MentionedEvent';
|
type: 'MentionedEvent';
|
||||||
tagIndex: number;
|
|
||||||
content: string;
|
content: string;
|
||||||
|
tagIndex: number;
|
||||||
eventId: string;
|
eventId: string;
|
||||||
marker: string | null; // TODO 'reply' | 'root' | 'mention' | null;
|
marker: string | null; // TODO 'reply' | 'root' | 'mention' | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MentionedUser = {
|
export type MentionedUser = {
|
||||||
type: 'MentionedUser';
|
type: 'MentionedUser';
|
||||||
tagIndex: number;
|
|
||||||
content: string;
|
content: string;
|
||||||
|
tagIndex: number;
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Bech32Entity = {
|
||||||
|
type: 'Bech32Entity';
|
||||||
|
content: string;
|
||||||
|
data:
|
||||||
|
| { type: 'npub' | 'note'; data: string }
|
||||||
|
| { type: 'nprofile'; data: ProfilePointer }
|
||||||
|
| { type: 'nevent'; data: EventPointer }
|
||||||
|
| { type: 'naddr'; data: AddressPointer };
|
||||||
|
};
|
||||||
|
|
||||||
export type HashTag = {
|
export type HashTag = {
|
||||||
type: 'HashTag';
|
type: 'HashTag';
|
||||||
content: string;
|
content: string;
|
||||||
@@ -31,16 +42,25 @@ export type UrlText = {
|
|||||||
content: string;
|
content: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ParsedTextNoteNode = PlainText | MentionedEvent | MentionedUser | HashTag | UrlText;
|
export type ParsedTextNoteNode =
|
||||||
|
| PlainText
|
||||||
|
| MentionedEvent
|
||||||
|
| MentionedUser
|
||||||
|
| Bech32Entity
|
||||||
|
| HashTag
|
||||||
|
| UrlText;
|
||||||
|
|
||||||
export type ParsedTextNote = ParsedTextNoteNode[];
|
export type ParsedTextNote = ParsedTextNoteNode[];
|
||||||
|
|
||||||
export const parseTextNote = (event: NostrEvent): ParsedTextNote => {
|
export const parseTextNote = (event: NostrEvent): ParsedTextNote => {
|
||||||
const matches = [
|
const matches = [
|
||||||
...event.content.matchAll(/(?:#\[(?<idx>\d+)\])/g),
|
...event.content.matchAll(/(?:#\[(?<idx>\d+)\])/g),
|
||||||
...event.content.matchAll(/#(?<hashtag>[^[\]()\d\s][^[\]()\s]+)/g),
|
...event.content.matchAll(/#(?<hashtag>[^[-`:-@!-/{-~\d\s][^[-`:-@!-/{-~\s]+)/g),
|
||||||
...event.content.matchAll(
|
...event.content.matchAll(
|
||||||
/(?<url>https?:\/\/[-a-zA-Z0-9.]+(?:\/[-\w.%:]+|\/)*(?:\?[-\w=.%:&]*)?(?:#[-\w=.%:&]*)?)/g,
|
/(?<nip19>(npub|note|nprofile|nevent|nrelay|naddr)1[ac-hj-np-z02-9]+)/gi,
|
||||||
|
),
|
||||||
|
...event.content.matchAll(
|
||||||
|
/(?<url>(https?|wss?):\/\/[-a-zA-Z0-9.]+(?:\/[-\w.@%:]+|\/)*(?:\?[-\w=.@%:&]*)?(?:#[-\w=.%:&]*)?)/g,
|
||||||
),
|
),
|
||||||
].sort((a, b) => a?.index - b?.index);
|
].sort((a, b) => a?.index - b?.index);
|
||||||
let pos = 0;
|
let pos = 0;
|
||||||
@@ -76,15 +96,28 @@ export const parseTextNote = (event: NostrEvent): ParsedTextNote => {
|
|||||||
};
|
};
|
||||||
result.push(mentionedUser);
|
result.push(mentionedUser);
|
||||||
} else if (tagName === 'e') {
|
} else if (tagName === 'e') {
|
||||||
|
const marker = tag[2] != null && tag[2].length > 0 ? tag[2] : null;
|
||||||
const mentionedEvent: MentionedEvent = {
|
const mentionedEvent: MentionedEvent = {
|
||||||
type: 'MentionedEvent',
|
type: 'MentionedEvent',
|
||||||
tagIndex,
|
tagIndex,
|
||||||
content: match[0],
|
content: match[0],
|
||||||
eventId: tag[1],
|
eventId: tag[1],
|
||||||
marker: tag[2],
|
marker,
|
||||||
};
|
};
|
||||||
result.push(mentionedEvent);
|
result.push(mentionedEvent);
|
||||||
}
|
}
|
||||||
|
} else if (match.groups?.nip19 && match.index >= pos) {
|
||||||
|
try {
|
||||||
|
const decoded = decode(match[0]);
|
||||||
|
const bech32Entity: Bech32Entity = {
|
||||||
|
type: 'Bech32Entity',
|
||||||
|
content: match[0],
|
||||||
|
data: decoded as Bech32Entity['data'],
|
||||||
|
};
|
||||||
|
result.push(bech32Entity);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`failed to parse Bech32 entity (NIP-19) but ignore this: ${match[0]}`);
|
||||||
|
}
|
||||||
} else if (match.groups?.hashtag && match.index >= pos) {
|
} else if (match.groups?.hashtag && match.index >= pos) {
|
||||||
pushPlainText(match.index);
|
pushPlainText(match.index);
|
||||||
const tagName = match.groups?.hashtag;
|
const tagName = match.groups?.hashtag;
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ const useShortcutKeys = ({ shortcuts = defaultShortcut, onShortcut }: UseShortcu
|
|||||||
onMount(() => {
|
onMount(() => {
|
||||||
const handleKeydown = throttle((ev: KeyboardEvent) => {
|
const handleKeydown = throttle((ev: KeyboardEvent) => {
|
||||||
if (ev.type !== 'keydown') return;
|
if (ev.type !== 'keydown') return;
|
||||||
|
if (ev.ctrlKey || ev.altKey || ev.metaKey) return;
|
||||||
if (ev.target instanceof HTMLTextAreaElement || ev.target instanceof HTMLInputElement) return;
|
if (ev.target instanceof HTMLTextAreaElement || ev.target instanceof HTMLInputElement) return;
|
||||||
|
|
||||||
const shortcut = shortcutsMap.get(ev.key);
|
const shortcut = shortcutsMap.get(ev.key);
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const useBatch = <TaskArgs, TaskResult>(
|
|||||||
) => {
|
) => {
|
||||||
const props = createMemo(propsProvider);
|
const props = createMemo(propsProvider);
|
||||||
const batchSize = createMemo(() => props().batchSize ?? 100);
|
const batchSize = createMemo(() => props().batchSize ?? 100);
|
||||||
const interval = createMemo(() => props().interval ?? 1000);
|
const interval = createMemo(() => props().interval ?? 2000);
|
||||||
|
|
||||||
const [seqId, setSeqId] = createSignal<number>(0);
|
const [seqId, setSeqId] = createSignal<number>(0);
|
||||||
const [taskQueue, setTaskQueue] = createSignal<Task<TaskArgs, TaskResult>[]>([]);
|
const [taskQueue, setTaskQueue] = createSignal<Task<TaskArgs, TaskResult>[]>([]);
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const useCommands = () => {
|
|||||||
relayUrls,
|
relayUrls,
|
||||||
pubkey,
|
pubkey,
|
||||||
content,
|
content,
|
||||||
|
tags,
|
||||||
notifyPubkeys,
|
notifyPubkeys,
|
||||||
rootEventId,
|
rootEventId,
|
||||||
mentionEventIds,
|
mentionEventIds,
|
||||||
@@ -52,6 +53,7 @@ const useCommands = () => {
|
|||||||
relayUrls: string[];
|
relayUrls: string[];
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
tags?: string[][];
|
||||||
notifyPubkeys?: string[];
|
notifyPubkeys?: string[];
|
||||||
rootEventId?: string;
|
rootEventId?: string;
|
||||||
mentionEventIds?: string[];
|
mentionEventIds?: string[];
|
||||||
@@ -65,13 +67,13 @@ const useCommands = () => {
|
|||||||
mentionEventIds.forEach((id) => eTags.push(['e', id, '', 'mention']));
|
mentionEventIds.forEach((id) => eTags.push(['e', id, '', 'mention']));
|
||||||
if (replyEventId != null) eTags.push(['e', replyEventId, '', 'reply']);
|
if (replyEventId != null) eTags.push(['e', replyEventId, '', 'reply']);
|
||||||
|
|
||||||
const tags = [...pTags, ...eTags];
|
const mergedTags = [...eTags, ...pTags, ...(tags ?? [])];
|
||||||
|
|
||||||
const preSignedEvent: NostrEvent = {
|
const preSignedEvent: NostrEvent = {
|
||||||
kind: 1,
|
kind: 1,
|
||||||
pubkey,
|
pubkey,
|
||||||
created_at: currentDate(),
|
created_at: currentDate(),
|
||||||
tags,
|
tags: mergedTags,
|
||||||
content,
|
content,
|
||||||
};
|
};
|
||||||
return publishEvent(relayUrls, preSignedEvent);
|
return publishEvent(relayUrls, preSignedEvent);
|
||||||
|
|||||||
@@ -146,10 +146,10 @@ const Home: Component = () => {
|
|||||||
<Column name="日本リレー" columnIndex={3} width="medium">
|
<Column name="日本リレー" columnIndex={3} width="medium">
|
||||||
<Timeline events={localTimeline()} />
|
<Timeline events={localTimeline()} />
|
||||||
</Column>
|
</Column>
|
||||||
<Column name="自分の投稿" columnIndex={4} lastColumn width="medium">
|
<Column name="自分の投稿" columnIndex={4} width="medium">
|
||||||
<Timeline events={myPosts()} />
|
<Timeline events={myPosts()} />
|
||||||
</Column>
|
</Column>
|
||||||
<Column name="自分のいいね" columnIndex={4} lastColumn width="medium">
|
<Column name="自分のいいね" columnIndex={5} lastColumn width="medium">
|
||||||
<Notification events={myReactions()} />
|
<Notification events={myReactions()} />
|
||||||
</Column>
|
</Column>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
2
src/types/nostr.d.ts
vendored
2
src/types/nostr.d.ts
vendored
@@ -14,7 +14,7 @@ type NostrAPI = {
|
|||||||
getRelays?(): Promise<{ [url: string]: { read: boolean; write: boolean } }>;
|
getRelays?(): Promise<{ [url: string]: { read: boolean; write: boolean } }>;
|
||||||
|
|
||||||
/** NIP-04: Encrypted Direct Messages */
|
/** NIP-04: Encrypted Direct Messages */
|
||||||
nip04: {
|
nip04?: {
|
||||||
/** returns ciphertext and iv as specified in nip-04 */
|
/** returns ciphertext and iv as specified in nip-04 */
|
||||||
encrypt(pubkey: string, plaintext: string): Promise<string>;
|
encrypt(pubkey: string, plaintext: string): Promise<string>;
|
||||||
/** takes ciphertext and iv as specified in nip-04 */
|
/** takes ciphertext and iv as specified in nip-04 */
|
||||||
|
|||||||
Reference in New Issue
Block a user