feat: support CW and enhance smartphone support

This commit is contained in:
Shusui MOYATANI
2023-03-18 16:37:29 +09:00
parent 4f10489d19
commit fdaa894c8f
7 changed files with 82 additions and 35 deletions

View File

@@ -1,30 +1,18 @@
import type { Component, JSX } from 'solid-js'; import type { Component, JSX } from 'solid-js';
import { useHandleCommand } from '@/hooks/useCommandBus'; import { useHandleCommand } from '@/hooks/useCommandBus';
const widthToClass = {
widest: 'w-[500px]',
wide: 'w-[350px]',
medium: 'w-[310px]',
narrow: 'w-[270px]',
} as const;
type ColumnProps = { type ColumnProps = {
name: string; name: string;
columnIndex: number; columnIndex: number;
lastColumn?: true; lastColumn?: true;
width: keyof typeof widthToClass | null | undefined; width: 'widest' | 'wide' | 'medium' | 'narrow' | null | undefined;
children: JSX.Element; children: JSX.Element;
}; };
const Column: Component<ColumnProps> = (props) => { const Column: Component<ColumnProps> = (props) => {
let columnDivRef: HTMLDivElement | undefined; let columnDivRef: HTMLDivElement | undefined;
const width = () => { const width = () => props.width ?? 'medium';
if (props.width == null) {
return widthToClass.medium;
}
return widthToClass[props.width];
};
useHandleCommand(() => ({ useHandleCommand(() => ({
commandType: 'moveToColumn', commandType: 'moveToColumn',
@@ -45,7 +33,16 @@ const Column: Component<ColumnProps> = (props) => {
})); }));
return ( return (
<div ref={columnDivRef} class={`flex shrink-0 flex-col border-r ${width()}`}> <div
ref={columnDivRef}
class="flex w-[80vw] shrink-0 snap-center snap-always flex-col border-r sm:snap-align-none"
classList={{
'sm:w-[500px]': width() === 'widest',
'sm:w-[350px]': width() === 'wide',
'sm:w-[310px]': width() === 'medium',
'sm:w-[270px]': width() === 'narrow',
}}
>
<div class="flex h-8 shrink-0 items-center border-b bg-white px-2"> <div class="flex h-8 shrink-0 items-center border-b bg-white px-2">
{/* <span class="column-icon">🏠</span> */} {/* <span class="column-icon">🏠</span> */}
<span class="column-name">{props.name}</span> <span class="column-name">{props.name}</span>

View File

@@ -14,6 +14,8 @@ import uniq from 'lodash/uniq';
import PaperAirplane from 'heroicons/24/solid/paper-airplane.svg'; import PaperAirplane from 'heroicons/24/solid/paper-airplane.svg';
import Photo from 'heroicons/24/outline/photo.svg'; import Photo from 'heroicons/24/outline/photo.svg';
import Eye from 'heroicons/24/solid/eye.svg';
import EyeSlash from 'heroicons/24/outline/eye-slash.svg';
import XMark from 'heroicons/24/outline/x-mark.svg'; import XMark from 'heroicons/24/outline/x-mark.svg';
import UserNameDisplay from '@/components/UserDisplayName'; import UserNameDisplay from '@/components/UserDisplayName';
@@ -50,10 +52,14 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
let fileInputRef: HTMLInputElement | undefined; let fileInputRef: HTMLInputElement | undefined;
const [text, setText] = createSignal<string>(''); const [text, setText] = createSignal<string>('');
const [isUploading, setIsUploading] = createSignal(false); const [contentWarning, setContentWarning] = createSignal(false);
const [isDragging, setIsDragging] = createSignal(false); const [contentWarningReason, setContentWarningReason] = createSignal('');
const clearText = () => setText(''); const clearText = () => {
setText('');
setContentWarningReason('');
setContentWarning(false);
};
const { config } = useConfig(); const { config } = useConfig();
const getPubkey = usePubkey(); const getPubkey = usePubkey();
@@ -117,14 +123,26 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
console.error('pubkey is not available'); console.error('pubkey is not available');
return; return;
} }
publishTextNoteMutation.mutate({ let textNote: Parameters<typeof commands.publishTextNote>[0] = {
relayUrls: config().relayUrls, relayUrls: config().relayUrls,
pubkey, pubkey,
content: text(), content: text(),
notifyPubkeys: notifyPubkeys(pubkey), };
rootEventId: replyTo()?.rootEvent()?.id ?? replyTo()?.id, if (replyTo() != null) {
replyEventId: replyTo()?.id, textNote = {
}); ...textNote,
notifyPubkeys: notifyPubkeys(pubkey),
rootEventId: replyTo()?.rootEvent()?.id ?? replyTo()?.id,
replyEventId: replyTo()?.id,
};
}
if (contentWarning()) {
textNote = {
...textNote,
contentWarning: contentWarningReason(),
};
}
publishTextNoteMutation.mutate(textNote);
}; };
const handleInput: JSX.EventHandler<HTMLTextAreaElement, InputEvent> = (ev) => { const handleInput: JSX.EventHandler<HTMLTextAreaElement, InputEvent> = (ev) => {
@@ -162,11 +180,11 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
const handleDragOver: JSX.EventHandler<HTMLTextAreaElement, DragEvent> = (ev) => { const handleDragOver: JSX.EventHandler<HTMLTextAreaElement, DragEvent> = (ev) => {
ev.preventDefault(); ev.preventDefault();
setIsDragging(true);
}; };
const submitDisabled = () => const submitDisabled = () =>
text().trim().length === 0 || text().trim().length === 0 ||
(contentWarning() && contentWarningReason().length === 0) ||
publishTextNoteMutation.isLoading || publishTextNoteMutation.isLoading ||
uploadFilesMutation.isLoading; uploadFilesMutation.isLoading;
@@ -193,6 +211,16 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
</div> </div>
</Show> </Show>
<form class="flex flex-col gap-1" onSubmit={handleSubmit}> <form class="flex flex-col gap-1" onSubmit={handleSubmit}>
<Show when={contentWarning()}>
<input
type="text"
class="rounded"
placeholder="警告の理由"
maxLength={32}
onInput={(ev) => setContentWarningReason(ev.currentTarget.value)}
value={contentWarningReason()}
/>
</Show>
<textarea <textarea
ref={(el) => { ref={(el) => {
textAreaRef = el; textAreaRef = el;
@@ -216,6 +244,23 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
</button> </button>
</div> </div>
</Show> </Show>
<button
class="flex items-center justify-center rounded p-2 text-xs font-bold text-white hover:bg-rose-300"
classList={{
'bg-rose-300': !contentWarning(),
'bg-rose-400': contentWarning(),
'h-8': mode() === 'normal',
'w-8': mode() === 'normal',
'h-7': mode() === 'reply',
'w-7': mode() === 'reply',
}}
type="button"
area-label="コンテンツ警告を設定"
title="コンテンツ警告を設定"
onClick={() => setContentWarning((e) => !e)}
>
<span>CW</span>
</button>
<button <button
class="rounded bg-primary p-2 font-bold text-white" class="rounded bg-primary p-2 font-bold text-white"
classList={{ classList={{
@@ -227,6 +272,8 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
'w-7': mode() === 'reply', 'w-7': mode() === 'reply',
}} }}
type="button" type="button"
title="画像を投稿"
area-label="画像を投稿"
disabled={fileUploadDisabled()} disabled={fileUploadDisabled()}
onClick={() => fileInputRef?.click()} onClick={() => fileInputRef?.click()}
> >
@@ -243,6 +290,8 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
'w-7': mode() === 'reply', 'w-7': mode() === 'reply',
}} }}
type="submit" type="submit"
area-label="投稿"
title="投稿"
disabled={submitDisabled()} disabled={submitDisabled()}
> >
<PaperAirplane /> <PaperAirplane />

View File

@@ -23,7 +23,3 @@ code {
.link { .link {
@apply underline text-blue-500; @apply underline text-blue-500;
} }
.h-fill-available {
height: -webkit-fill-available;
}

View File

@@ -44,6 +44,7 @@ const useCommands = () => {
pubkey, pubkey,
content, content,
tags, tags,
contentWarning,
notifyPubkeys, notifyPubkeys,
rootEventId, rootEventId,
mentionEventIds, mentionEventIds,
@@ -57,16 +58,22 @@ const useCommands = () => {
rootEventId?: string; rootEventId?: string;
mentionEventIds?: string[]; mentionEventIds?: string[];
replyEventId?: string; replyEventId?: string;
contentWarning?: string;
}): Promise<Promise<void>[]> => { }): Promise<Promise<void>[]> => {
// NIP-10
const pTags = notifyPubkeys?.map((p) => ['p', p]) ?? []; const pTags = notifyPubkeys?.map((p) => ['p', p]) ?? [];
const eTags = []; const eTags = [];
// NIP-10
if (rootEventId != null) eTags.push(['e', rootEventId, '', 'root']); if (rootEventId != null) eTags.push(['e', rootEventId, '', 'root']);
if (mentionEventIds != null) if (mentionEventIds != null)
mentionEventIds.forEach((id) => eTags.push(['e', id, '', 'mention'])); mentionEventIds.forEach((id) => eTags.push(['e', id, '', 'mention']));
if (replyEventId != null) eTags.push(['e', replyEventId, '', 'reply']); if (replyEventId != null) eTags.push(['e', replyEventId, '', 'reply']);
const mergedTags = [...eTags, ...pTags, ...(tags ?? [])]; const additionalTags = tags != null ? [...tags] : [];
if (contentWarning != null && content.length > 0) {
additionalTags.push(['content-warning', contentWarning]);
}
const mergedTags = [...eTags, ...pTags, ...additionalTags];
const preSignedEvent: NostrEvent = { const preSignedEvent: NostrEvent = {
kind: 1, kind: 1,

View File

@@ -135,9 +135,9 @@ const Home: Component = () => {
}); });
return ( return (
<div class="flex h-screen w-screen flex-row overflow-hidden"> <div class="absolute inset-0 flex w-screen flex-row overflow-hidden">
<SideBar /> <SideBar />
<div class="flex h-full flex-row overflow-y-hidden overflow-x-scroll"> <div class="flex h-full snap-x snap-mandatory flex-row overflow-y-hidden overflow-x-scroll">
<Column name="ホーム" columnIndex={1} width="widest"> <Column name="ホーム" columnIndex={1} width="widest">
<Timeline events={followingsPosts()} /> <Timeline events={followingsPosts()} />
</Column> </Column>

View File

@@ -1,7 +1,5 @@
export const isImageUrl = (url: URL): boolean => { export const isImageUrl = (url: URL): boolean => {
if (url.pathname.match(/\.(jpeg|jpg|png|gif|webp)$/i)) return true; return /\.(jpeg|jpg|png|gif|webp)$/i.test(url.pathname);
return false;
}; };
export const fixUrl = (url: URL): URL => { export const fixUrl = (url: URL): URL => {