From 177b96df32f08813b43d5d1bae4277d9437b7ce5 Mon Sep 17 00:00:00 2001 From: Shusui MOYATANI Date: Fri, 26 May 2023 23:14:15 +0900 Subject: [PATCH] update --- src/components/NotePostForm.tsx | 74 ++++++++++-------- src/components/SideBar.tsx | 2 +- src/components/column/Column.tsx | 6 +- src/components/column/Columns.tsx | 2 +- src/components/column/FollwingColumn.tsx | 1 + ...ChannelInfo.tsx => ChannelMetaDisplay.tsx} | 27 +------ src/components/event/LongFormContent.tsx | 37 +++++++++ src/components/modal/Config.tsx | 53 ++++++++++++- src/core/column.ts | 2 +- src/core/contentFilter.ts | 4 + src/core/useConfig.ts | 11 ++- src/hooks/useImageAnimation.tsx | 77 +++++++++++++++++++ src/index.css | 25 ++++-- src/nostr/event.ts | 3 + src/nostr/event/channel.tsx | 23 ++++++ src/nostr/useBatchedEvents.ts | 1 + src/nostr/useCommands.test.ts | 50 +++++++++++- src/nostr/useCommands.ts | 6 +- src/nostr/useSubscription.ts | 45 +++++++---- src/utils/emojipack.ts | 5 ++ src/utils/formatDate.ts | 2 +- 21 files changed, 366 insertions(+), 90 deletions(-) rename src/components/event/{ChannelInfo.tsx => ChannelMetaDisplay.tsx} (50%) create mode 100644 src/components/event/LongFormContent.tsx create mode 100644 src/hooks/useImageAnimation.tsx create mode 100644 src/nostr/event/channel.tsx diff --git a/src/components/NotePostForm.tsx b/src/components/NotePostForm.tsx index ed6987e..0a95a2c 100644 --- a/src/components/NotePostForm.tsx +++ b/src/components/NotePostForm.tsx @@ -136,20 +136,23 @@ const NotePostForm: Component = (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 = (props) => { ...notifyPubkeys(), ...pubkeyReferences, // 本文中の公開鍵(npub) ]), - rootEventId: replyTo()?.rootEvent()?.id ?? replyTo()?.id, + rootEventId: replyTo()?.rootEvent()?.id, replyEventId: replyTo()?.id, }; } @@ -232,22 +235,6 @@ const NotePostForm: Component = (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 = (ev) => { setText(ev.currentTarget.value); resizeTextArea(); @@ -267,10 +254,29 @@ const NotePostForm: Component = (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 = (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 = (props) => { const handleDrop: JSX.EventHandler = (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 = (props) => { } }); if (files.length === 0) return; - if (!ensureUploaderAgreement()) return; + // if (!ensureUploaderAgreement()) return; uploadFilesMutation.mutate(files); }; diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx index 127980e..aa49643 100644 --- a/src/components/SideBar.tsx +++ b/src/components/SideBar.tsx @@ -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(''); }; diff --git a/src/components/column/Column.tsx b/src/components/column/Column.tsx index 3761f3b..b52e678 100644 --- a/src/components/column/Column.tsx +++ b/src/components/column/Column.tsx @@ -57,7 +57,9 @@ const Column: Component = (props) => { fallback={ <>
{props.header}
-
{props.children}
+
+ {props.children} +
} > @@ -74,7 +76,7 @@ const Column: Component = (props) => {
ホームに戻る
-
    +
    diff --git a/src/components/column/Columns.tsx b/src/components/column/Columns.tsx index 7e4999e..eb174b9 100644 --- a/src/components/column/Columns.tsx +++ b/src/components/column/Columns.tsx @@ -12,7 +12,7 @@ const Columns = () => { const { config } = useConfig(); return ( -
    +
    {(column, index) => { const columnIndex = () => index() + 1; diff --git a/src/components/column/FollwingColumn.tsx b/src/components/column/FollwingColumn.tsx index 3434332..e3d50c4 100644 --- a/src/components/column/FollwingColumn.tsx +++ b/src/components/column/FollwingColumn.tsx @@ -29,6 +29,7 @@ const FollowingColumn: Component = (props) => { const authors = uniq([...followingPubkeys()]); if (authors.length === 0) return null; return { + debugId: 'following', relayUrls: config().relayUrls, filters: [ { diff --git a/src/components/event/ChannelInfo.tsx b/src/components/event/ChannelMetaDisplay.tsx similarity index 50% rename from src/components/event/ChannelInfo.tsx rename to src/components/event/ChannelMetaDisplay.tsx index 3b200e3..e539196 100644 --- a/src/components/event/ChannelInfo.tsx +++ b/src/components/event/ChannelMetaDisplay.tsx @@ -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; - -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 = (props) => { - const parsedContent = () => parseContent(props.event.content); + const parsedContent = () => parseChannelMeta(props.event.content); return ( diff --git a/src/components/event/LongFormContent.tsx b/src/components/event/LongFormContent.tsx new file mode 100644 index 0000000..d1afeff --- /dev/null +++ b/src/components/event/LongFormContent.tsx @@ -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 = (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 ( + + ); +}; + +export default LongFormContent; diff --git a/src/components/modal/Config.tsx b/src/components/modal/Config.tsx index b71644e..909972f 100644 --- a/src/components/modal/Config.tsx +++ b/src/components/modal/Config.tsx @@ -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 = (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 ( +
    +

    絵文字のインポート

    +

    絵文字の名前をキー、画像のURLを値とするJSONを読み込むことができます。

    +
    +