Skip to content

Commit

Permalink
feat: video uploads
Browse files Browse the repository at this point in the history
  • Loading branch information
mary-ext committed Oct 19, 2024
1 parent 7ffe8a8 commit a3495f0
Show file tree
Hide file tree
Showing 7 changed files with 354 additions and 39 deletions.
100 changes: 65 additions & 35 deletions src/components/composer/composer-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { createEventListener } from '~/lib/hooks/event-listener';
import { type GuardFunction, createGuard } from '~/lib/hooks/guard';
import { useAgent } from '~/lib/states/agent';
import { useSession } from '~/lib/states/session';
import { SUPPORTED_IMAGE_FORMATS, openImagePicker } from '~/lib/utils/blob';
import { SUPPORTED_IMAGE_FORMATS, SUPPORTED_VIDEO_FORMATS, openMediaPicker } from '~/lib/utils/blob';
import { assert } from '~/lib/utils/invariant';
import { on } from '~/lib/utils/misc';

Expand Down Expand Up @@ -69,9 +69,10 @@ import ImageEmbed from './embeds/image-embed';
import LinkEmbed from './embeds/link-embed';
import ListEmbed from './embeds/list-embed';
import QuoteEmbed from './embeds/quote-embed';
import VideoEmbed from './embeds/video-embed';
import type { GifMedia } from './gifs/gif-search-dialog';
import GifSearchDialogLazy from './gifs/gif-search-dialog-lazy';
import { publish } from './lib/api';
import { PublishError, publish } from './lib/api';
import { getRecordEmbedFromLink } from './lib/link-detection';
import {
type ComposerState,
Expand Down Expand Up @@ -187,7 +188,15 @@ const ComposerDialog = (props: ComposerDialogProps) => {

success = true;
} catch (err) {
setError(formatQueryError(err));
let message: string;

if (err instanceof PublishError) {
message = err.message;
} else {
message = formatQueryError(err);
}

setError(message);
} finally {
setPending(false);
}
Expand Down Expand Up @@ -491,16 +500,11 @@ const Post = ({
}}
</Show>

<Show
when={(() => {
const media = post.embed.media;
return media && (media.type === 'image' || media.type === 'gif');
})()}
>
<Show when={post.embed.media}>
<div class={`gap-2 text-contrast-muted` + (isActive() ? ` flex` : ` hidden`)}>
<CircleInfoOutlinedIcon class="mt-0.5 shrink-0 text-base" />
<p class="text-de">
Alt text helps describe images for low-vision users and provide context for everyone.
Alt text helps describe media for low-vision users and provide context for everyone.
</p>
</div>
</Show>
Expand Down Expand Up @@ -621,33 +625,48 @@ const PostAction = (props: {
return (embed || rtLength > 0) && rtLength < MAX_TEXT_LENGTH;
};

const canEmbedImage = () => {
const canEmbedImageOrVideo = () => {
const media = props.post.embed.media;
return !media || (media.type === 'image' && media.images.length < MAX_IMAGES);
};

const addImages = (blobs: Blob[]) => {
const images = blobs.filter((blob) => SUPPORTED_IMAGE_FORMATS.includes(blob.type));
if (images.length === 0) {
const addImagesOrVideo = (blobs: Blob[]) => {
const post = props.post;

const video = blobs.find((blob) => SUPPORTED_VIDEO_FORMATS.includes(blob.type));
if (video) {
let next = post.embed.media;
if (!next) {
next = {
type: 'video',
blob: video,
alt: '',
labels: [],
};
}

post.embed.media = next;
return;
}

const post = props.post;
const images = blobs.filter((blob) => SUPPORTED_IMAGE_FORMATS.includes(blob.type));
if (images.length) {
let next = post.embed.media;
if (!next) {
next = {
type: 'image',
images: images.slice(0, MAX_IMAGES).map((blob) => ({ blob, alt: '' })),
labels: [],
};
} else if (next.type === 'image') {
next.images = next.images.concat(
images.slice(0, MAX_IMAGES - next.images.length).map((blob) => ({ blob, alt: '' })),
);
}

let next = post.embed.media;
if (!next) {
next = {
type: 'image',
images: images.slice(0, MAX_IMAGES).map((blob) => ({ blob, alt: '' })),
labels: [],
};
} else if (next.type === 'image') {
next.images = next.images.concat(
images.slice(0, MAX_IMAGES - next.images.length).map((blob) => ({ blob, alt: '' })),
);
post.embed.media = next;
return;
}

post.embed.media = next;
};

const addGif = (gif: GifMedia) => {
Expand All @@ -674,9 +693,9 @@ const PostAction = (props: {
<IconButton
icon={ImageOutlinedIcon}
title="Attach image..."
disabled={!canEmbedImage()}
disabled={!canEmbedImageOrVideo()}
onClick={() => {
openImagePicker(addImages, true);
openMediaPicker(addImagesOrVideo, true);
}}
variant="accent"
/>
Expand Down Expand Up @@ -753,13 +772,13 @@ const PostAction = (props: {
</div>
</div>

{canEmbedImage() && <ImageDnd onAddImages={addImages} />}
{canEmbedImageOrVideo() && <MediaDnd onAddMedia={addImagesOrVideo} />}
</>
);
};

const ImageDnd = (props: { onAddImages: (blobs: Blob[]) => void }) => {
const onAddImages = props.onAddImages;
const MediaDnd = (props: { onAddMedia: (blobs: Blob[]) => void }) => {
const onAddMedia = props.onAddMedia;
const [dropping, setDropping] = createSignal(false);

let tracked: any;
Expand All @@ -772,7 +791,7 @@ const ImageDnd = (props: { onAddImages: (blobs: Blob[]) => void }) => {

if (clipboardData.types.includes('Files')) {
ev.preventDefault();
onAddImages(Array.from(clipboardData.files));
onAddMedia(Array.from(clipboardData.files));
}
});

Expand All @@ -788,7 +807,7 @@ const ImageDnd = (props: { onAddImages: (blobs: Blob[]) => void }) => {
tracked = undefined;

if (dataTransfer.types.includes('Files')) {
onAddImages(Array.from(dataTransfer.files));
onAddMedia(Array.from(dataTransfer.files));
}
});

Expand Down Expand Up @@ -852,6 +871,17 @@ const PostEmbeds = (props: { embed: PostEmbed; active: boolean }) => {
{(image) => <ImageEmbed embed={image()} active={props.active} onRemove={removeMedia} />}
</Match>

<Match
when={(() => {
const media = props.embed.media;
if (media && media.type === 'video') {
return media;
}
})()}
>
{(video) => <VideoEmbed embed={video()} active={props.active} onRemove={removeMedia} />}
</Match>

<Match
when={(() => {
const media = props.embed.media;
Expand Down
39 changes: 39 additions & 0 deletions src/components/composer/embeds/video-embed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { convertBlobToUrl } from '~/lib/utils/blob';

import IconButton from '~/components/icon-button';
import CrossLargeOutlinedIcon from '~/components/icons-central/cross-large-outline';
import Keyed from '~/components/keyed';

import type { PostVideoEmbed } from '../lib/state';

export interface VideoEmbedProps {
embed: PostVideoEmbed;
active: boolean;
onRemove: () => void;
}

const VideoEmbed = (props: VideoEmbedProps) => {
return (
<div class="relative">
<Keyed value={props.embed.blob}>
{(blob) => {
const blobUrl = convertBlobToUrl(blob);

return <video src={blobUrl} controls class="aspect-video rounded-md border border-outline" />;
}}
</Keyed>

<div hidden={!props.active} class="absolute right-0 top-0 p-1">
<IconButton
icon={CrossLargeOutlinedIcon}
title="Remove this video"
variant="black"
size="sm"
onClick={props.onRemove}
/>
</div>
</div>
);
};

export default VideoEmbed;
Loading

0 comments on commit a3495f0

Please sign in to comment.