mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-18 22:44:26 +01:00
fix: bug in displaying reply-to
This commit is contained in:
@@ -12,11 +12,13 @@ import EmojiPicker from '@/components/EmojiPicker';
|
|||||||
import UserNameDisplay from '@/components/UserDisplayName';
|
import UserNameDisplay from '@/components/UserDisplayName';
|
||||||
import useConfig from '@/core/useConfig';
|
import useConfig from '@/core/useConfig';
|
||||||
import { useHandleCommand } from '@/hooks/useCommandBus';
|
import { useHandleCommand } from '@/hooks/useCommandBus';
|
||||||
|
import usePersistStatus from '@/hooks/usePersistStatus';
|
||||||
import eventWrapper from '@/nostr/event';
|
import eventWrapper from '@/nostr/event';
|
||||||
import parseTextNote, { ParsedTextNote } from '@/nostr/parseTextNote';
|
import parseTextNote, { ParsedTextNote } from '@/nostr/parseTextNote';
|
||||||
import useCommands, { PublishTextNoteParams } from '@/nostr/useCommands';
|
import useCommands, { PublishTextNoteParams } from '@/nostr/useCommands';
|
||||||
import usePubkey from '@/nostr/usePubkey';
|
import usePubkey from '@/nostr/usePubkey';
|
||||||
import { uploadNostrBuild, uploadFiles } from '@/utils/imageUpload';
|
import { uploadNostrBuild, uploadFiles, uploaders } from '@/utils/imageUpload';
|
||||||
|
import openLink from '@/utils/openLink';
|
||||||
|
|
||||||
type NotePostFormProps = {
|
type NotePostFormProps = {
|
||||||
replyTo?: NostrEvent;
|
replyTo?: NostrEvent;
|
||||||
@@ -106,6 +108,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { config, getEmoji } = useConfig();
|
const { config, getEmoji } = useConfig();
|
||||||
|
const { persistStatus, didAgreeToToS, agreeToToS } = usePersistStatus();
|
||||||
const getPubkey = usePubkey();
|
const getPubkey = usePubkey();
|
||||||
const commands = useCommands();
|
const commands = useCommands();
|
||||||
|
|
||||||
@@ -150,23 +153,24 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const mentionedPubkeys = createMemo(() => replyTo()?.mentionedPubkeysWithoutAuthor() ?? []);
|
const mentionedPubkeysWithoutMe = createMemo(() => {
|
||||||
|
const p = getPubkey();
|
||||||
|
return (
|
||||||
|
replyTo()
|
||||||
|
?.mentionedPubkeys()
|
||||||
|
?.filter((pubkey) => pubkey !== p) ?? []
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const mentionedPubkeysWithoutMe = createMemo(() =>
|
const notifyPubkeys = createMemo(() => {
|
||||||
mentionedPubkeys().filter((pubkey) => pubkey !== getPubkey()),
|
if (props.replyTo == null) return [];
|
||||||
);
|
|
||||||
|
|
||||||
const notifyPubkeys = (pubkey: string, pubkeyReferences: string[]): string[] => {
|
|
||||||
if (props.replyTo == null) return pubkeyReferences;
|
|
||||||
return uniq([
|
return uniq([
|
||||||
// 返信先を先頭に
|
// 返信先を先頭に
|
||||||
props.replyTo.pubkey,
|
props.replyTo.pubkey,
|
||||||
// その他の返信先
|
// その他の返信先
|
||||||
...mentionedPubkeysWithoutMe(),
|
...mentionedPubkeysWithoutMe(),
|
||||||
// 本文中の公開鍵(npub)
|
|
||||||
...pubkeyReferences,
|
|
||||||
]);
|
]);
|
||||||
};
|
});
|
||||||
|
|
||||||
const buildEmojiTags = (emojis: string[]): string[][] => {
|
const buildEmojiTags = (emojis: string[]): string[][] => {
|
||||||
const emojiTags: string[][] = [];
|
const emojiTags: string[][] = [];
|
||||||
@@ -210,7 +214,10 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
|||||||
if (replyTo() != null) {
|
if (replyTo() != null) {
|
||||||
textNote = {
|
textNote = {
|
||||||
...textNote,
|
...textNote,
|
||||||
notifyPubkeys: notifyPubkeys(pubkey, pubkeyReferences),
|
notifyPubkeys: uniq([
|
||||||
|
...notifyPubkeys(),
|
||||||
|
...pubkeyReferences, // 本文中の公開鍵(npub)
|
||||||
|
]),
|
||||||
rootEventId: replyTo()?.rootEvent()?.id ?? replyTo()?.id,
|
rootEventId: replyTo()?.rootEvent()?.id ?? replyTo()?.id,
|
||||||
replyEventId: replyTo()?.id,
|
replyEventId: replyTo()?.id,
|
||||||
};
|
};
|
||||||
@@ -225,6 +232,22 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
|||||||
close();
|
close();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ensureUploaderAgreement = (): boolean => {
|
||||||
|
if (didAgreeToToS('nostrBuild')) return true;
|
||||||
|
|
||||||
|
window.alert(
|
||||||
|
'画像アップローダーの利用規約をお読みください。\n(新しいタブで利用規約を開きます)',
|
||||||
|
);
|
||||||
|
openLink(uploaders.nostrBuild.tos);
|
||||||
|
const didAgree = window.confirm('同意する場合はOKをクリックしてください。');
|
||||||
|
|
||||||
|
if (didAgree) {
|
||||||
|
agreeToToS('nostrBuild');
|
||||||
|
}
|
||||||
|
|
||||||
|
return didAgree;
|
||||||
|
};
|
||||||
|
|
||||||
const handleInput: JSX.EventHandler<HTMLTextAreaElement, InputEvent> = (ev) => {
|
const handleInput: JSX.EventHandler<HTMLTextAreaElement, InputEvent> = (ev) => {
|
||||||
setText(ev.currentTarget.value);
|
setText(ev.currentTarget.value);
|
||||||
resizeTextArea();
|
resizeTextArea();
|
||||||
@@ -246,6 +269,9 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
|||||||
|
|
||||||
const handleChangeFile: JSX.EventHandler<HTMLInputElement, Event> = (ev) => {
|
const handleChangeFile: JSX.EventHandler<HTMLInputElement, Event> = (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
if (uploadFilesMutation.isLoading) return;
|
||||||
|
if (!ensureUploaderAgreement()) return;
|
||||||
|
|
||||||
const files = [...(ev.currentTarget.files ?? [])];
|
const files = [...(ev.currentTarget.files ?? [])];
|
||||||
uploadFilesMutation.mutate(files);
|
uploadFilesMutation.mutate(files);
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
@@ -255,12 +281,14 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
|||||||
const handleDrop: JSX.EventHandler<HTMLTextAreaElement, DragEvent> = (ev) => {
|
const handleDrop: JSX.EventHandler<HTMLTextAreaElement, DragEvent> = (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if (uploadFilesMutation.isLoading) return;
|
if (uploadFilesMutation.isLoading) return;
|
||||||
|
if (!ensureUploaderAgreement()) return;
|
||||||
const files = [...(ev?.dataTransfer?.files ?? [])];
|
const files = [...(ev?.dataTransfer?.files ?? [])];
|
||||||
uploadFilesMutation.mutate(files);
|
uploadFilesMutation.mutate(files);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePaste: JSX.EventHandler<HTMLTextAreaElement, ClipboardEvent> = (ev) => {
|
const handlePaste: JSX.EventHandler<HTMLTextAreaElement, ClipboardEvent> = (ev) => {
|
||||||
if (uploadFilesMutation.isLoading) return;
|
if (uploadFilesMutation.isLoading) return;
|
||||||
|
|
||||||
const items = [...(ev?.clipboardData?.items ?? [])];
|
const items = [...(ev?.clipboardData?.items ?? [])];
|
||||||
|
|
||||||
const files: File[] = [];
|
const files: File[] = [];
|
||||||
@@ -273,6 +301,8 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (files.length === 0) return;
|
if (files.length === 0) return;
|
||||||
|
if (!ensureUploaderAgreement()) return;
|
||||||
|
|
||||||
uploadFilesMutation.mutate(files);
|
uploadFilesMutation.mutate(files);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -296,12 +326,13 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="p-1">
|
<div class="p-1">
|
||||||
<Show when={mentionedPubkeys().length > 0}>
|
<Show when={props.replyTo != null}>
|
||||||
<div>
|
<div>
|
||||||
<For each={mentionedPubkeys()}>
|
<For each={notifyPubkeys()}>
|
||||||
{(pubkey) => (
|
{(pubkey, index) => (
|
||||||
<>
|
<>
|
||||||
<UserNameDisplay pubkey={pubkey} />{' '}
|
<UserNameDisplay pubkey={pubkey} />
|
||||||
|
<Show when={index() !== notifyPubkeys().length - 1}> と </Show>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|||||||
@@ -28,7 +28,14 @@ const EventDisplayById: Component<EventDisplayByIdProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch fallback="投稿が見つかりません">
|
<Switch
|
||||||
|
fallback={
|
||||||
|
<span>
|
||||||
|
投稿が見つかりません
|
||||||
|
{props.eventId}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Match when={hidden()}>{null}</Match>
|
<Match when={hidden()}>{null}</Match>
|
||||||
<Match when={fetchedEvent()} keyed>
|
<Match when={fetchedEvent()} keyed>
|
||||||
{(event) => <EventDisplay event={event} {...restProps} />}
|
{(event) => <EventDisplay event={event} {...restProps} />}
|
||||||
|
|||||||
@@ -1,23 +1,28 @@
|
|||||||
import { Accessor } from 'solid-js';
|
import { Accessor } from 'solid-js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createSignalWithStorage,
|
createStoreWithStorage,
|
||||||
createStorageWithSerializer,
|
createStorageWithSerializer,
|
||||||
} from '@/hooks/createSignalWithStorage';
|
} from '@/hooks/createSignalWithStorage';
|
||||||
|
import { UploaderIds } from '@/utils/imageUpload';
|
||||||
|
|
||||||
type PersistStatus = {
|
type PersistStatus = {
|
||||||
loggedIn: boolean;
|
loggedIn: boolean;
|
||||||
agreeWithNostrBuild: boolean;
|
agreements: Record<UploaderIds, boolean>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UsePersistStatus = {
|
type UsePersistStatus = {
|
||||||
persistStatus: Accessor<PersistStatus>;
|
persistStatus: Accessor<PersistStatus>;
|
||||||
loggedIn: () => void;
|
loggedIn: () => void;
|
||||||
|
agreeToToS: (uploaderId: UploaderIds) => void;
|
||||||
|
didAgreeToToS: (uploaderId: UploaderIds) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const InitialPersistStatus = {
|
const InitialPersistStatus: PersistStatus = {
|
||||||
loggedIn: false,
|
loggedIn: false,
|
||||||
agreeWithNostrBuild: false,
|
agreements: {
|
||||||
|
nostrBuild: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const serializer = (persistStatus: PersistStatus): string => JSON.stringify(persistStatus);
|
const serializer = (persistStatus: PersistStatus): string => JSON.stringify(persistStatus);
|
||||||
@@ -26,7 +31,7 @@ const deserializer = (json: string): PersistStatus => JSON.parse(json) as Persis
|
|||||||
|
|
||||||
const storage = createStorageWithSerializer(() => window.localStorage, serializer, deserializer);
|
const storage = createStorageWithSerializer(() => window.localStorage, serializer, deserializer);
|
||||||
|
|
||||||
const [persistStatus, setPersistStatus] = createSignalWithStorage(
|
const [persistStatus, setPersistStatus] = createStoreWithStorage(
|
||||||
'RabbitPersistStatus',
|
'RabbitPersistStatus',
|
||||||
InitialPersistStatus,
|
InitialPersistStatus,
|
||||||
storage,
|
storage,
|
||||||
@@ -37,12 +42,20 @@ const usePersistStatus = (): UsePersistStatus => {
|
|||||||
setPersistStatus((current) => ({ ...current, loggedIn: true }));
|
setPersistStatus((current) => ({ ...current, loggedIn: true }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const agreeToToS = (key: UploaderIds) => {
|
||||||
|
setPersistStatus('agreements', (current) => ({ ...current, [key]: true }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const didAgreeToToS = (key: UploaderIds): boolean => persistStatus.agreements[key] ?? false;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
persistStatus: () => ({
|
persistStatus: () => ({
|
||||||
...InitialPersistStatus,
|
...InitialPersistStatus,
|
||||||
...persistStatus(),
|
...persistStatus,
|
||||||
}),
|
}),
|
||||||
loggedIn,
|
loggedIn,
|
||||||
|
agreeToToS,
|
||||||
|
didAgreeToToS,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -153,9 +153,6 @@ const eventWrapper = (event: NostrEvent) => {
|
|||||||
mentionedPubkeys(): string[] {
|
mentionedPubkeys(): string[] {
|
||||||
return uniq(this.pTags().map(([, pubkey]) => pubkey));
|
return uniq(this.pTags().map(([, pubkey]) => pubkey));
|
||||||
},
|
},
|
||||||
mentionedPubkeysWithoutAuthor(): string[] {
|
|
||||||
return this.mentionedPubkeys().filter((pubkey) => pubkey !== event.pubkey);
|
|
||||||
},
|
|
||||||
contentWarning(): ContentWarning {
|
contentWarning(): ContentWarning {
|
||||||
const tag = event.tags.find(([tagName]) => tagName === 'content-warning');
|
const tag = event.tags.find(([tagName]) => tagName === 'content-warning');
|
||||||
if (tag == null) return { contentWarning: false };
|
if (tag == null) return { contentWarning: false };
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
export type UploadResult = {
|
||||||
|
imageUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Uploader = {
|
||||||
|
name: string;
|
||||||
|
tos: string;
|
||||||
|
upload: (file: File) => Promise<UploadResult>;
|
||||||
|
};
|
||||||
|
|
||||||
const toHexString = (buff: ArrayBuffer): string => {
|
const toHexString = (buff: ArrayBuffer): string => {
|
||||||
const arr = new Array(buff.byteLength);
|
const arr = new Array(buff.byteLength);
|
||||||
const view = new Int8Array(buff);
|
const view = new Int8Array(buff);
|
||||||
@@ -9,10 +19,6 @@ const toHexString = (buff: ArrayBuffer): string => {
|
|||||||
return arr.join();
|
return arr.join();
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UploadResult = {
|
|
||||||
imageUrl: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const uploadNostrBuild = async (blob: Blob): Promise<UploadResult> => {
|
export const uploadNostrBuild = async (blob: Blob): Promise<UploadResult> => {
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.set('fileToUpload', blob);
|
form.set('fileToUpload', blob);
|
||||||
@@ -56,6 +62,16 @@ export const uploadVoidCat = async (blob: Blob): Promise<any> => {
|
|||||||
return res.json();
|
return res.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const uploaders = {
|
||||||
|
nostrBuild: {
|
||||||
|
name: 'nostr.build',
|
||||||
|
tos: 'https://nostr.build/tos/',
|
||||||
|
upload: uploadNostrBuild,
|
||||||
|
} satisfies Uploader,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type UploaderIds = keyof typeof uploaders;
|
||||||
|
|
||||||
export const uploadFiles =
|
export const uploadFiles =
|
||||||
<T>(uploadFn: (file: Blob) => Promise<T>) =>
|
<T>(uploadFn: (file: Blob) => Promise<T>) =>
|
||||||
(files: File[]): Promise<PromiseSettledResult<Awaited<T>>[]> => {
|
(files: File[]): Promise<PromiseSettledResult<Awaited<T>>[]> => {
|
||||||
|
|||||||
9
src/utils/openLink.ts
Normal file
9
src/utils/openLink.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const openLink = (url: string) => {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = new URL(url).toString();
|
||||||
|
a.target = '_blank';
|
||||||
|
a.rel = 'noreferrer noopener';
|
||||||
|
a.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default openLink;
|
||||||
Reference in New Issue
Block a user