mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-17 14:04:21 +01:00
feat: support inline image
This commit is contained in:
@@ -35,9 +35,9 @@ const useEvent = (propsProvider: () => UseEventProps | null): UseEvent => {
|
||||
return timeout(15000, `useEvent: ${currentProps.eventId}`)(exec(currentProps, signal));
|
||||
},
|
||||
{
|
||||
// 5 minutes
|
||||
staleTime: 5 * 60 * 1000,
|
||||
cacheTime: 15 * 60 * 1000,
|
||||
// a hour
|
||||
staleTime: 60 * 60 * 1000,
|
||||
cacheTime: 60 * 60 * 1000,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createSignal, onMount, type Accessor } from 'solid-js';
|
||||
let asking = false;
|
||||
const [pubkey, setPubkey] = createSignal<string | undefined>(undefined);
|
||||
|
||||
// TODO 失敗したときに通知等を表示したい
|
||||
const usePubkey = (): Accessor<string | undefined> => {
|
||||
onMount(() => {
|
||||
let count = 0;
|
||||
|
||||
@@ -19,7 +19,7 @@ export type UseReactions = {
|
||||
};
|
||||
|
||||
const { exec } = useBatchedEvents<UseReactionsProps>(() => ({
|
||||
interval: 5000,
|
||||
interval: 3400,
|
||||
generateKey: ({ eventId }) => eventId,
|
||||
mergeFilters: (args) => {
|
||||
const eventIds = args.map((arg) => arg.eventId);
|
||||
@@ -43,9 +43,9 @@ const useReactions = (propsProvider: () => UseReactionsProps | null): UseReactio
|
||||
return timeout(15000, `useReactions: ${currentProps.eventId}`)(exec(currentProps, signal));
|
||||
},
|
||||
{
|
||||
// 1 minutes
|
||||
// 3 minutes
|
||||
staleTime: 1 * 60 * 1000,
|
||||
cacheTime: 1 * 60 * 1000,
|
||||
cacheTime: 3 * 60 * 1000,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
33
src/components/textNote/ImageDisplay.tsx
Normal file
33
src/components/textNote/ImageDisplay.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Component } from 'solid-js';
|
||||
|
||||
type ImageDisplayProps = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
const fixUrl = (url: URL): string => {
|
||||
const result = new URL(url);
|
||||
if (url.host === 'i.imgur.com') {
|
||||
const match = url.pathname.match(/^\/([a-zA-Z0-9]+)\.(jpg|jpeg|png|gif)/);
|
||||
if (match != null) {
|
||||
const imageId = match[1];
|
||||
result.pathname = `${imageId}l.webp`;
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
};
|
||||
|
||||
const ImageDisplay: Component<ImageDisplayProps> = (props) => {
|
||||
const url = () => new URL(props.url);
|
||||
|
||||
return (
|
||||
<a href={props.url} target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
class="max-h-full max-w-full rounded object-contain shadow"
|
||||
src={fixUrl(url())}
|
||||
alt={props.url}
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageDisplay;
|
||||
@@ -4,6 +4,7 @@ import type { Event as NostrEvent } from 'nostr-tools';
|
||||
import PlainTextDisplay from '@/components/textNote/PlainTextDisplay';
|
||||
import MentionedUserDisplay from '@/components/textNote/MentionedUserDisplay';
|
||||
import MentionedEventDisplay from '@/components/textNote/MentionedEventDisplay';
|
||||
import ImageDisplay from '@/components/textNote/ImageDisplay';
|
||||
|
||||
export type TextNoteContentDisplayProps = {
|
||||
event: NostrEvent;
|
||||
@@ -24,7 +25,22 @@ const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
|
||||
return <MentionedEventDisplay mentionedEvent={item} />;
|
||||
}
|
||||
if (item.type === 'HashTag') {
|
||||
return <span class="text-blue-500 underline ">{item.content}</span>;
|
||||
return <span class="text-blue-500 underline">{item.content}</span>;
|
||||
}
|
||||
if (item.type === 'URL') {
|
||||
if (item.content.match(/\.(jpeg|jpg|png|gif|webp)$/i)) {
|
||||
return <ImageDisplay url={item.content} />;
|
||||
}
|
||||
return (
|
||||
<a
|
||||
class="text-blue-500 underline"
|
||||
href={item.content}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{item.content}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
|
||||
@@ -26,25 +26,38 @@ export type HashTag = {
|
||||
tagName: string;
|
||||
};
|
||||
|
||||
export type ParsedTextNoteNode = PlainText | MentionedEvent | MentionedUser | HashTag;
|
||||
export type UrlText = {
|
||||
type: 'URL';
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type ParsedTextNoteNode = PlainText | MentionedEvent | MentionedUser | HashTag | UrlText;
|
||||
|
||||
export type ParsedTextNote = ParsedTextNoteNode[];
|
||||
|
||||
export const parseTextNote = (event: NostrEvent): ParsedTextNote => {
|
||||
const matches = Array.from(
|
||||
event.content.matchAll(/(?:#\[(?<idx>\d+)\]|#(?<hashtag>[^\[\]\(\)\s]+))/g),
|
||||
);
|
||||
const matches = [
|
||||
...event.content.matchAll(/(?:#\[(?<idx>\d+)\])/g),
|
||||
...event.content.matchAll(/#(?<hashtag>[^[]\(\)\s]+)/g),
|
||||
...event.content.matchAll(
|
||||
/(?<url>https?:\/\/[-a-zA-Z0-9.]+(?:\/[-\w.%]+|\/)*(?:\?[-\w=&]*)?(?:#[-\w]*)?)/g,
|
||||
),
|
||||
].sort((a, b) => a?.index - b?.index);
|
||||
let pos = 0;
|
||||
const result: ParsedTextNote = [];
|
||||
|
||||
const pushPlainText = (index: number | undefined) => {
|
||||
if (index != null && pos !== index) {
|
||||
const content = event.content.slice(pos, index);
|
||||
const plainText: PlainText = { type: 'PlainText', content };
|
||||
result.push(plainText);
|
||||
}
|
||||
};
|
||||
|
||||
matches.forEach((match) => {
|
||||
if (match.groups?.hashtag) {
|
||||
pushPlainText(match.index);
|
||||
const tagName = match.groups?.hashtag;
|
||||
if (pos !== match.index) {
|
||||
const content = event.content.slice(pos, match.index);
|
||||
const plainText: PlainText = { type: 'PlainText', content };
|
||||
result.push(plainText);
|
||||
}
|
||||
const hashtag: HashTag = {
|
||||
type: 'HashTag',
|
||||
content: match[0],
|
||||
@@ -55,11 +68,9 @@ export const parseTextNote = (event: NostrEvent): ParsedTextNote => {
|
||||
const tagIndex = parseInt(match.groups.idx, 10);
|
||||
const tag = event.tags[tagIndex];
|
||||
if (tag == null) return;
|
||||
if (pos !== match.index) {
|
||||
const content = event.content.slice(pos, match.index);
|
||||
const plainText: PlainText = { type: 'PlainText', content };
|
||||
result.push(plainText);
|
||||
}
|
||||
|
||||
pushPlainText(match.index);
|
||||
|
||||
const tagName = tag[0];
|
||||
if (tagName === 'p') {
|
||||
const mentionedUser: MentionedUser = {
|
||||
@@ -79,6 +90,10 @@ export const parseTextNote = (event: NostrEvent): ParsedTextNote => {
|
||||
};
|
||||
result.push(mentionedEvent);
|
||||
}
|
||||
} else if (match.groups?.url) {
|
||||
pushPlainText(match.index);
|
||||
const url: UrlText = { type: 'URL', content: match.groups?.url };
|
||||
result.push(url);
|
||||
}
|
||||
pos = match.index + match[0].length;
|
||||
});
|
||||
|
||||
@@ -49,6 +49,11 @@ const Hello: Component = () => {
|
||||
<div class="text-7xl">🐰</div>
|
||||
<h1 class="text-5xl font-bold text-rose-300">Rabbit</h1>
|
||||
<div>Rabbit is a Web client for Nostr.</div>
|
||||
<p class="text-center">
|
||||
<span class="font-bold text-rose-400">注意: 現在ベータ版です。</span>
|
||||
<br />
|
||||
未実装の機能やバグがあることを承知の上でご利用ください。
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-8 shadow-md">
|
||||
<Switch>
|
||||
|
||||
Reference in New Issue
Block a user