mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-20 23:44:20 +01:00
feat: support CW and enhance smartphone support
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -23,7 +23,3 @@ code {
|
|||||||
.link {
|
.link {
|
||||||
@apply underline text-blue-500;
|
@apply underline text-blue-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-fill-available {
|
|
||||||
height: -webkit-fill-available;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
Reference in New Issue
Block a user