mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-17 14:04:21 +01:00
update
This commit is contained in:
211
src/batchClient.ts
Normal file
211
src/batchClient.ts
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
/**
|
||||||
|
* This file is licensed under MIT license, not AGPL.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2023 Syusui Moyatani
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
import { matchFilter, type Filter, type Event as NostrEvent, type SimplePool } from 'nostr-tools';
|
||||||
|
|
||||||
|
export type BatchExecutorConstructor<Task> = {
|
||||||
|
executor: (reqs: Task[]) => void;
|
||||||
|
interval: number;
|
||||||
|
size: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
let incrementalId = 0;
|
||||||
|
|
||||||
|
const nextId = (): number => {
|
||||||
|
const currentId = incrementalId;
|
||||||
|
incrementalId += 1;
|
||||||
|
return currentId;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ObservableTask<BatchRequest, BatchResponse> {
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
req: BatchRequest;
|
||||||
|
|
||||||
|
res: BatchResponse | undefined;
|
||||||
|
|
||||||
|
isCompleted = false;
|
||||||
|
|
||||||
|
#updateListeners: ((res: BatchResponse) => void)[] = [];
|
||||||
|
|
||||||
|
#completeListeners: (() => void)[] = [];
|
||||||
|
|
||||||
|
#promise: Promise<BatchResponse> | undefined;
|
||||||
|
|
||||||
|
constructor(req: BatchRequest) {
|
||||||
|
this.id = nextId();
|
||||||
|
this.req = req;
|
||||||
|
}
|
||||||
|
|
||||||
|
#executeUpdateListeners() {
|
||||||
|
const { res } = this;
|
||||||
|
if (res != null) {
|
||||||
|
this.#updateListeners.forEach((listener) => {
|
||||||
|
listener(res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(res: BatchResponse) {
|
||||||
|
this.res = res;
|
||||||
|
this.#executeUpdateListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWith(f: (current: BatchResponse | undefined) => BatchResponse) {
|
||||||
|
this.res = f(this.res);
|
||||||
|
this.#executeUpdateListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
complete() {
|
||||||
|
this.isCompleted = true;
|
||||||
|
this.#completeListeners.forEach((listener) => {
|
||||||
|
listener();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdate(f: (res: BatchResponse) => void) {
|
||||||
|
this.#updateListeners.push(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
onComplete(f: () => void) {
|
||||||
|
this.#completeListeners.push(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
toPromise(): Promise<BatchResponse> {
|
||||||
|
if (this.#promise == null) {
|
||||||
|
this.#promise = new Promise((resolve, reject) => {
|
||||||
|
this.onComplete(() => {
|
||||||
|
if (this.res != null) {
|
||||||
|
resolve(this.res);
|
||||||
|
} else {
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.#promise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BatchExecutor<Task> {
|
||||||
|
#executor: (reqs: Task[]) => void;
|
||||||
|
|
||||||
|
#interval: number;
|
||||||
|
|
||||||
|
#size: number;
|
||||||
|
|
||||||
|
#tasks: Task[] = [];
|
||||||
|
|
||||||
|
#timerId: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
constructor({ executor, interval, size }: BatchExecutorConstructor<Task>) {
|
||||||
|
this.#executor = executor;
|
||||||
|
this.#interval = interval;
|
||||||
|
this.#size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
#executeTasks() {
|
||||||
|
this.#executor(this.#tasks);
|
||||||
|
this.#tasks = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
#startTimerIfNotStarted() {
|
||||||
|
if (this.#timerId == null) {
|
||||||
|
this.#timerId = setTimeout(() => {
|
||||||
|
this.#executeTasks();
|
||||||
|
this.stop();
|
||||||
|
}, this.#interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pushTask(task: Task) {
|
||||||
|
this.#tasks.push(task);
|
||||||
|
if (this.#tasks.length < this.#size) {
|
||||||
|
this.#startTimerIfNotStarted();
|
||||||
|
} else {
|
||||||
|
this.#executeTasks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if (this.#timerId != null) {
|
||||||
|
clearTimeout(this.#timerId);
|
||||||
|
this.#timerId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BatchSubscriptionTask = ObservableTask<Filter[], NostrEvent[]>;
|
||||||
|
|
||||||
|
export class BatchSubscription {
|
||||||
|
#batchExecutor: BatchExecutor<BatchSubscriptionTask>;
|
||||||
|
|
||||||
|
constructor(pool: SimplePool, relays: string[]) {
|
||||||
|
this.#batchExecutor = new BatchExecutor<BatchSubscriptionTask>({
|
||||||
|
interval: 2000,
|
||||||
|
size: 50,
|
||||||
|
executor: (tasks) => {
|
||||||
|
const filterTaskMap = new Map<Filter, BatchSubscriptionTask>();
|
||||||
|
tasks.forEach((task) => {
|
||||||
|
const filters = task.req;
|
||||||
|
filters.forEach((filter) => {
|
||||||
|
filterTaskMap.set(filter, task);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const mergedFilter = [...filterTaskMap.keys()];
|
||||||
|
const sub = pool.sub(relays, mergedFilter);
|
||||||
|
const filterEvents = new Map<Filter, NostrEvent[]>();
|
||||||
|
|
||||||
|
sub.on('event', (event: NostrEvent & { id: string }) => {
|
||||||
|
mergedFilter.forEach((filter) => {
|
||||||
|
if (matchFilter(filter, event)) {
|
||||||
|
const task = filterTaskMap.get(filter);
|
||||||
|
if (task == null) {
|
||||||
|
console.error('task for filter not found', filter);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
task.updateWith((current) => {
|
||||||
|
if (current == null) return [event];
|
||||||
|
return [...current, event];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
sub.on('eose', () => {
|
||||||
|
tasks.forEach((task) => {
|
||||||
|
task.complete();
|
||||||
|
});
|
||||||
|
sub.unsub();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sub(filters: Filter[]): BatchSubscriptionTask {
|
||||||
|
const task = new ObservableTask<Filter[], NostrEvent[]>(filters);
|
||||||
|
this.#batchExecutor.pushTask(task);
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Component, JSX } from 'solid-js';
|
import type { Component, JSX } from 'solid-js';
|
||||||
import { useHandleCommand } from '@/hooks/useCommandBus';
|
import { useHandleCommand } from '@/hooks/useCommandBus';
|
||||||
|
|
||||||
type ColumnProps = {
|
export type ColumnProps = {
|
||||||
name: string;
|
name: string;
|
||||||
columnIndex: number;
|
columnIndex: number;
|
||||||
lastColumn?: true;
|
lastColumn?: true;
|
||||||
|
|||||||
@@ -142,6 +142,13 @@ const OtherConfig = () => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleShowImage = () => {
|
||||||
|
setConfig((current) => ({
|
||||||
|
...current,
|
||||||
|
showImage: !(current.showImage ?? true),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold">その他</h3>
|
<h3 class="font-bold">その他</h3>
|
||||||
@@ -153,6 +160,10 @@ const OtherConfig = () => {
|
|||||||
onClick={() => toggleKeepOpenPostForm()}
|
onClick={() => toggleKeepOpenPostForm()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="flex-1">画像をデフォルトで表示する</div>
|
||||||
|
<ToggleButton value={config().showImage} onClick={() => toggleShowImage()} />
|
||||||
|
</div>
|
||||||
{/*
|
{/*
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">リアクションのデフォルト</div>
|
<div class="flex-1">リアクションのデフォルト</div>
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
|||||||
props.textAreaRef?.(el);
|
props.textAreaRef?.(el);
|
||||||
}}
|
}}
|
||||||
name="text"
|
name="text"
|
||||||
class="rounded border-none"
|
class="min-h-[40px] rounded border-none"
|
||||||
rows={4}
|
rows={4}
|
||||||
placeholder={placeholder(mode())}
|
placeholder={placeholder(mode())}
|
||||||
onInput={handleInput}
|
onInput={handleInput}
|
||||||
@@ -260,7 +260,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
|||||||
'w-7': mode() === 'reply',
|
'w-7': mode() === 'reply',
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
area-label="コンテンツ警告を設定"
|
aria-label="コンテンツ警告を設定"
|
||||||
title="コンテンツ警告を設定"
|
title="コンテンツ警告を設定"
|
||||||
onClick={() => setContentWarning((e) => !e)}
|
onClick={() => setContentWarning((e) => !e)}
|
||||||
>
|
>
|
||||||
@@ -278,7 +278,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
|||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
title="画像を投稿"
|
title="画像を投稿"
|
||||||
area-label="画像を投稿"
|
aria-label="画像を投稿"
|
||||||
disabled={fileUploadDisabled()}
|
disabled={fileUploadDisabled()}
|
||||||
onClick={() => fileInputRef?.click()}
|
onClick={() => fileInputRef?.click()}
|
||||||
>
|
>
|
||||||
@@ -295,7 +295,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
|||||||
'w-7': mode() === 'reply',
|
'w-7': mode() === 'reply',
|
||||||
}}
|
}}
|
||||||
type="submit"
|
type="submit"
|
||||||
area-label="投稿"
|
aria-label="投稿"
|
||||||
title="投稿"
|
title="投稿"
|
||||||
disabled={submitDisabled()}
|
disabled={submitDisabled()}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Component, createMemo, Show } from 'solid-js';
|
import { Component, createMemo, Show } from 'solid-js';
|
||||||
import { npubEncode } from 'nostr-tools/nip19';
|
|
||||||
|
|
||||||
import GlobeAlt from 'heroicons/24/outline/globe-alt.svg';
|
import GlobeAlt from 'heroicons/24/outline/globe-alt.svg';
|
||||||
import XMark from 'heroicons/24/outline/x-mark.svg';
|
import XMark from 'heroicons/24/outline/x-mark.svg';
|
||||||
@@ -8,10 +7,11 @@ import Modal from '@/components/Modal';
|
|||||||
import Copy from '@/components/utils/Copy';
|
import Copy from '@/components/utils/Copy';
|
||||||
|
|
||||||
import useProfile from '@/nostr/useProfile';
|
import useProfile from '@/nostr/useProfile';
|
||||||
import useConfig from '@/nostr/useConfig';
|
import npubEncodeFallback from '@/utils/npubEncodeFallback';
|
||||||
|
|
||||||
export type ProfileDisplayProps = {
|
export type ProfileDisplayProps = {
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
|
onClose?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||||
@@ -19,13 +19,17 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
|||||||
pubkey: props.pubkey,
|
pubkey: props.pubkey,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const npub = createMemo(() => npubEncode(props.pubkey));
|
const npub = createMemo(() => npubEncodeFallback(props.pubkey));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal>
|
<Modal onClose={() => props.onClose?.()}>
|
||||||
<div class="max-h-full w-[640px] max-w-full overflow-scroll">
|
<div class="max-h-full w-[640px] max-w-full overflow-scroll">
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<button class="h-8 w-8 text-stone-700">
|
<button
|
||||||
|
class="h-8 w-8 text-stone-700"
|
||||||
|
aria-label="Close"
|
||||||
|
onClick={() => props.onClose?.()}
|
||||||
|
>
|
||||||
<XMark />
|
<XMark />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,16 +42,22 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
|||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex h-[64px] items-center gap-2 px-2">
|
<div class="flex h-[64px] items-center gap-4 px-4">
|
||||||
<div class="mt-[-64px] h-28 w-28 shrink-0 rounded-lg border-2 object-cover">
|
<div class="mt-[-64px] h-28 w-28 shrink-0 rounded-lg bg-stone-400 shadow-md">
|
||||||
<Show when={profile()?.picture} keyed>
|
<Show when={profile()?.picture} keyed>
|
||||||
{(pictureUrl) => <img src={pictureUrl} alt="user icon" class="h-full w-full" />}
|
{(pictureUrl) => (
|
||||||
|
<img
|
||||||
|
src={pictureUrl}
|
||||||
|
alt="user icon"
|
||||||
|
class="h-full w-full rounded-lg object-cover"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="truncate text-xl font-bold">{profile()?.display_name}</div>
|
<div class="truncate text-xl font-bold">{profile()?.display_name}</div>
|
||||||
<div class="shrink-0 text-sm">@{profile()?.name}</div>
|
<div class="shrink-0 text-sm font-bold">@{profile()?.name}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<div class="truncate text-xs">{npub()}</div>
|
<div class="truncate text-xs">{npub()}</div>
|
||||||
@@ -55,23 +65,27 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="max-h-32 overflow-scroll whitespace-pre-wrap px-4 pt-1 text-sm">
|
<div class="max-h-32 overflow-scroll whitespace-pre-wrap px-5 py-2 text-sm">
|
||||||
{profile()?.about}
|
{profile()?.about}
|
||||||
</div>
|
</div>
|
||||||
<ul class="px-4 py-2 text-xs">
|
<ul class="border-t px-5 py-2 text-xs">
|
||||||
<Show when={profile()?.website}>
|
<Show when={profile()?.website}>
|
||||||
<li class="flex items-center gap-1">
|
<li class="flex items-center gap-1">
|
||||||
<span class="inline-block h-4 w-4" area-label="website" title="website">
|
<span class="inline-block h-4 w-4" area-label="website" title="website">
|
||||||
<GlobeAlt />
|
<GlobeAlt />
|
||||||
</span>
|
</span>
|
||||||
<a href={profile()?.website} target="_blank" rel="noreferrer noopener">
|
<a
|
||||||
|
class="text-blue-500 underline"
|
||||||
|
href={profile()?.website}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
>
|
||||||
{profile()?.website}
|
{profile()?.website}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</Show>
|
</Show>
|
||||||
</ul>
|
</ul>
|
||||||
</Show>
|
</Show>
|
||||||
<div class="h-16 border" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, Switch, Match } from 'solid-js';
|
import { Component, Switch, Match } from 'solid-js';
|
||||||
import { npubEncode } from 'nostr-tools/nip19';
|
|
||||||
|
|
||||||
import useProfile from '@/nostr/useProfile';
|
import useProfile from '@/nostr/useProfile';
|
||||||
|
import npubEncodeFallback from '@/utils/npubEncodeFallback';
|
||||||
|
|
||||||
type UserNameDisplayProps = {
|
type UserNameDisplayProps = {
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
@@ -13,7 +13,7 @@ const UserNameDisplay: Component<UserNameDisplayProps> = (props) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch fallback={npubEncode(props.pubkey)}>
|
<Switch fallback={npubEncodeFallback(props.pubkey)}>
|
||||||
<Match when={(profile()?.display_name?.length ?? 0) > 0}>{profile()?.display_name}</Match>
|
<Match when={(profile()?.display_name?.length ?? 0) > 0}>{profile()?.display_name}</Match>
|
||||||
<Match when={(profile()?.name?.length ?? 0) > 0}>@{profile()?.name}</Match>
|
<Match when={(profile()?.name?.length ?? 0) > 0}>@{profile()?.name}</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import UserDisplayName from '@/components/UserDisplayName';
|
|||||||
|
|
||||||
import useProfile from '@/nostr/useProfile';
|
import useProfile from '@/nostr/useProfile';
|
||||||
import useEvent from '@/nostr/useEvent';
|
import useEvent from '@/nostr/useEvent';
|
||||||
import { npubEncode } from 'nostr-tools/nip19';
|
|
||||||
|
|
||||||
type ReactionProps = {
|
type ReactionProps = {
|
||||||
event: NostrEvent;
|
event: NostrEvent;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Show } from 'solid-js';
|
import { Show } from 'solid-js';
|
||||||
import { npubEncode } from 'nostr-tools/nip19';
|
|
||||||
|
|
||||||
import useProfile from '@/nostr/useProfile';
|
import useProfile from '@/nostr/useProfile';
|
||||||
|
import npubEncodeFallback from '@/utils/npubEncodeFallback';
|
||||||
|
|
||||||
export type GeneralUserMentionDisplayProps = {
|
export type GeneralUserMentionDisplayProps = {
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
@@ -13,7 +13,10 @@ const GeneralUserMentionDisplay = (props: GeneralUserMentionDisplayProps) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={(profile()?.name?.length ?? 0) > 0} fallback={`@${npubEncode(props.pubkey)}`}>
|
<Show
|
||||||
|
when={(profile()?.name?.length ?? 0) > 0}
|
||||||
|
fallback={`@${npubEncodeFallback(props.pubkey)}`}
|
||||||
|
>
|
||||||
@{profile()?.name ?? props.pubkey}
|
@{profile()?.name ?? props.pubkey}
|
||||||
</Show>
|
</Show>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
import type { MentionedUser } from '@/core/parseTextNote';
|
import type { MentionedUser } from '@/core/parseTextNote';
|
||||||
import GeneralUserMentionDisplay from '@/components/textNote/GeneralUserMentionDisplay';
|
import GeneralUserMentionDisplay from '@/components/textNote/GeneralUserMentionDisplay';
|
||||||
|
import useModalState from '@/hooks/useModalState';
|
||||||
|
|
||||||
export type MentionedUserDisplayProps = {
|
export type MentionedUserDisplayProps = {
|
||||||
mentionedUser: MentionedUser;
|
mentionedUser: MentionedUser;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MentionedUserDisplay = (props: MentionedUserDisplayProps) => {
|
const MentionedUserDisplay = (props: MentionedUserDisplayProps) => {
|
||||||
|
const { showProfile } = useModalState();
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
showProfile(props.mentionedUser.pubkey);
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<span class="text-blue-500 underline">
|
<button class="inline text-blue-500 underline" onClick={handleClick}>
|
||||||
<GeneralUserMentionDisplay pubkey={props.mentionedUser.pubkey} />
|
<GeneralUserMentionDisplay pubkey={props.mentionedUser.pubkey} />
|
||||||
</span>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import MentionedEventDisplay from '@/components/textNote/MentionedEventDisplay';
|
|||||||
import ImageDisplay from '@/components/textNote/ImageDisplay';
|
import ImageDisplay from '@/components/textNote/ImageDisplay';
|
||||||
import eventWrapper from '@/core/event';
|
import eventWrapper from '@/core/event';
|
||||||
import { isImageUrl } from '@/utils/imageUrl';
|
import { isImageUrl } from '@/utils/imageUrl';
|
||||||
|
import useConfig from '@/nostr/useConfig';
|
||||||
import EventLink from '../EventLink';
|
import EventLink from '../EventLink';
|
||||||
import TextNoteDisplayById from './TextNoteDisplayById';
|
import TextNoteDisplayById from './TextNoteDisplayById';
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ export type TextNoteContentDisplayProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
|
const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
|
||||||
|
const { config } = useConfig();
|
||||||
const event = () => eventWrapper(props.event);
|
const event = () => eventWrapper(props.event);
|
||||||
return (
|
return (
|
||||||
<For each={parseTextNote(props.event)}>
|
<For each={parseTextNote(props.event)}>
|
||||||
@@ -50,7 +52,9 @@ const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
|
|||||||
return (
|
return (
|
||||||
<ImageDisplay
|
<ImageDisplay
|
||||||
url={item.content}
|
url={item.content}
|
||||||
initialHidden={event().contentWarning().contentWarning || !props.embedding}
|
initialHidden={
|
||||||
|
!config().showImage || event().contentWarning().contentWarning || !props.embedding
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ 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 { npubEncode } from 'nostr-tools/nip19';
|
import npubEncodeFallback from '@/utils/npubEncodeFallback';
|
||||||
|
import useModalState from '@/hooks/useModalState';
|
||||||
import UserNameDisplay from '../UserDisplayName';
|
import UserNameDisplay from '../UserDisplayName';
|
||||||
import TextNoteDisplayById from './TextNoteDisplayById';
|
import TextNoteDisplayById from './TextNoteDisplayById';
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
const { config } = useConfig();
|
const { config } = useConfig();
|
||||||
const formatDate = useFormatDate();
|
const formatDate = useFormatDate();
|
||||||
const pubkey = usePubkey();
|
const pubkey = usePubkey();
|
||||||
|
const { showProfile } = useModalState();
|
||||||
|
|
||||||
const [showReplyForm, setShowReplyForm] = createSignal(false);
|
const [showReplyForm, setShowReplyForm] = createSignal(false);
|
||||||
const closeReplyForm = () => setShowReplyForm(false);
|
const closeReplyForm = () => setShowReplyForm(false);
|
||||||
@@ -142,7 +144,10 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<div class="nostr-textnote 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 overflow-hidden object-cover">
|
<button
|
||||||
|
class="author-icon h-10 w-10 shrink-0 overflow-hidden object-cover"
|
||||||
|
onClick={() => showProfile(event().pubkey)}
|
||||||
|
>
|
||||||
<Show when={author()?.picture}>
|
<Show when={author()?.picture}>
|
||||||
{/* TODO 画像は脆弱性回避のためにimgじゃない方法で読み込みたい */}
|
{/* TODO 画像は脆弱性回避のためにimgじゃない方法で読み込みたい */}
|
||||||
<img
|
<img
|
||||||
@@ -152,21 +157,27 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
class="h-10 w-10 rounded"
|
class="h-10 w-10 rounded"
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</button>
|
||||||
<div class="min-w-0 flex-auto">
|
<div class="min-w-0 flex-auto">
|
||||||
<div class="flex justify-between gap-1 text-xs">
|
<div class="flex justify-between gap-1 text-xs">
|
||||||
<div class="author flex min-w-0 truncate">
|
<button
|
||||||
|
class="author flex min-w-0 truncate"
|
||||||
|
onClick={() => showProfile(event().pubkey)}
|
||||||
|
>
|
||||||
{/* TODO link to author */}
|
{/* TODO link to author */}
|
||||||
<Show when={(author()?.display_name?.length ?? 0) > 0}>
|
<Show when={(author()?.display_name?.length ?? 0) > 0}>
|
||||||
<div class="author-name truncate pr-1 font-bold">{author()?.display_name}</div>
|
<div class="author-name truncate pr-1 font-bold">{author()?.display_name}</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class="author-username truncate text-zinc-600">
|
<div class="author-username truncate text-zinc-600">
|
||||||
<Show when={author()?.name != null} fallback={`@${npubEncode(props.event.pubkey)}`}>
|
<Show
|
||||||
|
when={author()?.name != null}
|
||||||
|
fallback={`@${npubEncodeFallback(event().pubkey)}`}
|
||||||
|
>
|
||||||
@{author()?.name}
|
@{author()?.name}
|
||||||
</Show>
|
</Show>
|
||||||
{/* TODO <Match when={author()?.nip05 != null}>@{author()?.nip05}</Match> */}
|
{/* TODO <Match when={author()?.nip05 != null}>@{author()?.nip05}</Match> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
<div class="created-at shrink-0">{createdAt()}</div>
|
<div class="created-at shrink-0">{createdAt()}</div>
|
||||||
</div>
|
</div>
|
||||||
<Show when={showReplyEvent()} keyed>
|
<Show when={showReplyEvent()} keyed>
|
||||||
@@ -180,9 +191,12 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
<div class="text-xs">
|
<div class="text-xs">
|
||||||
<For each={event().mentionedPubkeys()}>
|
<For each={event().mentionedPubkeys()}>
|
||||||
{(replyToPubkey: string) => (
|
{(replyToPubkey: string) => (
|
||||||
<span class="pr-1 text-blue-500 underline">
|
<button
|
||||||
|
class="pr-1 text-blue-500 underline"
|
||||||
|
onClick={() => showProfile(replyToPubkey)}
|
||||||
|
>
|
||||||
<GeneralUserMentionDisplay pubkey={replyToPubkey} />
|
<GeneralUserMentionDisplay pubkey={replyToPubkey} />
|
||||||
</span>
|
</button>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
{'への返信'}
|
{'への返信'}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const Copy: Component<CopyProps> = (props) => {
|
|||||||
</button>
|
</button>
|
||||||
<Show when={showPopup()}>
|
<Show when={showPopup()}>
|
||||||
<div
|
<div
|
||||||
class="absolute left-[-1rem] top-[-1.5rem] rounded-lg
|
class="absolute left-[-1rem] top-[-1.5rem] rounded
|
||||||
bg-rose-300 p-1 text-xs font-bold text-white shadow"
|
bg-rose-300 p-1 text-xs font-bold text-white shadow"
|
||||||
>
|
>
|
||||||
Copied!
|
Copied!
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// import { z } from 'zod';
|
// import { z } from 'zod';
|
||||||
import { type Event as NostrEvent, type Filter } from 'nostr-tools';
|
import { type Event as NostrEvent, type Filter } from 'nostr-tools';
|
||||||
import ColumnComponent from '@/components/Column';
|
import { type ColumnProps } from '@/components/Column';
|
||||||
|
|
||||||
export type NotificationType =
|
export type NotificationType =
|
||||||
// The event which includes ["p", ...] tags.
|
// The event which includes ["p", ...] tags.
|
||||||
@@ -42,7 +42,8 @@ type BulidOptions = {
|
|||||||
// export const buildFilter = (options: BuildOptions) => {};
|
// export const buildFilter = (options: BuildOptions) => {};
|
||||||
|
|
||||||
export type BaseColumn = {
|
export type BaseColumn = {
|
||||||
columnWidth: (typeof ColumnComponent)['width'];
|
title: string;
|
||||||
|
columnWidth: ColumnProps['width'];
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A column which shows posts by following users */
|
/** A column which shows posts by following users */
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createSignal, createEffect, onMount, Accessor, Setter } from 'solid-js';
|
import { createSignal, createEffect, onMount, type Signal } from 'solid-js';
|
||||||
|
import { createStore, SetStoreFunction, type Store, type StoreNode } from 'solid-js/store';
|
||||||
|
|
||||||
type GenericStorage<T> = {
|
type GenericStorage<T> = {
|
||||||
getItem(key: string): T | null;
|
getItem(key: string): T | null;
|
||||||
@@ -28,20 +29,42 @@ export const createSignalWithStorage = <T>(
|
|||||||
key: string,
|
key: string,
|
||||||
initialValue: T,
|
initialValue: T,
|
||||||
storage: GenericStorage<T>,
|
storage: GenericStorage<T>,
|
||||||
): [Accessor<T>, Setter<T>] => {
|
): Signal<T> => {
|
||||||
const [loaded, setLoaded] = createSignal<boolean>(false);
|
const [loaded, setLoaded] = createSignal<boolean>(false);
|
||||||
const [value, setValue] = createSignal<T>(initialValue);
|
const [state, setState] = createSignal(initialValue);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const data = storage.getItem(key);
|
const data = storage.getItem(key);
|
||||||
// If there is no data, default value is used.
|
// If there is no data, default value is used.
|
||||||
if (data != null) setValue(() => data);
|
if (data != null) setState(() => data);
|
||||||
setLoaded(true);
|
setLoaded(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (loaded()) storage.setItem(key, value());
|
if (loaded()) storage.setItem(key, state());
|
||||||
});
|
});
|
||||||
|
|
||||||
return [value, setValue];
|
return [state, setState];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createStoreWithStorage = <T extends StoreNode>(
|
||||||
|
key: string,
|
||||||
|
initialValue: T,
|
||||||
|
storage: GenericStorage<T>,
|
||||||
|
): [Store<T>, SetStoreFunction<T>] => {
|
||||||
|
const [loaded, setLoaded] = createSignal<boolean>(false);
|
||||||
|
const [state, setState] = createStore<T>(initialValue);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const data = storage.getItem(key);
|
||||||
|
// If there is no data, default value is used.
|
||||||
|
if (data != null) setState(data);
|
||||||
|
setLoaded(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (loaded()) storage.setItem(key, state);
|
||||||
|
});
|
||||||
|
|
||||||
|
return [state, setState];
|
||||||
};
|
};
|
||||||
|
|||||||
20
src/hooks/useModalState.ts
Normal file
20
src/hooks/useModalState.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { createSignal, type Signal } from 'solid-js';
|
||||||
|
|
||||||
|
type ModalState =
|
||||||
|
| { type: 'Profile'; pubkey: string }
|
||||||
|
| { type: 'UserTimeline'; pubkey: string }
|
||||||
|
| { type: 'Closed' };
|
||||||
|
|
||||||
|
const [modalState, setModalState] = createSignal<ModalState>({ type: 'Closed' });
|
||||||
|
|
||||||
|
const useModalState = () => {
|
||||||
|
const showProfile = (pubkey: string) => {
|
||||||
|
setModalState({ type: 'Profile', pubkey });
|
||||||
|
};
|
||||||
|
const closeModal = () => {
|
||||||
|
setModalState({ type: 'Closed' });
|
||||||
|
};
|
||||||
|
return { modalState, setModalState, showProfile, closeModal };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useModalState;
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
createSignal,
|
createSignal,
|
||||||
createEffect,
|
|
||||||
createMemo,
|
createMemo,
|
||||||
createRoot,
|
createRoot,
|
||||||
observable,
|
observable,
|
||||||
@@ -8,13 +7,16 @@ import {
|
|||||||
type Signal,
|
type Signal,
|
||||||
} from 'solid-js';
|
} from 'solid-js';
|
||||||
import { type Event as NostrEvent, type Filter, Kind } from 'nostr-tools';
|
import { type Event as NostrEvent, type Filter, Kind } from 'nostr-tools';
|
||||||
|
import { npubEncode } from 'nostr-tools/nip19';
|
||||||
import { createQuery, useQueryClient, type CreateQueryResult } from '@tanstack/solid-query';
|
import { createQuery, useQueryClient, type CreateQueryResult } from '@tanstack/solid-query';
|
||||||
|
|
||||||
import timeout from '@/utils/timeout';
|
import timeout from '@/utils/timeout';
|
||||||
import useBatch, { type Task } from '@/nostr/useBatch';
|
import useBatch, { type Task } from '@/nostr/useBatch';
|
||||||
import eventWrapper from '@/core/event';
|
import eventWrapper from '@/core/event';
|
||||||
import useSubscription from '@/nostr/useSubscription';
|
import useSubscription from '@/nostr/useSubscription';
|
||||||
|
import npubEncodeFallback from '@/utils/npubEncodeFallback';
|
||||||
import useConfig from './useConfig';
|
import useConfig from './useConfig';
|
||||||
|
import usePool from './usePool';
|
||||||
|
|
||||||
type TaskArg =
|
type TaskArg =
|
||||||
| { type: 'Profile'; pubkey: string }
|
| { type: 'Profile'; pubkey: string }
|
||||||
@@ -54,8 +56,8 @@ export type UseProfileProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type UseProfile = {
|
type UseProfile = {
|
||||||
profile: () => Profile | undefined;
|
profile: () => Profile | null;
|
||||||
query: CreateQueryResult<NostrEvent | undefined>;
|
query: CreateQueryResult<NostrEvent | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Textnote
|
// Textnote
|
||||||
@@ -64,8 +66,8 @@ export type UseTextNoteProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type UseTextNote = {
|
export type UseTextNote = {
|
||||||
event: Accessor<NostrEvent | undefined>;
|
event: Accessor<NostrEvent | null>;
|
||||||
query: CreateQueryResult<NostrEvent | undefined>;
|
query: CreateQueryResult<NostrEvent | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reactions
|
// Reactions
|
||||||
@@ -107,10 +109,16 @@ type Following = {
|
|||||||
export type UseFollowings = {
|
export type UseFollowings = {
|
||||||
followings: Accessor<Following[]>;
|
followings: Accessor<Following[]>;
|
||||||
followingPubkeys: Accessor<string[]>;
|
followingPubkeys: Accessor<string[]>;
|
||||||
query: CreateQueryResult<NostrEvent | undefined>;
|
query: CreateQueryResult<NostrEvent | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
setInterval(() => console.log('batchSub', count), 1000);
|
||||||
|
|
||||||
const { exec } = useBatch<TaskArg, TaskRes>(() => ({
|
const { exec } = useBatch<TaskArg, TaskRes>(() => ({
|
||||||
|
interval: 2000,
|
||||||
|
batchSize: 100,
|
||||||
executor: (tasks) => {
|
executor: (tasks) => {
|
||||||
const profileTasks = new Map<string, Task<TaskArg, TaskRes>[]>();
|
const profileTasks = new Map<string, Task<TaskArg, TaskRes>[]>();
|
||||||
const textNoteTasks = new Map<string, Task<TaskArg, TaskRes>[]>();
|
const textNoteTasks = new Map<string, Task<TaskArg, TaskRes>[]>();
|
||||||
@@ -194,46 +202,49 @@ const { exec } = useBatch<TaskArg, TaskRes>(() => ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { config } = useConfig();
|
const { config } = useConfig();
|
||||||
|
const pool = usePool();
|
||||||
|
|
||||||
useSubscription(() => ({
|
const sub = pool().sub(config().relayUrls, filters);
|
||||||
relayUrls: config().relayUrls,
|
|
||||||
filters,
|
count += 1;
|
||||||
continuous: false,
|
|
||||||
onEvent: (event: NostrEvent & { id: string }) => {
|
sub.on('event', (event: NostrEvent & { id: string }) => {
|
||||||
if (event.kind === Kind.Metadata) {
|
if (event.kind === Kind.Metadata) {
|
||||||
const registeredTasks = profileTasks.get(event.pubkey) ?? [];
|
const registeredTasks = profileTasks.get(event.pubkey) ?? [];
|
||||||
|
resolveTasks(registeredTasks, event);
|
||||||
|
} else if (event.kind === Kind.Text) {
|
||||||
|
const registeredTasks = textNoteTasks.get(event.id) ?? [];
|
||||||
|
resolveTasks(registeredTasks, event);
|
||||||
|
} else if (event.kind === Kind.Reaction) {
|
||||||
|
const eventTags = eventWrapper(event).taggedEvents();
|
||||||
|
eventTags.forEach((eventTag) => {
|
||||||
|
const taggedEventId = eventTag.id;
|
||||||
|
const registeredTasks = reactionsTasks.get(taggedEventId) ?? [];
|
||||||
resolveTasks(registeredTasks, event);
|
resolveTasks(registeredTasks, event);
|
||||||
} else if (event.kind === Kind.Text) {
|
});
|
||||||
const registeredTasks = textNoteTasks.get(event.id) ?? [];
|
} else if ((event.kind as number) === 6) {
|
||||||
|
const eventTags = eventWrapper(event).taggedEvents();
|
||||||
|
eventTags.forEach((eventTag) => {
|
||||||
|
const taggedEventId = eventTag.id;
|
||||||
|
const registeredTasks = repostsTasks.get(taggedEventId) ?? [];
|
||||||
resolveTasks(registeredTasks, event);
|
resolveTasks(registeredTasks, event);
|
||||||
} else if (event.kind === Kind.Reaction) {
|
});
|
||||||
const eventTags = eventWrapper(event).taggedEvents();
|
} else if (event.kind === Kind.Contacts) {
|
||||||
eventTags.forEach((eventTag) => {
|
const registeredTasks = followingsTasks.get(event.pubkey) ?? [];
|
||||||
const taggedEventId = eventTag.id;
|
resolveTasks(registeredTasks, event);
|
||||||
const registeredTasks = reactionsTasks.get(taggedEventId) ?? [];
|
}
|
||||||
resolveTasks(registeredTasks, event);
|
});
|
||||||
});
|
|
||||||
} else if ((event.kind as number) === 6) {
|
sub.on('eose', () => {
|
||||||
const eventTags = eventWrapper(event).taggedEvents();
|
finalizeTasks();
|
||||||
eventTags.forEach((eventTag) => {
|
sub.unsub();
|
||||||
const taggedEventId = eventTag.id;
|
count -= 1;
|
||||||
const registeredTasks = repostsTasks.get(taggedEventId) ?? [];
|
});
|
||||||
resolveTasks(registeredTasks, event);
|
|
||||||
});
|
|
||||||
} else if (event.kind === Kind.Contacts) {
|
|
||||||
const registeredTasks = followingsTasks.get(event.pubkey) ?? [];
|
|
||||||
resolveTasks(registeredTasks, event);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onEOSE: () => {
|
|
||||||
finalizeTasks();
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const pickLatestEvent = (events: NostrEvent[]): NostrEvent | undefined => {
|
const pickLatestEvent = (events: NostrEvent[]): NostrEvent | null => {
|
||||||
if (events.length === 0) return undefined;
|
if (events.length === 0) return null;
|
||||||
return events.reduce((a, b) => (a.created_at > b.created_at ? a : b));
|
return events.reduce((a, b) => (a.created_at > b.created_at ? a : b));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -245,8 +256,9 @@ export const useProfile = (propsProvider: () => UseProfileProps | null): UseProf
|
|||||||
() => ['useProfile', props()] as const,
|
() => ['useProfile', props()] as const,
|
||||||
({ queryKey, signal }) => {
|
({ queryKey, signal }) => {
|
||||||
const [, currentProps] = queryKey;
|
const [, currentProps] = queryKey;
|
||||||
if (currentProps == null) return undefined;
|
if (currentProps == null) return Promise.resolve(null);
|
||||||
const { pubkey } = currentProps;
|
const { pubkey } = currentProps;
|
||||||
|
if (pubkey.startsWith('npub1')) return Promise.resolve(null);
|
||||||
const promise = exec({ type: 'Profile', pubkey }, signal).then((batchedEvents) => {
|
const promise = exec({ type: 'Profile', pubkey }, signal).then((batchedEvents) => {
|
||||||
const latestEvent = () => {
|
const latestEvent = () => {
|
||||||
const latest = pickLatestEvent(batchedEvents().events);
|
const latest = pickLatestEvent(batchedEvents().events);
|
||||||
@@ -257,7 +269,7 @@ export const useProfile = (propsProvider: () => UseProfileProps | null): UseProf
|
|||||||
try {
|
try {
|
||||||
queryClient.setQueryData(queryKey, latestEvent());
|
queryClient.setQueryData(queryKey, latestEvent());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error('updating profile error', err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return latestEvent();
|
return latestEvent();
|
||||||
@@ -273,16 +285,16 @@ export const useProfile = (propsProvider: () => UseProfileProps | null): UseProf
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const profile = createMemo((): Profile | undefined => {
|
const profile = createMemo((): Profile | null => {
|
||||||
if (query.data == null) return undefined;
|
if (query.data == null) return null;
|
||||||
const { content } = query.data;
|
const { content } = query.data;
|
||||||
if (content == null || content.length === 0) return undefined;
|
if (content == null || content.length === 0) return null;
|
||||||
// TODO 大きすぎたりしないかどうか、JSONかどうかのチェック
|
// TODO 大きすぎたりしないかどうか、JSONかどうかのチェック
|
||||||
try {
|
try {
|
||||||
return JSON.parse(content) as Profile;
|
return JSON.parse(content) as Profile;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('failed to parse profile (kind 0): ', err, content);
|
console.error('failed to parse profile (kind 0): ', err, content);
|
||||||
return undefined;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -297,7 +309,7 @@ export const useTextNote = (propsProvider: () => UseTextNoteProps | null): UseTe
|
|||||||
() => ['useTextNote', props()] as const,
|
() => ['useTextNote', props()] as const,
|
||||||
({ queryKey, signal }) => {
|
({ queryKey, signal }) => {
|
||||||
const [, currentProps] = queryKey;
|
const [, currentProps] = queryKey;
|
||||||
if (currentProps == null) return undefined;
|
if (currentProps == null) return null;
|
||||||
const { eventId } = currentProps;
|
const { eventId } = currentProps;
|
||||||
const promise = exec({ type: 'TextNote', eventId }, signal).then((batchedEvents) => {
|
const promise = exec({ type: 'TextNote', eventId }, signal).then((batchedEvents) => {
|
||||||
const event = batchedEvents().events[0];
|
const event = batchedEvents().events[0];
|
||||||
@@ -417,7 +429,7 @@ export const useFollowings = (propsProvider: () => UseFollowingsProps | null): U
|
|||||||
genQueryKey,
|
genQueryKey,
|
||||||
({ queryKey, signal }) => {
|
({ queryKey, signal }) => {
|
||||||
const [, currentProps] = queryKey;
|
const [, currentProps] = queryKey;
|
||||||
if (currentProps == null) return undefined;
|
if (currentProps == null) return Promise.resolve(null);
|
||||||
const { pubkey } = currentProps;
|
const { pubkey } = currentProps;
|
||||||
const promise = exec({ type: 'Followings', pubkey }, signal).then((batchedEvents) => {
|
const promise = exec({ type: 'Followings', pubkey }, signal).then((batchedEvents) => {
|
||||||
const latestEvent = () => {
|
const latestEvent = () => {
|
||||||
@@ -429,7 +441,7 @@ export const useFollowings = (propsProvider: () => UseFollowingsProps | null): U
|
|||||||
try {
|
try {
|
||||||
queryClient.setQueryData(queryKey, latestEvent());
|
queryClient.setQueryData(queryKey, latestEvent());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error('updating followings error', err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return latestEvent();
|
return latestEvent();
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { type Accessor, type Setter } from 'solid-js';
|
import { type Accessor, type Setter } from 'solid-js';
|
||||||
import {
|
import {
|
||||||
createStorageWithSerializer,
|
createStorageWithSerializer,
|
||||||
createSignalWithStorage,
|
createStoreWithStorage,
|
||||||
} from '@/hooks/createSignalWithStorage';
|
} from '@/hooks/createSignalWithStorage';
|
||||||
|
|
||||||
export type Config = {
|
export type Config = {
|
||||||
relayUrls: string[];
|
relayUrls: string[];
|
||||||
dateFormat: 'relative' | 'absolute-long' | 'absolute-short';
|
dateFormat: 'relative' | 'absolute-long' | 'absolute-short';
|
||||||
keepOpenPostForm: boolean;
|
keepOpenPostForm: boolean;
|
||||||
|
showImage: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UseConfig = {
|
type UseConfig = {
|
||||||
@@ -38,6 +39,7 @@ const InitialConfig = (): Config => {
|
|||||||
relayUrls,
|
relayUrls,
|
||||||
dateFormat: 'relative',
|
dateFormat: 'relative',
|
||||||
keepOpenPostForm: false,
|
keepOpenPostForm: false,
|
||||||
|
showImage: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -50,25 +52,19 @@ const deserializer = (json: string): Config =>
|
|||||||
} as Config);
|
} as Config);
|
||||||
|
|
||||||
const storage = createStorageWithSerializer(() => window.localStorage, serializer, deserializer);
|
const storage = createStorageWithSerializer(() => window.localStorage, serializer, deserializer);
|
||||||
const [config, setConfig] = createSignalWithStorage('RabbitConfig', InitialConfig(), storage);
|
const [config, setConfig] = createStoreWithStorage('RabbitConfig', InitialConfig(), storage);
|
||||||
|
|
||||||
const useConfig = (): UseConfig => {
|
const useConfig = (): UseConfig => {
|
||||||
const addRelay = (relayUrl: string) => {
|
const addRelay = (relayUrl: string) => {
|
||||||
setConfig((current) => ({
|
setConfig('relayUrls', (current) => [...current, relayUrl]);
|
||||||
...current,
|
|
||||||
relayUrls: [...current.relayUrls, relayUrl],
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeRelay = (relayUrl: string) => {
|
const removeRelay = (relayUrl: string) => {
|
||||||
setConfig((current) => ({
|
setConfig('relayUrls', (current) => current.filter((e) => e !== relayUrl));
|
||||||
...current,
|
|
||||||
relayUrls: current.relayUrls.filter((e) => e !== relayUrl),
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
config,
|
config: () => config,
|
||||||
setConfig,
|
setConfig,
|
||||||
addRelay,
|
addRelay,
|
||||||
removeRelay,
|
removeRelay,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const sortEvents = (events: NostrEvent[]) =>
|
|||||||
|
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
setInterval(() => console.log(count), 1000);
|
setInterval(() => console.log('sub', count), 1000);
|
||||||
|
|
||||||
const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
||||||
const pool = usePool();
|
const pool = usePool();
|
||||||
@@ -34,6 +34,7 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
|||||||
const { relayUrls, filters, options, onEvent, onEOSE, continuous = true } = props;
|
const { relayUrls, filters, options, onEvent, onEOSE, continuous = true } = props;
|
||||||
|
|
||||||
const sub = pool().sub(relayUrls, filters, options);
|
const sub = pool().sub(relayUrls, filters, options);
|
||||||
|
let subscribing = true;
|
||||||
count += 1;
|
count += 1;
|
||||||
|
|
||||||
let pushed = false;
|
let pushed = false;
|
||||||
@@ -75,7 +76,10 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
|||||||
|
|
||||||
if (!continuous) {
|
if (!continuous) {
|
||||||
sub.unsub();
|
sub.unsub();
|
||||||
count -= 1;
|
if (subscribing) {
|
||||||
|
subscribing = false;
|
||||||
|
count -= 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -93,7 +97,10 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
|||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
sub.unsub();
|
sub.unsub();
|
||||||
// count -= 1;
|
if (subscribing) {
|
||||||
|
subscribing = false;
|
||||||
|
count -= 1;
|
||||||
|
}
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
import { createSignal, createEffect, onMount, onCleanup, Show, type Component } from 'solid-js';
|
import {
|
||||||
|
createSignal,
|
||||||
|
createEffect,
|
||||||
|
onMount,
|
||||||
|
onCleanup,
|
||||||
|
Show,
|
||||||
|
Switch,
|
||||||
|
Match,
|
||||||
|
type Component,
|
||||||
|
} from 'solid-js';
|
||||||
import { useNavigate } from '@solidjs/router';
|
import { useNavigate } from '@solidjs/router';
|
||||||
import uniq from 'lodash/uniq';
|
import uniq from 'lodash/uniq';
|
||||||
|
|
||||||
@@ -16,12 +25,14 @@ import usePubkey from '@/nostr/usePubkey';
|
|||||||
import { useMountShortcutKeys } from '@/hooks/useShortcutKeys';
|
import { useMountShortcutKeys } from '@/hooks/useShortcutKeys';
|
||||||
import usePersistStatus from '@/hooks/usePersistStatus';
|
import usePersistStatus from '@/hooks/usePersistStatus';
|
||||||
import ensureNonNull from '@/utils/ensureNonNull';
|
import ensureNonNull from '@/utils/ensureNonNull';
|
||||||
import ProfileDisplay from '@/components/Profile';
|
import ProfileDisplay from '@/components/ProfileDisplay';
|
||||||
|
import useModalState from '@/hooks/useModalState';
|
||||||
|
|
||||||
const Home: Component = () => {
|
const Home: Component = () => {
|
||||||
useMountShortcutKeys();
|
useMountShortcutKeys();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { persistStatus } = usePersistStatus();
|
const { persistStatus } = usePersistStatus();
|
||||||
|
const { modalState, closeModal } = useModalState();
|
||||||
|
|
||||||
const pool = usePool();
|
const pool = usePool();
|
||||||
const { config } = useConfig();
|
const { config } = useConfig();
|
||||||
@@ -156,11 +167,17 @@ const Home: Component = () => {
|
|||||||
<Notification events={myReactions()} />
|
<Notification events={myReactions()} />
|
||||||
</Column>
|
</Column>
|
||||||
</div>
|
</div>
|
||||||
{/*
|
<Show when={modalState()} keyed>
|
||||||
<Show when={pubkey()} keyed>
|
{(state) => (
|
||||||
{(pubkeyNonNull: string) => <ProfileDisplay pubkey={pubkeyNonNull} />}
|
<Switch>
|
||||||
|
<Match when={state.type === 'Profile' && state.pubkey} keyed>
|
||||||
|
{(pubkeyNonNull: string) => (
|
||||||
|
<ProfileDisplay pubkey={pubkeyNonNull} onClose={closeModal} />
|
||||||
|
)}
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
*/}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
12
src/utils/npubEncodeFallback.ts
Normal file
12
src/utils/npubEncodeFallback.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { npubEncode } from 'nostr-tools/nip19';
|
||||||
|
|
||||||
|
const npubEncodeFallback = (pubkey: string): string => {
|
||||||
|
try {
|
||||||
|
return npubEncode(pubkey);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('failed to encode pubkey into npub', pubkey);
|
||||||
|
return pubkey;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default npubEncodeFallback;
|
||||||
@@ -13,6 +13,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
target: 'esnext',
|
target: 'esnext',
|
||||||
|
sourcemap: 'inline',
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
Reference in New Issue
Block a user