feat: support video

This commit is contained in:
Shusui MOYATANI
2023-07-02 19:14:12 +09:00
parent 916fc8707f
commit 2d109f2f42
10 changed files with 70 additions and 26 deletions

View File

@@ -1,7 +1,7 @@
import { Component, createSignal, Show } from 'solid-js';
import SafeLink from '@/components/utils/SafeLink';
import { fixUrl } from '@/utils/imageUrl';
import { fixUrl } from '@/utils/url';
type ImageDisplayProps = {
url: string;

View File

@@ -8,6 +8,7 @@ import EventDisplayById from '@/components/event/EventDisplayById';
import ImageDisplay from '@/components/event/textNote/ImageDisplay';
import MentionedEventDisplay from '@/components/event/textNote/MentionedEventDisplay';
import MentionedUserDisplay from '@/components/event/textNote/MentionedUserDisplay';
import VideoDisplay from '@/components/event/textNote/VideoDisplay';
import EventLink from '@/components/EventLink';
import SafeLink from '@/components/utils/SafeLink';
import { createSearchColumn } from '@/core/column';
@@ -15,7 +16,7 @@ import useConfig from '@/core/useConfig';
import { useRequestCommand } from '@/hooks/useCommandBus';
import { textNote } from '@/nostr/event';
import { type ParsedTextNoteNode } from '@/nostr/parseTextNote';
import { isImageUrl } from '@/utils/imageUrl';
import { isImageUrl, isVideoUrl } from '@/utils/url';
export type TextNoteContentDisplayProps = {
event: NostrEvent;
@@ -41,15 +42,14 @@ const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
return <span>{item.content}</span>;
}
if (item.type === 'URL') {
const initialHidden = () =>
!config().showMedia || event().contentWarning().contentWarning || !props.embedding;
if (isImageUrl(item.content)) {
return (
<ImageDisplay
url={item.content}
initialHidden={
!config().showImage || event().contentWarning().contentWarning || !props.embedding
}
/>
);
return <ImageDisplay url={item.content} initialHidden={initialHidden()} />;
}
if (isVideoUrl(item.content)) {
return <VideoDisplay url={item.content} initialHidden={initialHidden()} />;
}
return <SafeLink class="text-blue-500 underline" href={item.content} />;
}

View File

@@ -0,0 +1,41 @@
import { Component, createSignal, Show } from 'solid-js';
import SafeLink from '@/components/utils/SafeLink';
type VideoDisplayProps = {
url: string;
initialHidden: boolean;
};
const VideoDisplay: Component<VideoDisplayProps> = (props) => {
let videoRef: HTMLVideoElement | undefined;
const [hidden, setHidden] = createSignal(props.initialHidden);
return (
<Show
when={!hidden()}
fallback={
<button
class="rounded bg-stone-300 p-3 text-xs text-stone-600 hover:shadow"
onClick={() => setHidden(false)}
>
</button>
}
>
<SafeLink class="my-2 block" href={props.url}>
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
<video
ref={videoRef}
class="max-h-64 max-w-full rounded-sm object-contain shadow"
src={props.url}
controls
>
<a href={props.url}></a>
</video>
</SafeLink>
</Show>
);
};
export default VideoDisplay;

View File

@@ -451,10 +451,10 @@ const OtherConfig = () => {
}));
};
const toggleShowImage = () => {
const toggleShowMedia = () => {
setConfig((current) => ({
...current,
showImage: !(current.showImage ?? true),
showMedia: !(current.showMedia ?? true),
}));
};
@@ -477,8 +477,8 @@ const OtherConfig = () => {
/>
</div>
<div class="flex w-full">
<div class="flex-1">{i18n()('config.display.showImagesByDefault')}</div>
<ToggleButton value={config().showImage} onClick={() => toggleShowImage()} />
<div class="flex-1">{i18n()('config.display.showMediaByDefault')}</div>
<ToggleButton value={config().showMedia} onClick={() => toggleShowMedia()} />
</div>
<div class="flex w-full">
<div class="flex-1">{i18n()('config.display.hideNumbers')}</div>

View File

@@ -32,7 +32,7 @@ export type Config = {
keepOpenPostForm: boolean;
useEmojiReaction: boolean;
showEmojiReaction: boolean;
showImage: boolean;
showMedia: boolean; // TODO 'always' | 'only-followings' | 'never'
hideCount: boolean;
mutedPubkeys: string[];
mutedKeywords: string[];
@@ -80,7 +80,7 @@ const InitialConfig = (): Config => ({
keepOpenPostForm: false,
useEmojiReaction: false,
showEmojiReaction: false,
showImage: true,
showMedia: true,
hideCount: false,
mutedPubkeys: [],
mutedKeywords: [],

View File

@@ -98,7 +98,7 @@ export default {
showEmojiReaction: 'Show emoji reactions on posts',
others: 'Others',
keepOpenPostForm: 'Remain the input field open after posting',
showImagesByDefault: 'Load images by default',
showMediaByDefault: 'Load media by default',
hideNumbers: 'Hide the numbers of reactions, reposts and followers',
},
customEmoji: {

View File

@@ -96,7 +96,7 @@ export default {
showEmojiReaction: '投稿にリアクションされた絵文字を表示する',
others: 'その他',
keepOpenPostForm: '投稿後も投稿欄を開いたままにする',
showImagesByDefault: 'デフォルトで画像を読み込む',
showMediaByDefault: 'デフォルトで画像や動画を読み込む',
hideNumbers: 'いいねやリポスト、フォロワーなどの数を隠す',
},
customEmoji: {

View File

@@ -1,15 +1,9 @@
import { z } from 'zod';
import { isImageUrl } from '@/utils/imageUrl';
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(),
picture: z.string().url().optional(),
});
export type ChannelMeta = z.infer<typeof ChannelMetaSchema>;

View File

@@ -2,7 +2,7 @@ import assert from 'assert';
import { describe, it } from 'vitest';
import { fixUrl } from '@/utils/imageUrl';
import { fixUrl } from '@/utils/url';
describe('fixUrl', () => {
it('should return an image url for a given imgur.com URL with additional path', () => {

View File

@@ -7,6 +7,15 @@ export const isImageUrl = (urlString: string): boolean => {
}
};
export const isVideoUrl = (urlString: string): boolean => {
try {
const url = new URL(urlString);
return /\.(mpg|mpeg|mp4|avi|mov|webm|ogv)$/i.test(url.pathname);
} catch {
return false;
}
};
export const fixUrl = (urlString: string): string => {
try {
const url = new URL(urlString);