This commit is contained in:
Shusui MOYATANI
2023-05-26 23:14:15 +09:00
parent 7a9632bc48
commit 177b96df32
21 changed files with 366 additions and 90 deletions

View File

@@ -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);
};

View File

@@ -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('');
};

View File

@@ -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>

View File

@@ -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;

View File

@@ -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: [
{

View File

@@ -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>

View 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;

View File

@@ -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: () => 'ミュート',