mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-18 14:34:25 +01:00
update
This commit is contained in:
@@ -136,20 +136,23 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
||||
|
||||
const uploadFilesMutation = createMutation({
|
||||
mutationKey: ['uploadFiles'],
|
||||
mutationFn: (files: File[]) => {
|
||||
return uploadFiles(uploadNostrBuild)(files)
|
||||
.then((uploadResults) => {
|
||||
uploadResults.forEach((result) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
console.log('succeeded to upload', result);
|
||||
appendText(result.value.imageUrl);
|
||||
resizeTextArea();
|
||||
} else {
|
||||
console.error('failed to upload', result);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
mutationFn: async (files: File[]) => {
|
||||
const uploadResults = await uploadFiles(uploadNostrBuild)(files);
|
||||
const failed: File[] = [];
|
||||
|
||||
uploadResults.forEach((result, i) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
appendText(result.value.imageUrl);
|
||||
resizeTextArea();
|
||||
} else {
|
||||
failed.push(files[i]);
|
||||
}
|
||||
});
|
||||
|
||||
if (failed.length > 0) {
|
||||
const filenames = failed.map((f) => f.name).join(', ');
|
||||
window.alert(`ファイルのアップロードに失敗しました: ${filenames}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -218,7 +221,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
||||
...notifyPubkeys(),
|
||||
...pubkeyReferences, // 本文中の公開鍵(npub)
|
||||
]),
|
||||
rootEventId: replyTo()?.rootEvent()?.id ?? replyTo()?.id,
|
||||
rootEventId: replyTo()?.rootEvent()?.id,
|
||||
replyEventId: replyTo()?.id,
|
||||
};
|
||||
}
|
||||
@@ -232,22 +235,6 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
||||
close();
|
||||
};
|
||||
|
||||
const ensureUploaderAgreement = (): boolean => {
|
||||
if (didAgreeToToS('nostrBuild')) return true;
|
||||
|
||||
window.alert(
|
||||
'画像アップローダーの利用規約をお読みください。\n(新しいタブで利用規約を開きます)',
|
||||
);
|
||||
openLink(uploaders.nostrBuild.tos);
|
||||
const didAgree = window.confirm('同意する場合はOKをクリックしてください。');
|
||||
|
||||
if (didAgree) {
|
||||
agreeToToS('nostrBuild');
|
||||
}
|
||||
|
||||
return didAgree;
|
||||
};
|
||||
|
||||
const handleInput: JSX.EventHandler<HTMLTextAreaElement, InputEvent> = (ev) => {
|
||||
setText(ev.currentTarget.value);
|
||||
resizeTextArea();
|
||||
@@ -267,10 +254,29 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const ensureUploaderAgreement = (): boolean => {
|
||||
return true;
|
||||
/*
|
||||
if (didAgreeToToS('nostrBuild')) return true;
|
||||
|
||||
window.alert(
|
||||
'画像アップローダーの利用規約をお読みください。\n(新しいタブで利用規約を開きます)',
|
||||
);
|
||||
openLink(uploaders.nostrBuild.tos);
|
||||
const didAgree = window.confirm('同意する場合はOKをクリックしてください。');
|
||||
|
||||
if (didAgree) {
|
||||
agreeToToS('nostrBuild');
|
||||
}
|
||||
|
||||
return didAgree;
|
||||
*/
|
||||
};
|
||||
|
||||
const handleChangeFile: JSX.EventHandler<HTMLInputElement, Event> = (ev) => {
|
||||
ev.preventDefault();
|
||||
if (uploadFilesMutation.isLoading) return;
|
||||
if (!ensureUploaderAgreement()) return;
|
||||
// if (!ensureUploaderAgreement()) return;
|
||||
|
||||
const files = [...(ev.currentTarget.files ?? [])];
|
||||
uploadFilesMutation.mutate(files);
|
||||
@@ -281,7 +287,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
||||
const handleDrop: JSX.EventHandler<HTMLTextAreaElement, DragEvent> = (ev) => {
|
||||
ev.preventDefault();
|
||||
if (uploadFilesMutation.isLoading) return;
|
||||
if (!ensureUploaderAgreement()) return;
|
||||
// if (!ensureUploaderAgreement()) return;
|
||||
const files = [...(ev?.dataTransfer?.files ?? [])];
|
||||
uploadFilesMutation.mutate(files);
|
||||
};
|
||||
@@ -301,7 +307,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
||||
}
|
||||
});
|
||||
if (files.length === 0) return;
|
||||
if (!ensureUploaderAgreement()) return;
|
||||
// if (!ensureUploaderAgreement()) return;
|
||||
|
||||
uploadFilesMutation.mutate(files);
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ const SearchButton = () => {
|
||||
ev.preventDefault();
|
||||
|
||||
saveColumn(createSearchColumn({ query: query() }));
|
||||
request({ command: 'moveToLastColumn' }).catch((err) => console.log(err));
|
||||
request({ command: 'moveToLastColumn' }).catch((err) => console.error(err));
|
||||
popupRef?.close();
|
||||
setQuery('');
|
||||
};
|
||||
|
||||
@@ -57,7 +57,9 @@ const Column: Component<ColumnProps> = (props) => {
|
||||
fallback={
|
||||
<>
|
||||
<div class="shrink-0 border-b">{props.header}</div>
|
||||
<div class="flex flex-col overflow-y-scroll scroll-smooth">{props.children}</div>
|
||||
<div class="scrollbar flex flex-col overflow-y-scroll scroll-smooth">
|
||||
{props.children}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
@@ -74,7 +76,7 @@ const Column: Component<ColumnProps> = (props) => {
|
||||
<div>ホームに戻る</div>
|
||||
</button>
|
||||
</div>
|
||||
<ul class="flex h-full flex-col overflow-y-scroll scroll-smooth">
|
||||
<ul class="scrollbar flex h-full flex-col overflow-y-scroll scroll-smooth">
|
||||
<TimelineContentDisplay timelineContent={timeline} />
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ const Columns = () => {
|
||||
const { config } = useConfig();
|
||||
|
||||
return (
|
||||
<div class="flex h-full snap-x snap-mandatory flex-row overflow-y-hidden overflow-x-scroll">
|
||||
<div class="scrollbar flex h-full snap-x snap-mandatory flex-row overflow-y-hidden overflow-x-scroll">
|
||||
<For each={config().columns}>
|
||||
{(column, index) => {
|
||||
const columnIndex = () => index() + 1;
|
||||
|
||||
@@ -29,6 +29,7 @@ const FollowingColumn: Component<FollowingColumnDisplayProps> = (props) => {
|
||||
const authors = uniq([...followingPubkeys()]);
|
||||
if (authors.length === 0) return null;
|
||||
return {
|
||||
debugId: 'following',
|
||||
relayUrls: config().relayUrls,
|
||||
filters: [
|
||||
{
|
||||
|
||||
@@ -2,38 +2,15 @@ import { Component, Show } from 'solid-js';
|
||||
|
||||
import ChatBubbleLeftRight from 'heroicons/24/outline/chat-bubble-left-right.svg';
|
||||
import { Event as NostrEvent } from 'nostr-tools';
|
||||
import { z } from 'zod';
|
||||
|
||||
import EventLink from '@/components/EventLink';
|
||||
import { isImageUrl } from '@/utils/imageUrl';
|
||||
import { parseChannelMeta } from '@/nostr/event/channel';
|
||||
|
||||
export type ChannelInfoProps = {
|
||||
event: NostrEvent;
|
||||
};
|
||||
|
||||
const ChannelMetaSchema = z.object({
|
||||
name: z.string(),
|
||||
about: z.string().optional(),
|
||||
picture: z
|
||||
.string()
|
||||
.url()
|
||||
.refine((url) => isImageUrl(url), { message: 'not an image url' })
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type ChannelMeta = z.infer<typeof ChannelMetaSchema>;
|
||||
|
||||
const parseContent = (content: string): ChannelMeta | null => {
|
||||
try {
|
||||
return ChannelMetaSchema.parse(JSON.parse(content));
|
||||
} catch (err) {
|
||||
console.warn('failed to parse chat channel schema: ', err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const ChannelInfo: Component<ChannelInfoProps> = (props) => {
|
||||
const parsedContent = () => parseContent(props.event.content);
|
||||
const parsedContent = () => parseChannelMeta(props.event.content);
|
||||
|
||||
return (
|
||||
<Show when={parsedContent()} keyed>
|
||||
37
src/components/event/LongFormContent.tsx
Normal file
37
src/components/event/LongFormContent.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Component } from 'solid-js';
|
||||
|
||||
import DocumentText from 'heroicons/24/outline/document-text.svg';
|
||||
import { Kind, Event as NostrEvent } from 'nostr-tools';
|
||||
|
||||
import eventWrapper from '@/nostr/event';
|
||||
|
||||
export type LongFormContentProps = {
|
||||
event: NostrEvent;
|
||||
};
|
||||
|
||||
const LongFormContent: Component<LongFormContentProps> = (props) => {
|
||||
const event = () => eventWrapper(props.event);
|
||||
const getMeta = (name: string) => {
|
||||
const tags = event().findTagsByName(name);
|
||||
if (tags.length === 0) return null;
|
||||
const [, lastTagValue] = tags[tags.length - 1];
|
||||
return lastTagValue;
|
||||
};
|
||||
const title = () => getMeta('title');
|
||||
// const imageUrl = () => getMeta('image');
|
||||
// const summary = () => getMeta('summary');
|
||||
// const publishdAt = () => getMeta('published_at');
|
||||
|
||||
return (
|
||||
<button class="flex flex-col gap-1 px-1">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="inline-block h-4 w-4 text-purple-400">
|
||||
<DocumentText />
|
||||
</span>
|
||||
<span>{title()}</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default LongFormContent;
|
||||
@@ -13,6 +13,7 @@ import UserNameDisplay from '@/components/UserDisplayName';
|
||||
import useConfig, { type Config } from '@/core/useConfig';
|
||||
import useModalState from '@/hooks/useModalState';
|
||||
import usePubkey from '@/nostr/usePubkey';
|
||||
import { simpleEmojiPackSchema, convertToEmojiConfig } from '@/utils/emojipack';
|
||||
import ensureNonNull from '@/utils/ensureNonNull';
|
||||
|
||||
type ConfigProps = {
|
||||
@@ -228,6 +229,8 @@ const EmojiConfig = () => {
|
||||
ev.preventDefault();
|
||||
if (shortcodeInput().length === 0 || urlInput().length === 0) return;
|
||||
saveEmoji({ shortcode: shortcodeInput(), url: urlInput() });
|
||||
setShortcodeInput('');
|
||||
setUrlInput('');
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -253,6 +256,7 @@ const EmojiConfig = () => {
|
||||
class="flex-1 rounded-md focus:border-rose-100 focus:ring-rose-300"
|
||||
type="text"
|
||||
name="shortcode"
|
||||
placeholder="smiley"
|
||||
value={shortcodeInput()}
|
||||
pattern="^\\w+$"
|
||||
required
|
||||
@@ -266,7 +270,7 @@ const EmojiConfig = () => {
|
||||
type="text"
|
||||
name="url"
|
||||
value={urlInput()}
|
||||
placeholder="https://.../emoji.png"
|
||||
placeholder="https://example.com/smiley.png"
|
||||
pattern={HttpUrlRegex}
|
||||
required
|
||||
onChange={(ev) => setUrlInput(ev.currentTarget.value)}
|
||||
@@ -280,6 +284,46 @@ const EmojiConfig = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const EmojiImport = () => {
|
||||
const { saveEmojis } = useConfig();
|
||||
|
||||
const [jsonInput, setJSONInput] = createSignal('');
|
||||
|
||||
const handleClickSaveEmoji: JSX.EventHandler<HTMLFormElement, SubmitEvent> = (ev) => {
|
||||
ev.preventDefault();
|
||||
if (jsonInput().length === 0) return;
|
||||
|
||||
try {
|
||||
const data = simpleEmojiPackSchema.parse(JSON.parse(jsonInput()));
|
||||
const emojis = convertToEmojiConfig(data);
|
||||
saveEmojis(emojis);
|
||||
setJSONInput('');
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? `:${err.message}` : '';
|
||||
window.alert(`JSONの読み込みに失敗しました${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="py-2">
|
||||
<h3 class="font-bold">絵文字のインポート</h3>
|
||||
<p>絵文字の名前をキー、画像のURLを値とするJSONを読み込むことができます。</p>
|
||||
<form class="flex flex-col gap-2" onSubmit={handleClickSaveEmoji}>
|
||||
<textarea
|
||||
class="flex-1 rounded-md focus:border-rose-100 focus:ring-rose-300"
|
||||
name="json"
|
||||
value={jsonInput()}
|
||||
placeholder='{ "smiley": "https://example.com/smiley.png" }'
|
||||
onChange={(ev) => setJSONInput(ev.currentTarget.value)}
|
||||
/>
|
||||
<button type="submit" class="w-24 self-end rounded bg-rose-300 p-2 font-bold text-white">
|
||||
インポート
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MuteConfig = () => {
|
||||
const { config, removeMutedPubkey, addMutedKeyword, removeMutedKeyword } = useConfig();
|
||||
|
||||
@@ -428,7 +472,12 @@ const ConfigUI = (props: ConfigProps) => {
|
||||
{
|
||||
name: () => 'カスタム絵文字',
|
||||
icon: () => <FaceSmile />,
|
||||
render: () => <EmojiConfig />,
|
||||
render: () => (
|
||||
<>
|
||||
<EmojiConfig />
|
||||
<EmojiImport />
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: () => 'ミュート',
|
||||
|
||||
Reference in New Issue
Block a user