mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-17 14:04:21 +01:00
update
This commit is contained in:
@@ -14,7 +14,7 @@
|
|||||||
<meta property="og:image" content="" />
|
<meta property="og:image" content="" />
|
||||||
<meta property="og:locale" content="ja_JP">
|
<meta property="og:locale" content="ja_JP">
|
||||||
<!-- <link rel="shortcut icon" type="image/png" href="/src/assets/icon.png" /> -->
|
<!-- <link rel="shortcut icon" type="image/png" href="/src/assets/icon.png" /> -->
|
||||||
<title>nostRabbit</title>
|
<title>🐰</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ const getEvents = async ({
|
|||||||
/**
|
/**
|
||||||
* This aims to fetch stored data, and doesn't support fetching streaming data continuously.
|
* This aims to fetch stored data, and doesn't support fetching streaming data continuously.
|
||||||
*
|
*
|
||||||
* This will be useful when you want to fetch profile or following list, reactions, and something like that.
|
* 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) => {
|
const useCachedEvents = (propsProvider: () => UseSubscriptionProps) => {
|
||||||
const pool = usePool();
|
const pool = usePool();
|
||||||
@@ -52,7 +53,7 @@ const useCachedEvents = (propsProvider: () => UseSubscriptionProps) => {
|
|||||||
return createQuery(
|
return createQuery(
|
||||||
() => {
|
() => {
|
||||||
const { relayUrls, filters, options } = propsProvider();
|
const { relayUrls, filters, options } = propsProvider();
|
||||||
return ['useCachedEvents', relayUrls, filters, options];
|
return ['useCachedEvents', relayUrls, filters, options] as const;
|
||||||
},
|
},
|
||||||
({ queryKey, signal }) => {
|
({ queryKey, signal }) => {
|
||||||
const [, relayUrls, filters, options] = queryKey;
|
const [, relayUrls, filters, options] = queryKey;
|
||||||
|
|||||||
@@ -1,56 +1,89 @@
|
|||||||
import { getEventHash } from 'nostr-tools/event';
|
import { getEventHash } from 'nostr-tools/event';
|
||||||
import type { Event as NostrEvent } from 'nostr-tools/event';
|
import type { Event as NostrEvent } from 'nostr-tools/event';
|
||||||
|
import type { Pub } from 'nostr-tools/relay';
|
||||||
|
|
||||||
import usePool from '@/clients/usePool';
|
import usePool from '@/clients/usePool';
|
||||||
|
|
||||||
type UseCommands = {
|
// NIP-20: Command Result
|
||||||
publishTextNote: (props: {
|
const waitCommandResult = (pub: Pub): Promise<void> => {
|
||||||
relayUrls: string[];
|
return new Promise((resolve, reject) => {
|
||||||
pubkey: string;
|
pub.on('ok', () => {
|
||||||
content: string;
|
console.log(`${relayUrl} has accepted our event`);
|
||||||
}) => Promise<void>;
|
resolve();
|
||||||
|
});
|
||||||
|
pub.on('failed', (reason: string) => {
|
||||||
|
console.log(`failed to publish to ${relayUrl}: ${reason}`);
|
||||||
|
reject(reason);
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const useCommands = (): UseCommands => {
|
const useCommands = () => {
|
||||||
const pool = usePool();
|
const pool = usePool();
|
||||||
|
|
||||||
const publishTextNote = async ({
|
const publishEvent = async (relayUrls: string[], event: NostrEvent): Promise<Promise<void>[]> => {
|
||||||
relayUrls,
|
const preSignedEvent: NostrEvent = { ...event };
|
||||||
pubkey,
|
|
||||||
content,
|
|
||||||
}: {
|
|
||||||
relayUrls: string[];
|
|
||||||
pubkey: string;
|
|
||||||
content: string;
|
|
||||||
}) => {
|
|
||||||
const preSignedEvent: NostrEvent = {
|
|
||||||
kind: 1,
|
|
||||||
pubkey,
|
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
|
||||||
tags: [],
|
|
||||||
content,
|
|
||||||
};
|
|
||||||
preSignedEvent.id = getEventHash(preSignedEvent);
|
preSignedEvent.id = getEventHash(preSignedEvent);
|
||||||
const signedEvent = await window.nostr.signEvent(preSignedEvent);
|
// TODO define window.nostr
|
||||||
|
const signedEvent = (await window.nostr.signEvent(preSignedEvent)) as NostrEvent;
|
||||||
|
|
||||||
return relayUrls.map(async (relayUrl) => {
|
return relayUrls.map(async (relayUrl) => {
|
||||||
const relay = await pool().ensureRelay(relayUrl);
|
const relay = await pool().ensureRelay(relayUrl);
|
||||||
const pub = relay.publish(signedEvent);
|
const pub = relay.publish(signedEvent);
|
||||||
|
return waitCommandResult(pub);
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
pub.on('ok', () => {
|
|
||||||
console.log(`${relayUrl} has accepted our event`);
|
|
||||||
resolve(null);
|
|
||||||
});
|
|
||||||
pub.on('failed', (reason: any) => {
|
|
||||||
console.log(`failed to publish to ${relayUrl}: ${reason}`);
|
|
||||||
reject(reason);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return { publishTextNote };
|
return {
|
||||||
|
// NIP-01
|
||||||
|
publishTextNote({
|
||||||
|
relayUrls,
|
||||||
|
pubkey,
|
||||||
|
content,
|
||||||
|
}: {
|
||||||
|
relayUrls: string[];
|
||||||
|
pubkey: string;
|
||||||
|
content: string;
|
||||||
|
}): Promise<Promise<void>[]> {
|
||||||
|
const preSignedEvent: NostrEvent = {
|
||||||
|
kind: 1,
|
||||||
|
pubkey,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [],
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
// TODO define window.nostr
|
||||||
|
return publishEvent(relayUrls, preSignedEvent);
|
||||||
|
},
|
||||||
|
// NIP-25
|
||||||
|
publishReaction({
|
||||||
|
relayUrls,
|
||||||
|
pubkey,
|
||||||
|
content,
|
||||||
|
eventId,
|
||||||
|
notifyPubkey,
|
||||||
|
}: {
|
||||||
|
relayUrls: string[];
|
||||||
|
pubkey: string;
|
||||||
|
content: string;
|
||||||
|
eventId: string;
|
||||||
|
notifyPubkey: string;
|
||||||
|
}): Promise<Promise<void>[]> {
|
||||||
|
// TODO ensure that content is + or - or emoji.
|
||||||
|
const preSignedEvent: NostrEvent = {
|
||||||
|
kind: 7,
|
||||||
|
pubkey,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [
|
||||||
|
['e', eventId],
|
||||||
|
['p', notifyPubkey],
|
||||||
|
],
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
// TODO define window.nostr
|
||||||
|
return publishEvent(relayUrls, preSignedEvent);
|
||||||
|
},
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useCommands;
|
export default useCommands;
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
|
import { type Event as NostrEvent } from 'nostr-tools/event';
|
||||||
|
import { type Accessor } from 'solid-js';
|
||||||
|
|
||||||
import useCachedEvents from '@/clients/useCachedEvents';
|
import useCachedEvents from '@/clients/useCachedEvents';
|
||||||
|
|
||||||
type UseEventProps = {
|
export type UseEventProps = {
|
||||||
relayUrls: string[];
|
relayUrls: string[];
|
||||||
eventId: string;
|
eventId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useEvent = (propsProvider: () => UseEventProps) => {
|
export type UseEvent = {
|
||||||
|
event: Accessor<NostrEvent>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useEvent = (propsProvider: () => UseEventProps): UseEvent => {
|
||||||
const query = useCachedEvents(() => {
|
const query = useCachedEvents(() => {
|
||||||
const { relayUrls, eventId } = propsProvider();
|
const { relayUrls, eventId } = propsProvider();
|
||||||
return {
|
return {
|
||||||
@@ -20,7 +27,7 @@ const useEvent = (propsProvider: () => UseEventProps) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const event = () => query.data?.[0];
|
const event = () => query.data?.[0] as NostrEvent;
|
||||||
|
|
||||||
return { event };
|
return { event };
|
||||||
};
|
};
|
||||||
|
|||||||
0
src/clients/useReactions.ts
Normal file
0
src/clients/useReactions.ts
Normal file
43
src/components/DeprecatedRepost.tsx
Normal file
43
src/components/DeprecatedRepost.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// NIP-18 (DEPRECATED)
|
||||||
|
import { Show, type Component } from 'solid-js';
|
||||||
|
import { Event as NostrEvent } from 'nostr-tools/event';
|
||||||
|
import ArrowPathRoundedSquare from 'heroicons/24/outline/arrow-path-rounded-square.svg';
|
||||||
|
|
||||||
|
import useConfig from '@/clients/useConfig';
|
||||||
|
import useEvent from '@/clients/useEvent';
|
||||||
|
import useProfile from '@/clients/useProfile';
|
||||||
|
|
||||||
|
import TextNote from '@/components/TextNote';
|
||||||
|
|
||||||
|
export type DeprecatedRepostProps = {
|
||||||
|
event: NostrEvent;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DeprecatedRepost: Component<DeprecatedRepostProps> = (props) => {
|
||||||
|
const [config] = useConfig();
|
||||||
|
const pubkey = () => props.event.pubkey;
|
||||||
|
const eventId = () => props.event.tags.find(([tagName]) => tagName === 'e')?.[1];
|
||||||
|
|
||||||
|
if (eventId() == null) {
|
||||||
|
return 'event not found';
|
||||||
|
}
|
||||||
|
|
||||||
|
const { profile } = useProfile(() => ({ relayUrls: config().relayUrls, pubkey: pubkey() }));
|
||||||
|
const { event } = useEvent(() => ({ relayUrls: config().relayUrls, eventId: eventId() }));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div class="flex content-center px-1 text-xs">
|
||||||
|
<div class="h-5 w-5 pr-1 text-green-500" aria-hidden="true">
|
||||||
|
<ArrowPathRoundedSquare />
|
||||||
|
</div>
|
||||||
|
<div>{profile()?.display_name} Reposted</div>
|
||||||
|
</div>
|
||||||
|
<Show when={event() != null}>
|
||||||
|
<TextNote event={event()} />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeprecatedRepost;
|
||||||
@@ -1,17 +1,24 @@
|
|||||||
import { createMemo, Show, For } from 'solid-js';
|
import { Show, For, createSignal, createMemo, onMount, onCleanup } from 'solid-js';
|
||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
import type { Event as NostrEvent } from 'nostr-tools/event';
|
import type { Event as NostrEvent } from 'nostr-tools/event';
|
||||||
|
|
||||||
|
import HeartOutlined from 'heroicons/24/outline/heart.svg';
|
||||||
|
import ArrowPathRoundedSquare from 'heroicons/24/outline/arrow-path-rounded-square.svg';
|
||||||
|
|
||||||
import useProfile from '@/clients/useProfile';
|
import useProfile from '@/clients/useProfile';
|
||||||
import useConfig from '@/clients/useConfig';
|
import useConfig from '@/clients/useConfig';
|
||||||
import TextNoteContentDisplay from '@/components/textNote/TextNoteContentDisplay';
|
import useDatePulser from '@/hooks/useDatePulser';
|
||||||
|
import { formatRelative } from '@/utils/formatDate';
|
||||||
import ColumnItem from '@/components/ColumnItem';
|
import ColumnItem from '@/components/ColumnItem';
|
||||||
import GeneralUserMentionDisplay from './textNote/GeneralUserMentionDisplay';
|
import GeneralUserMentionDisplay from '@/components/textNote/GeneralUserMentionDisplay';
|
||||||
|
import TextNoteContentDisplay from '@/components/textNote/TextNoteContentDisplay';
|
||||||
|
|
||||||
export type TextNoteProps = {
|
export type TextNoteProps = {
|
||||||
event: NostrEvent;
|
event: NostrEvent;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TextNote: Component<TextNoteProps> = (props) => {
|
const TextNote: Component<TextNoteProps> = (props) => {
|
||||||
|
const currentDate = useDatePulser();
|
||||||
const [config] = useConfig();
|
const [config] = useConfig();
|
||||||
const { profile: author } = useProfile(() => ({
|
const { profile: author } = useProfile(() => ({
|
||||||
relayUrls: config().relayUrls,
|
relayUrls: config().relayUrls,
|
||||||
@@ -21,7 +28,7 @@ const TextNote: Component<TextNoteProps> = (props) => {
|
|||||||
props.event.tags.filter((tag) => tag[0] === 'p').map((e) => e[1]),
|
props.event.tags.filter((tag) => tag[0] === 'p').map((e) => e[1]),
|
||||||
);
|
);
|
||||||
// TODO 日付をいい感じにフォーマットする関数を作る
|
// TODO 日付をいい感じにフォーマットする関数を作る
|
||||||
const createdAt = () => new Date(props.event.created_at * 1000).toLocaleTimeString();
|
const createdAt = () => formatRelative(new Date(props.event.created_at * 1000), currentDate());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="textnote">
|
<div class="textnote">
|
||||||
@@ -43,7 +50,7 @@ const TextNote: Component<TextNoteProps> = (props) => {
|
|||||||
<Show when={author()?.display_name}>
|
<Show when={author()?.display_name}>
|
||||||
<div class="author-name pr-1 font-bold">{author()?.display_name}</div>
|
<div class="author-name pr-1 font-bold">{author()?.display_name}</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class="author-username truncate">
|
<div class="author-username truncate text-zinc-600">
|
||||||
<Show when={author()?.name} fallback={props.event.pubkey}>
|
<Show when={author()?.name} fallback={props.event.pubkey}>
|
||||||
@{author()?.name}
|
@{author()?.name}
|
||||||
</Show>
|
</Show>
|
||||||
@@ -66,6 +73,14 @@ const TextNote: Component<TextNoteProps> = (props) => {
|
|||||||
<div class="content whitespace-pre-wrap break-all">
|
<div class="content whitespace-pre-wrap break-all">
|
||||||
<TextNoteContentDisplay event={props.event} />
|
<TextNoteContentDisplay event={props.event} />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex justify-evenly">
|
||||||
|
<button class="h-4 w-4 text-zinc-400">
|
||||||
|
<ArrowPathRoundedSquare />
|
||||||
|
</button>
|
||||||
|
<button class="h-4 w-4 text-zinc-400">
|
||||||
|
<HeartOutlined />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ColumnItem>
|
</ColumnItem>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { For, Switch, Match, type Component } from 'solid-js';
|
|||||||
import type { Event as NostrEvent } from 'nostr-tools/event';
|
import type { Event as NostrEvent } from 'nostr-tools/event';
|
||||||
|
|
||||||
import TextNote from '@/components/TextNote';
|
import TextNote from '@/components/TextNote';
|
||||||
|
import DeprecatedRepost from '@/components/DeprecatedRepost';
|
||||||
|
|
||||||
export type TimelineProps = {
|
export type TimelineProps = {
|
||||||
events: NostrEvent[];
|
events: NostrEvent[];
|
||||||
@@ -16,7 +17,7 @@ export const Timeline: Component<TimelineProps> = (props) => {
|
|||||||
<TextNote event={event} />
|
<TextNote event={event} />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={event.kind === 6}>
|
<Match when={event.kind === 6}>
|
||||||
<div>Deprecated Repost</div>
|
<DeprecatedRepost event={event} />
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export type TextNoteContentDisplayProps = {
|
|||||||
event: NostrEvent;
|
event: NostrEvent;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
|
const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
|
||||||
return (
|
return (
|
||||||
<For each={parseTextNote(props.event)}>
|
<For each={parseTextNote(props.event)}>
|
||||||
{(item: ParsedTextNoteNode) => {
|
{(item: ParsedTextNoteNode) => {
|
||||||
|
|||||||
13
src/hooks/useDatePulser.ts
Normal file
13
src/hooks/useDatePulser.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { createSignal, type Accessor } from 'solid-js';
|
||||||
|
|
||||||
|
const [currentDate, setCurrentDate] = createSignal(new Date());
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
setCurrentDate(new Date());
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
const useDatePulser = (): Accessor<Date> => {
|
||||||
|
return currentDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDatePulser;
|
||||||
@@ -8,8 +8,16 @@ export type UseMessageChannelProps = {
|
|||||||
id: typeof CommandChannel;
|
id: typeof CommandChannel;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useMessageChannel = (propsProvider: () => UseMessageChannelProps) => {
|
export type MessageChannelRequest<T> = {
|
||||||
const channel = () => channels()[id];
|
requestId: string;
|
||||||
|
message: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Primitives = number | string | null;
|
||||||
|
type Serializable = Record<string, Primitives | Array<Primitives>>;
|
||||||
|
|
||||||
|
const useMessageChannel = <T extends Serializable>(propsProvider: () => UseMessageChannelProps) => {
|
||||||
|
const channel = () => channels()[propsProvider().id];
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const { id } = propsProvider();
|
const { id } = propsProvider();
|
||||||
@@ -21,17 +29,18 @@ const useMessageChannel = (propsProvider: () => UseMessageChannelProps) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const listen = async (requestId: string, timeout = 1000) => {
|
const listen = async (requestId: string, timeout = 1000): Promise<T> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const listener = (event: MessageEvent) => {
|
const listener = (event: MessageEvent) => {
|
||||||
if (event.origin !== window.location.origin) return;
|
if (event.origin !== window.location.origin) return;
|
||||||
|
if (typeof event.data !== 'string') return;
|
||||||
|
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data) as MessageChannelRequest<T>;
|
||||||
|
|
||||||
if (data.requestId !== requestId) return;
|
if (data.requestId !== requestId) return;
|
||||||
|
|
||||||
channel().port2.removeEventListener('message', listener);
|
channel().port2.removeEventListener('message', listener);
|
||||||
resolve(data);
|
resolve(data.message);
|
||||||
};
|
};
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -44,14 +53,15 @@ const useMessageChannel = (propsProvider: () => UseMessageChannelProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
async requst(message) {
|
async requst(message: T) {
|
||||||
const requestId = Math.random();
|
const requestId = Math.random().toString();
|
||||||
const messageStr = JSON.stringify({ ...message, requestId });
|
const messageStr = JSON.stringify({ message, requestId });
|
||||||
const response = listen(requestId, timeout);
|
const response = listen(requestId, timeout);
|
||||||
channel().postMessage(messageStr);
|
channel().postMessage(messageStr);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
handle(handler) {},
|
handle(handler) {},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default useMessageChannel;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import Column from '@/components/Column';
|
|||||||
import NotePostForm from '@/components/NotePostForm';
|
import NotePostForm from '@/components/NotePostForm';
|
||||||
import SideBar from '@/components/SideBar';
|
import SideBar from '@/components/SideBar';
|
||||||
import Timeline from '@/components/Timeline';
|
import Timeline from '@/components/Timeline';
|
||||||
|
import TextNote from '@/components/TextNote';
|
||||||
import useCommands from '@/clients/useCommands';
|
import useCommands from '@/clients/useCommands';
|
||||||
import useConfig from '@/clients/useConfig';
|
import useConfig from '@/clients/useConfig';
|
||||||
import useSubscription from '@/clients/useSubscription';
|
import useSubscription from '@/clients/useSubscription';
|
||||||
@@ -93,6 +94,18 @@ const Home: Component = () => {
|
|||||||
<SideBar postForm={() => <NotePostForm onPost={handlePost} />} />
|
<SideBar postForm={() => <NotePostForm onPost={handlePost} />} />
|
||||||
<div class="flex flex-row overflow-y-hidden overflow-x-scroll">
|
<div class="flex flex-row overflow-y-hidden overflow-x-scroll">
|
||||||
<Column name="ホーム" width="widest">
|
<Column name="ホーム" width="widest">
|
||||||
|
<TextNote
|
||||||
|
event={
|
||||||
|
{
|
||||||
|
id: 12345,
|
||||||
|
kind: 1,
|
||||||
|
pubkey: pubkeyHex,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [],
|
||||||
|
content: 'hello',
|
||||||
|
} as NostrEvent
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Timeline events={followingsPosts()} />
|
<Timeline events={followingsPosts()} />
|
||||||
</Column>
|
</Column>
|
||||||
<Column name="自分の投稿" width="medium">
|
<Column name="自分の投稿" width="medium">
|
||||||
|
|||||||
67
src/utils/formatDate.ts
Normal file
67
src/utils/formatDate.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
type ParsedDate =
|
||||||
|
| { kind: 'abs'; value: Date }
|
||||||
|
| { kind: 'days'; value: number }
|
||||||
|
| { kind: 'hours'; value: number }
|
||||||
|
| { kind: 'minutes'; value: number }
|
||||||
|
| { kind: 'seconds'; value: number }
|
||||||
|
| { kind: 'now' };
|
||||||
|
|
||||||
|
export type DateFormatter = (parsedDate: ParsedDate) => string;
|
||||||
|
|
||||||
|
const defaultDateFormatter = (parsedDate: ParsedDate): string => {
|
||||||
|
switch (parsedDate.kind) {
|
||||||
|
case 'abs':
|
||||||
|
return parsedDate.value.toLocaleDateString();
|
||||||
|
case 'days':
|
||||||
|
return `${parsedDate.value}d`;
|
||||||
|
case 'hours':
|
||||||
|
return `${parsedDate.value}h`;
|
||||||
|
case 'minutes':
|
||||||
|
return `${parsedDate.value}m`;
|
||||||
|
case 'seconds':
|
||||||
|
return `${parsedDate.value}s`;
|
||||||
|
case 'now':
|
||||||
|
return 'now';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const calcDiffSec = (date: Date, currentDate: Date): number =>
|
||||||
|
(Number(currentDate) - Number(date)) / 1000;
|
||||||
|
|
||||||
|
const parseDateDiff = (date: Date, currentDate: Date): ParsedDate => {
|
||||||
|
const diffSec = calcDiffSec(date, currentDate);
|
||||||
|
|
||||||
|
if (diffSec < 10) {
|
||||||
|
return { kind: 'now' };
|
||||||
|
}
|
||||||
|
if (diffSec < 60) {
|
||||||
|
return { kind: 'seconds', value: Math.round(diffSec) };
|
||||||
|
}
|
||||||
|
if (diffSec < 3600) {
|
||||||
|
return { kind: 'minutes', value: Math.round(diffSec / 60) };
|
||||||
|
}
|
||||||
|
if (diffSec < 86400) {
|
||||||
|
// 1 days
|
||||||
|
return { kind: 'hours', value: Math.round(diffSec / 3600) };
|
||||||
|
}
|
||||||
|
if (diffSec < 604800) {
|
||||||
|
// 1 week
|
||||||
|
return { kind: 'days', value: Math.round(diffSec / 86400) };
|
||||||
|
}
|
||||||
|
return { kind: 'abs', value: date };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatAbsolute = (date: Date, currentDate: Date = new Date()): string => {
|
||||||
|
if (date.getDate() === currentDate.getDate()) {
|
||||||
|
return date.toLocaleTimeString();
|
||||||
|
}
|
||||||
|
return date.toLocaleString();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatRelative = (
|
||||||
|
date: Date,
|
||||||
|
currentDate: Date = new Date(),
|
||||||
|
formatter: DateFormatter = defaultDateFormatter,
|
||||||
|
): string => formatter(parseDateDiff(date, currentDate));
|
||||||
Reference in New Issue
Block a user