mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-17 05:54:19 +01:00
update
This commit is contained in:
@@ -25,7 +25,7 @@ const Column: Component<ColumnProps> = (props) => {
|
||||
|
||||
useHandleCommand(() => ({
|
||||
commandType: 'moveToLastColumn',
|
||||
handler: (command) => {
|
||||
handler: () => {
|
||||
if (props.lastColumn) {
|
||||
columnDivRef?.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ const ToggleButton = (props: {
|
||||
'justify-end': props.value,
|
||||
}}
|
||||
area-label={props.value}
|
||||
onClick={props.onClick}
|
||||
onClick={(event) => props.onClick(event)}
|
||||
>
|
||||
<span class="m-[-2px] inline-block h-5 w-5 rounded-full border border-primary bg-white shadow" />
|
||||
</button>
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
// NIP-18 (DEPRECATED)
|
||||
import { Show, Switch, Match, type Component, createMemo } from 'solid-js';
|
||||
import { type Component, createMemo } from 'solid-js';
|
||||
import { Event as NostrEvent } from 'nostr-tools';
|
||||
import ArrowPathRoundedSquare from 'heroicons/24/outline/arrow-path-rounded-square.svg';
|
||||
|
||||
import useConfig from '@/nostr/useConfig';
|
||||
import useEvent from '@/nostr/useEvent';
|
||||
import useProfile from '@/nostr/useProfile';
|
||||
|
||||
import ColumnItem from '@/components/ColumnItem';
|
||||
import UserDisplayName from '@/components/UserDisplayName';
|
||||
import TextNote from '@/components/TextNote';
|
||||
import eventWrapper from '@/core/event';
|
||||
import useFormatDate from '@/hooks/useFormatDate';
|
||||
import TextNoteDisplayById from './textNote/TextNoteDisplayById';
|
||||
|
||||
@@ -8,7 +8,6 @@ import ArrowPathRoundedSquare from 'heroicons/24/outline/arrow-path-rounded-squa
|
||||
import ChatBubbleLeft from 'heroicons/24/outline/chat-bubble-left.svg';
|
||||
import EllipsisHorizontal from 'heroicons/24/outline/ellipsis-horizontal.svg';
|
||||
|
||||
import ColumnItem from '@/components/ColumnItem';
|
||||
import GeneralUserMentionDisplay from '@/components/textNote/GeneralUserMentionDisplay';
|
||||
import ContentWarningDisplay from '@/components/textNote/ContentWarningDisplay';
|
||||
import TextNoteContentDisplay from '@/components/textNote/TextNoteContentDisplay';
|
||||
@@ -107,7 +106,7 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
|
||||
const createdAt = () => formatDate(event().createdAtAsDate());
|
||||
|
||||
const handleRepost: JSX.EventHandler<HTMLButtonElement, MouseEvent> = (ev) => {
|
||||
const handleRepost: JSX.EventHandler<HTMLButtonElement, MouseEvent> = () => {
|
||||
if (isRepostedByMe()) {
|
||||
// TODO remove reaction
|
||||
return;
|
||||
@@ -123,7 +122,7 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleReaction: JSX.EventHandler<HTMLButtonElement, MouseEvent> = (ev) => {
|
||||
const handleReaction: JSX.EventHandler<HTMLButtonElement, MouseEvent> = () => {
|
||||
if (isReactedByMe()) {
|
||||
// TODO remove reaction
|
||||
return;
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
// import { z } from 'zod';
|
||||
import { Event as NostrEvent } from 'nostr-tools';
|
||||
import { type Event as NostrEvent, type Filter } from 'nostr-tools';
|
||||
import ColumnComponent from '@/components/Column';
|
||||
|
||||
export type NotificationTypes =
|
||||
export type NotificationType =
|
||||
// The event which includes ["p", ...] tags.
|
||||
| 'PubkeyMention'
|
||||
// The event which includes ["e", ...] tags.
|
||||
| 'EventMention'
|
||||
// The event which has the deprecated kind 6.
|
||||
| 'DeprecatedRepost'
|
||||
// The event which has
|
||||
| 'Followed'
|
||||
| 'PlusReaction'
|
||||
| 'MinusReaction'
|
||||
| 'EmojiReaction';
|
||||
// The event which has the deprecated kind 7.
|
||||
| 'Reaction';
|
||||
|
||||
export type GenericFilterOptions = {
|
||||
matching: string;
|
||||
@@ -21,24 +19,73 @@ export type GenericFilterOptions = {
|
||||
};
|
||||
|
||||
export type NotificationFilterOptions = {
|
||||
allowedTypes: NotificationTypes[];
|
||||
allowedTypes: NotificationType[];
|
||||
};
|
||||
|
||||
type BulidOptions = {
|
||||
supportedNips: string[];
|
||||
};
|
||||
|
||||
const notificationFilter =
|
||||
(filterOption: NotificationFilterOptions) =>
|
||||
(event: NostrEvent): boolean => {};
|
||||
// const notificationFilter =
|
||||
// (filterOption: NotificationFilterOptions) =>
|
||||
// (event: NostrEvent): boolean => {};
|
||||
//
|
||||
// const genericFilter =
|
||||
// (filterOption: GenericFilterOptions) =>
|
||||
// (event: NostrEvent): boolean => {
|
||||
// event.content;
|
||||
// };
|
||||
//
|
||||
// /**
|
||||
// * build a filter for the subscription ("REQ")
|
||||
// */
|
||||
// export const buildFilter = (options: BuildOptions) => {};
|
||||
|
||||
const genericFilter =
|
||||
(filterOption: GenericFilterOptions) =>
|
||||
(event: NostrEvent): boolean => {
|
||||
event.content;
|
||||
};
|
||||
export type BaseColumn = {
|
||||
columnWidth: (typeof ColumnComponent)['width'];
|
||||
};
|
||||
|
||||
/**
|
||||
* build a filter for the subscription ("REQ")
|
||||
*/
|
||||
export const buildFilter = (options: BuildOptions) => {};
|
||||
/** A column which shows posts by following users */
|
||||
export type FollowingColumn = {
|
||||
columnType: 'Following';
|
||||
pubkey: string;
|
||||
};
|
||||
|
||||
/** A column which shows replies, reactions, reposts to the specific user */
|
||||
export type NotificationColumn = {
|
||||
columnType: 'Notification';
|
||||
notificationTypes: NotificationType[];
|
||||
pubkey: string;
|
||||
};
|
||||
|
||||
/** A column which shows posts from the specific user */
|
||||
export type PostsColumn = {
|
||||
columnType: 'Posts';
|
||||
pubkey: string;
|
||||
};
|
||||
|
||||
/** A column which shows reactions published by the specific user */
|
||||
export type ReactionsColumn = {
|
||||
columnType: 'Reactions';
|
||||
pubkey: string;
|
||||
};
|
||||
|
||||
/** A column which shows text notes and reposts posted to the specific relays */
|
||||
export type GlobalColumn = {
|
||||
columnType: 'Global';
|
||||
relayUrls: string[];
|
||||
};
|
||||
|
||||
/** A column which shows text notes and reposts posted to the specific relays */
|
||||
export type CustomFilterColumn = {
|
||||
columnType: 'CustomFilter';
|
||||
filters: Filter[];
|
||||
};
|
||||
|
||||
export type ColumnConfig =
|
||||
| FollowingColumn
|
||||
| NotificationColumn
|
||||
| GlobalColumn
|
||||
| PostsColumn
|
||||
| ReactionsColumn
|
||||
| CustomFilterColumn;
|
||||
|
||||
@@ -30,7 +30,6 @@ export type Bech32Entity = {
|
||||
| { type: 'nprofile'; data: ProfilePointer }
|
||||
| { type: 'nevent'; data: EventPointer };
|
||||
};
|
||||
// | { type: 'naddr'; data: AddressPointer };
|
||||
|
||||
export type HashTag = {
|
||||
type: 'HashTag';
|
||||
@@ -53,13 +52,12 @@ export type ParsedTextNoteNode =
|
||||
|
||||
export type ParsedTextNote = ParsedTextNoteNode[];
|
||||
|
||||
export const parseTextNote = (event: NostrEvent): ParsedTextNote => {
|
||||
const parseTextNote = (event: NostrEvent): ParsedTextNote => {
|
||||
const matches = [
|
||||
...event.content.matchAll(/(?:#\[(?<idx>\d+)\])/g),
|
||||
...event.content.matchAll(/#(?<hashtag>[^[-^`:-@!-/{-~\d\s][^[-^`:-@!-/{-~\s]+)/g),
|
||||
...event.content.matchAll(
|
||||
/(?<nip19>(npub|note|nprofile|nevent|nrelay|naddr)1[ac-hj-np-z02-9]+)/gi,
|
||||
),
|
||||
// nrelay and naddr is not supported by nostr-tools
|
||||
...event.content.matchAll(/(?<nip19>(npub|note|nprofile|nevent)1[ac-hj-np-z02-9]+)/gi),
|
||||
...event.content.matchAll(
|
||||
/(?<url>(https?|wss?):\/\/[-a-zA-Z0-9.]+(?:\/[-\w.@%:]+|\/)*(?:\?[-\w=.@%:&]*)?(?:#[-\w=.%:&]*)?)/g,
|
||||
),
|
||||
|
||||
@@ -20,7 +20,8 @@ type TaskArg =
|
||||
| { type: 'Profile'; pubkey: string }
|
||||
| { type: 'TextNote'; eventId: string }
|
||||
| { type: 'Reactions'; mentionedEventId: string }
|
||||
| { type: 'DeprecatedReposts'; mentionedEventId: string };
|
||||
| { type: 'DeprecatedReposts'; mentionedEventId: string }
|
||||
| { type: 'Followings'; pubkey: string };
|
||||
|
||||
type BatchedEvents = { completed: boolean; events: NostrEvent[] };
|
||||
|
||||
@@ -92,12 +93,30 @@ export type UseDeprecatedReposts = {
|
||||
query: CreateQueryResult<NostrEvent[]>;
|
||||
};
|
||||
|
||||
// Followings
|
||||
type UseFollowingsProps = {
|
||||
pubkey: string;
|
||||
};
|
||||
|
||||
type Following = {
|
||||
pubkey: string;
|
||||
mainRelayUrl?: string;
|
||||
petname?: string;
|
||||
};
|
||||
|
||||
export type UseFollowings = {
|
||||
followings: Accessor<Following[]>;
|
||||
followingPubkeys: Accessor<string[]>;
|
||||
query: CreateQueryResult<NostrEvent | undefined>;
|
||||
};
|
||||
|
||||
const { exec } = useBatch<TaskArg, TaskRes>(() => ({
|
||||
executor: (tasks) => {
|
||||
const profileTasks = new Map<string, Task<TaskArg, TaskRes>[]>();
|
||||
const textNoteTasks = new Map<string, Task<TaskArg, TaskRes>[]>();
|
||||
const reactionsTasks = new Map<string, Task<TaskArg, TaskRes>[]>();
|
||||
const repostsTasks = new Map<string, Task<TaskArg, TaskRes>[]>();
|
||||
const followingsTasks = new Map<string, Task<TaskArg, TaskRes>[]>();
|
||||
|
||||
tasks.forEach((task) => {
|
||||
if (task.args.type === 'Profile') {
|
||||
@@ -112,6 +131,9 @@ const { exec } = useBatch<TaskArg, TaskRes>(() => ({
|
||||
} else if (task.args.type === 'DeprecatedReposts') {
|
||||
const current = repostsTasks.get(task.args.mentionedEventId) ?? [];
|
||||
repostsTasks.set(task.args.mentionedEventId, [...current, task]);
|
||||
} else if (task.args.type === 'Followings') {
|
||||
const current = followingsTasks.get(task.args.pubkey) ?? [];
|
||||
followingsTasks.set(task.args.pubkey, [...current, task]);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -119,6 +141,7 @@ const { exec } = useBatch<TaskArg, TaskRes>(() => ({
|
||||
const textNoteIds = [...textNoteTasks.keys()];
|
||||
const reactionsIds = [...reactionsTasks.keys()];
|
||||
const repostsIds = [...repostsTasks.keys()];
|
||||
const followingsIds = [...followingsTasks.keys()];
|
||||
|
||||
const filters: Filter[] = [];
|
||||
|
||||
@@ -134,6 +157,9 @@ const { exec } = useBatch<TaskArg, TaskRes>(() => ({
|
||||
if (repostsIds.length > 0) {
|
||||
filters.push({ kinds: [6], '#e': repostsIds });
|
||||
}
|
||||
if (followingsIds.length > 0) {
|
||||
filters.push({ kinds: [Kind.Contacts], authors: followingsIds });
|
||||
}
|
||||
|
||||
if (filters.length === 0) return;
|
||||
|
||||
@@ -194,6 +220,9 @@ const { exec } = useBatch<TaskArg, TaskRes>(() => ({
|
||||
const registeredTasks = repostsTasks.get(taggedEventId) ?? [];
|
||||
resolveTasks(registeredTasks, event);
|
||||
});
|
||||
} else if (event.kind === Kind.Contacts) {
|
||||
const registeredTasks = followingsTasks.get(event.pubkey) ?? [];
|
||||
resolveTasks(registeredTasks, event);
|
||||
}
|
||||
},
|
||||
onEOSE: () => {
|
||||
@@ -203,6 +232,11 @@ const { exec } = useBatch<TaskArg, TaskRes>(() => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const pickLatestEvent = (events: NostrEvent[]): NostrEvent | undefined => {
|
||||
if (events.length === 0) return undefined;
|
||||
return events.reduce((a, b) => (a.created_at > b.created_at ? a : b));
|
||||
};
|
||||
|
||||
export const useProfile = (propsProvider: () => UseProfileProps | null): UseProfile => {
|
||||
const props = createMemo(propsProvider);
|
||||
const queryClient = useQueryClient();
|
||||
@@ -215,9 +249,8 @@ export const useProfile = (propsProvider: () => UseProfileProps | null): UseProf
|
||||
const { pubkey } = currentProps;
|
||||
const promise = exec({ type: 'Profile', pubkey }, signal).then((batchedEvents) => {
|
||||
const latestEvent = () => {
|
||||
const { events } = batchedEvents();
|
||||
if (events.length === 0) throw new Error(`profile not found: ${pubkey}`);
|
||||
const latest = events.reduce((a, b) => (a.created_at > b.created_at ? a : b));
|
||||
const latest = pickLatestEvent(batchedEvents().events);
|
||||
if (latest == null) throw new Error(`profile not found: ${pubkey}`);
|
||||
return latest;
|
||||
};
|
||||
observable(batchedEvents).subscribe(() => {
|
||||
@@ -233,9 +266,10 @@ export const useProfile = (propsProvider: () => UseProfileProps | null): UseProf
|
||||
return timeout(15000, `useProfile: ${pubkey}`)(promise);
|
||||
},
|
||||
{
|
||||
// profile is updated occasionally
|
||||
staleTime: 5 * 60 * 1000, // 5min
|
||||
cacheTime: 24 * 60 * 60 * 1000, // 1day
|
||||
// Profiles are updated occasionally, so a short staleTime is used here.
|
||||
// cacheTime is long so that the user see profiles instantly.
|
||||
staleTime: 5 * 60 * 1000, // 5 min
|
||||
cacheTime: 24 * 60 * 60 * 1000, // 1 day
|
||||
},
|
||||
);
|
||||
|
||||
@@ -273,9 +307,9 @@ export const useTextNote = (propsProvider: () => UseTextNoteProps | null): UseTe
|
||||
return timeout(15000, `useTextNote: ${eventId}`)(promise);
|
||||
},
|
||||
{
|
||||
// text note cannot be updated.
|
||||
staleTime: 24 * 60 * 60 * 1000, // 1 day
|
||||
cacheTime: 24 * 60 * 60 * 1000, // 1 day
|
||||
// Text notes never change, so they can be stored for a long time.
|
||||
staleTime: 4 * 60 * 60 * 1000, // 4 hour
|
||||
cacheTime: 4 * 60 * 60 * 1000, // 4 hour
|
||||
},
|
||||
);
|
||||
|
||||
@@ -299,11 +333,9 @@ export const useReactions = (propsProvider: () => UseReactionsProps | null): Use
|
||||
const promise = exec({ type: 'Reactions', mentionedEventId }, signal).then(
|
||||
(batchedEvents) => {
|
||||
const events = () => batchedEvents().events;
|
||||
setTimeout(() => {
|
||||
observable(batchedEvents).subscribe(() => {
|
||||
queryClient.setQueryData(queryKey, events());
|
||||
});
|
||||
});
|
||||
return events();
|
||||
},
|
||||
);
|
||||
@@ -311,7 +343,7 @@ export const useReactions = (propsProvider: () => UseReactionsProps | null): Use
|
||||
},
|
||||
{
|
||||
staleTime: 1 * 60 * 1000, // 1 min
|
||||
cacheTime: 5 * 60 * 1000, // 5 min
|
||||
cacheTime: 4 * 60 * 60 * 1000, // 4 hour
|
||||
},
|
||||
);
|
||||
|
||||
@@ -351,11 +383,9 @@ export const useDeprecatedReposts = (
|
||||
const promise = exec({ type: 'DeprecatedReposts', mentionedEventId }, signal).then(
|
||||
(batchedEvents) => {
|
||||
const events = () => batchedEvents().events;
|
||||
setTimeout(() => {
|
||||
observable(batchedEvents).subscribe(() => {
|
||||
queryClient.setQueryData(queryKey, events());
|
||||
});
|
||||
});
|
||||
return events();
|
||||
},
|
||||
);
|
||||
@@ -363,7 +393,7 @@ export const useDeprecatedReposts = (
|
||||
},
|
||||
{
|
||||
staleTime: 1 * 60 * 1000, // 1 min
|
||||
cacheTime: 5 * 60 * 1000, // 5 min
|
||||
cacheTime: 4 * 60 * 60 * 1000, // 4 hour
|
||||
},
|
||||
);
|
||||
|
||||
@@ -377,3 +407,70 @@ export const useDeprecatedReposts = (
|
||||
|
||||
return { reposts, isRepostedBy, invalidateDeprecatedReposts, query };
|
||||
};
|
||||
|
||||
export const useFollowings = (propsProvider: () => UseFollowingsProps | null): UseFollowings => {
|
||||
const queryClient = useQueryClient();
|
||||
const props = createMemo(propsProvider);
|
||||
const genQueryKey = () => ['useFollowings', props()] as const;
|
||||
|
||||
const query = createQuery(
|
||||
genQueryKey,
|
||||
({ queryKey, signal }) => {
|
||||
const [, currentProps] = queryKey;
|
||||
if (currentProps == null) return undefined;
|
||||
const { pubkey } = currentProps;
|
||||
const promise = exec({ type: 'Followings', pubkey }, signal).then((batchedEvents) => {
|
||||
const latestEvent = () => {
|
||||
const latest = pickLatestEvent(batchedEvents().events);
|
||||
if (latest == null) throw new Error(`followings not found: ${pubkey}`);
|
||||
return latest;
|
||||
};
|
||||
observable(batchedEvents).subscribe(() => {
|
||||
try {
|
||||
queryClient.setQueryData(queryKey, latestEvent());
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
return latestEvent();
|
||||
});
|
||||
return timeout(15000, `useFollowings: ${pubkey}`)(promise);
|
||||
},
|
||||
{
|
||||
staleTime: 5 * 60 * 1000, // 5 min
|
||||
cacheTime: 4 * 60 * 60 * 1000, // 4 hour
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
);
|
||||
|
||||
const followings = () => {
|
||||
if (query.data == null) return [];
|
||||
|
||||
const event = query.data;
|
||||
|
||||
const result: Following[] = [];
|
||||
event.tags.forEach((tag) => {
|
||||
// TODO zodにする
|
||||
const [tagName, followingPubkey, mainRelayUrl, petname] = tag;
|
||||
if (!tag.every((e) => typeof e === 'string')) return;
|
||||
if (tagName !== 'p') return;
|
||||
|
||||
const following: Following = { pubkey: followingPubkey, petname };
|
||||
if (mainRelayUrl != null && mainRelayUrl.length > 0) {
|
||||
following.mainRelayUrl = mainRelayUrl;
|
||||
}
|
||||
|
||||
result.push(following);
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const followingPubkeys = (): string[] => {
|
||||
return followings().map((follow) => follow.pubkey);
|
||||
};
|
||||
|
||||
return { followings, followingPubkeys, query };
|
||||
};
|
||||
|
||||
export default useFollowings;
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import { createQuery } from '@tanstack/solid-query';
|
||||
import { type UseSubscriptionProps } from '@/nostr/useSubscription';
|
||||
import type { Event as NostrEvent, Filter, SimplePool, SubscriptionOptions } from 'nostr-tools';
|
||||
import usePool from './usePool';
|
||||
|
||||
type GetEventsArgs = {
|
||||
pool: SimplePool;
|
||||
relayUrls: string[];
|
||||
filters: Filter[];
|
||||
// TODO 継続的に取得する場合、Promiseでは無理なので、無理やりキャッシュにストアする仕組みを使う
|
||||
continuous?: boolean;
|
||||
options?: SubscriptionOptions;
|
||||
signal?: AbortSignal;
|
||||
};
|
||||
|
||||
const getEvents = async ({
|
||||
pool,
|
||||
relayUrls,
|
||||
filters,
|
||||
options,
|
||||
signal,
|
||||
}: GetEventsArgs): Promise<NostrEvent[]> => {
|
||||
const result: NostrEvent[] = [];
|
||||
|
||||
const sub = pool.sub(relayUrls, filters, options);
|
||||
sub.on('event', (event: NostrEvent) => result.push(event));
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
sub.on('eose', () => {
|
||||
sub.unsub();
|
||||
resolve(result);
|
||||
});
|
||||
|
||||
if (signal != null) {
|
||||
signal.addEventListener('abort', () => {
|
||||
sub.unsub();
|
||||
reject(signal.reason);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This aims to fetch stored data, and doesn't support fetching streaming data continuously.
|
||||
*
|
||||
* This is useful when you want to fetch some data which change occasionally:
|
||||
* profile or following list, reactions, and something like that.
|
||||
*/
|
||||
const useCachedEvents = (propsProvider: () => UseSubscriptionProps | null) => {
|
||||
const pool = usePool();
|
||||
|
||||
return createQuery(
|
||||
() => {
|
||||
const currentProps = propsProvider();
|
||||
return ['useCachedEvents', currentProps] as const;
|
||||
},
|
||||
({ queryKey, signal }) => {
|
||||
const [, currentProps] = queryKey;
|
||||
if (currentProps == null) return [];
|
||||
return getEvents({ pool: pool(), signal, ...currentProps });
|
||||
},
|
||||
{
|
||||
staleTime: 5 * 60 * 1000,
|
||||
cacheTime: 15 * 60 * 1000,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export default useCachedEvents;
|
||||
@@ -6,7 +6,7 @@ import usePool from '@/nostr/usePool';
|
||||
const currentDate = (): number => Math.floor(Date.now() / 1000);
|
||||
|
||||
// NIP-20: Command Result
|
||||
const waitCommandResult = (pub: Pub): Promise<void> => {
|
||||
const waitCommandResult = (pub: Pub, relayUrl: string): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
pub.on('ok', () => {
|
||||
console.log(`${relayUrl} has accepted our event`);
|
||||
@@ -34,7 +34,7 @@ const useCommands = () => {
|
||||
return relayUrls.map(async (relayUrl) => {
|
||||
const relay = await pool().ensureRelay(relayUrl);
|
||||
const pub = relay.publish(signedEvent);
|
||||
return waitCommandResult(pub);
|
||||
return waitCommandResult(pub, relayUrl);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -17,28 +17,40 @@ type UseConfig = {
|
||||
removeRelay: (url: string) => void;
|
||||
};
|
||||
|
||||
const InitialConfig: Config = {
|
||||
relayUrls: [
|
||||
'wss://relay-jp.nostr.wirednet.jp',
|
||||
'wss://nostr.h3z.jp',
|
||||
const InitialConfig = (): Config => {
|
||||
const relayUrls = [
|
||||
'wss://relay.damus.io',
|
||||
'wss://nos.lol',
|
||||
'wss://relay.snort.social',
|
||||
'wss://relay.current.fyi',
|
||||
];
|
||||
if (navigator.language === 'ja') {
|
||||
relayUrls.push(
|
||||
'wss://nostr.h3z.jp',
|
||||
'wss://relay.nostr.wirednet.jp',
|
||||
'wss://nostr-relay.nokotaro.com',
|
||||
'wss://relay-jp.nostr.wirednet.jp',
|
||||
'wss://nostr.holybea.com',
|
||||
],
|
||||
'wss://nostr-relay.nokotaro.com',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
relayUrls,
|
||||
dateFormat: 'relative',
|
||||
keepOpenPostForm: false,
|
||||
};
|
||||
};
|
||||
|
||||
const serializer = (config: Config): string => JSON.stringify(config);
|
||||
// TODO zod使う
|
||||
const deserializer = (json: string): Config => JSON.parse(json) as Config;
|
||||
const deserializer = (json: string): Config =>
|
||||
({
|
||||
...InitialConfig(),
|
||||
...JSON.parse(json),
|
||||
} as Config);
|
||||
|
||||
const storage = createStorageWithSerializer(() => window.localStorage, serializer, deserializer);
|
||||
const [config, setConfig] = createSignalWithStorage('RabbitConfig', InitialConfig, storage);
|
||||
const [config, setConfig] = createSignalWithStorage('RabbitConfig', InitialConfig(), storage);
|
||||
|
||||
const useConfig = (): UseConfig => {
|
||||
const addRelay = (relayUrl: string) => {
|
||||
@@ -56,7 +68,7 @@ const useConfig = (): UseConfig => {
|
||||
};
|
||||
|
||||
return {
|
||||
config: () => ({ ...InitialConfig, ...config() }),
|
||||
config,
|
||||
setConfig,
|
||||
addRelay,
|
||||
removeRelay,
|
||||
|
||||
@@ -1,65 +1,3 @@
|
||||
import { createMemo } from 'solid-js';
|
||||
import useCachedEvents from '@/nostr/useCachedEvents';
|
||||
|
||||
type UseFollowingsProps = {
|
||||
relayUrls: string[];
|
||||
pubkey: string;
|
||||
};
|
||||
|
||||
type Following = {
|
||||
pubkey: string;
|
||||
mainRelayUrl?: string;
|
||||
petname?: string;
|
||||
};
|
||||
|
||||
const useFollowings = (propsProvider: () => UseFollowingsProps | null) => {
|
||||
const props = createMemo(propsProvider);
|
||||
const query = useCachedEvents(() => {
|
||||
const currentProps = props();
|
||||
if (currentProps == null) return null;
|
||||
const { relayUrls, pubkey } = currentProps;
|
||||
return {
|
||||
relayUrls,
|
||||
filters: [
|
||||
{
|
||||
kinds: [3],
|
||||
authors: [pubkey],
|
||||
limit: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const followings = () => {
|
||||
if (query.data != null && query.data.length === 0) return [];
|
||||
|
||||
const event = query.data?.reduce((a, b) => (a.created_at > b.created_at ? a : b));
|
||||
|
||||
if (event == null) return [];
|
||||
|
||||
const result: Following[] = [];
|
||||
event.tags.forEach((tag) => {
|
||||
// TODO zodにする
|
||||
const [tagName, followingPubkey, mainRelayUrl, petname] = tag;
|
||||
if (!tag.every((e) => typeof e === 'string')) return;
|
||||
if (tagName !== 'p') return;
|
||||
|
||||
const following: Following = { pubkey: followingPubkey, petname };
|
||||
if (mainRelayUrl != null && mainRelayUrl.length > 0) {
|
||||
following.mainRelayUrl = mainRelayUrl;
|
||||
}
|
||||
|
||||
result.push(following);
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const followingPubkeys = (): string[] => {
|
||||
return followings().map((follow) => follow.pubkey);
|
||||
};
|
||||
|
||||
return { followings, followingPubkeys };
|
||||
};
|
||||
import { useFollowings } from '@/nostr/useBatchedEvents';
|
||||
|
||||
export default useFollowings;
|
||||
|
||||
@@ -25,7 +25,7 @@ const usePubkey = (): Accessor<string | undefined> => {
|
||||
window.nostr
|
||||
.getPublicKey()
|
||||
.then((key) => setPubkey(key))
|
||||
.catch((err) => console.error(`failed to obtain public key: ${err}`));
|
||||
.catch((err) => console.error('failed to obtain public key: ', err));
|
||||
}
|
||||
count += 1;
|
||||
}, 1000);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createSignal, onMount, Switch, Match, type Component } from 'solid-js';
|
||||
import { useNavigate } from '@solidjs/router';
|
||||
import usePersistStatus from '@/hooks/usePersistStatus';
|
||||
import { persistQueryClient } from '@tanstack/react-query-persist-client';
|
||||
|
||||
type SignerStatus = 'checking' | 'available' | 'unavailable';
|
||||
|
||||
|
||||
@@ -30,8 +30,7 @@ const Home: Component = () => {
|
||||
createEffect(() => {
|
||||
config().relayUrls.map(async (relayUrl) => {
|
||||
const relay = await pool().ensureRelay(relayUrl);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
relay.on('notice', (msg: any) => {
|
||||
relay.on('notice', (msg: string) => {
|
||||
console.error(`NOTICE: ${relayUrl}: ${msg}`);
|
||||
});
|
||||
});
|
||||
@@ -44,6 +43,10 @@ const Home: Component = () => {
|
||||
})),
|
||||
);
|
||||
|
||||
createEffect(() => {
|
||||
console.log(followingPubkeys());
|
||||
});
|
||||
|
||||
const { events: followingsPosts } = useSubscription(() =>
|
||||
ensureNonNull([pubkey()] as const)(([pubkeyNonNull]) => ({
|
||||
relayUrls: config().relayUrls,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"esModuleInterop": true,
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"types": ["vite/client", "mocha", "node"],
|
||||
"types": ["vite/client", "node"],
|
||||
"noEmit": true,
|
||||
"isolatedModules": true,
|
||||
"baseUrl": ".",
|
||||
|
||||
Reference in New Issue
Block a user