Skip to content

Commit

Permalink
Video support. (#40)
Browse files Browse the repository at this point in the history
* video now mutes when opening video modal

* username bug fix & images upload by id

* refactor: reuse logic when checking if username exists

* style: minor nit changes spaces and newline

* feat: improve logic when handling uploading media

* feat: improve logic when extracting image and video data

* feat: remove alt check when uploading media

* feat: overall finished pr

* feat: make tweet card non draggable to allow sliding on video progress

* feat: add button on video tweet to open modal on mobile

---------

Co-authored-by: ccrsxx <[email protected]>
  • Loading branch information
Ketchupchh and ccrsxx authored Mar 4, 2024
1 parent c6bf6fb commit 6722c14
Show file tree
Hide file tree
Showing 11 changed files with 197 additions and 91 deletions.
8 changes: 8 additions & 0 deletions src/components/home/update-username.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function UpdateUsername(): JSX.Element {
const [available, setAvailable] = useState(false);
const [loading, setLoading] = useState(false);
const [visited, setVisited] = useState(false);
const [searching, setSearching] = useState(false);
const [inputValue, setInputValue] = useState('');
const [errorMessage, setErrorMessage] = useState('');

Expand All @@ -28,13 +29,17 @@ export function UpdateUsername(): JSX.Element {

useEffect(() => {
const checkAvailability = async (value: string): Promise<void> => {
setSearching(true);

const empty = await checkUsernameAvailability(value);

if (empty) setAvailable(true);
else {
setAvailable(false);
setErrorMessage('This username has been taken. Please choose another.');
}

setSearching(false);
};

if (!visited && inputValue.length > 0) setVisited(true);
Expand Down Expand Up @@ -63,6 +68,8 @@ export function UpdateUsername(): JSX.Element {

if (!available) return;

if (searching) return;

setLoading(true);

await sleep(500);
Expand All @@ -82,6 +89,7 @@ export function UpdateUsername(): JSX.Element {

const cancelUpdateUsername = (): void => {
closeModal();

if (!alreadySet) void updateUsername(user?.id as string);
};

Expand Down
126 changes: 83 additions & 43 deletions src/components/input/image-preview.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import cn from 'clsx';
import { useModal } from '@lib/hooks/useModal';
Expand Down Expand Up @@ -50,6 +50,8 @@ export function ImagePreview({
const [selectedIndex, setSelectedIndex] = useState(0);
const [selectedImage, setSelectedImage] = useState<ImageData | null>(null);

const videoRef = useRef<HTMLVideoElement>(null);

const { open, openModal, closeModal } = useModal();

useEffect(() => {
Expand All @@ -58,7 +60,13 @@ export function ImagePreview({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedIndex]);

const handleSelectedImage = (index: number) => () => {
const handleVideoStop = (): void => {
if (videoRef.current) videoRef.current.pause();
};

const handleSelectedImage = (index: number, isVideo?: boolean) => () => {
if (isVideo) handleVideoStop();

setSelectedIndex(index);
openModal();
};
Expand Down Expand Up @@ -106,52 +114,84 @@ export function ImagePreview({
/>
</Modal>
<AnimatePresence mode='popLayout'>
{imagesPreview.map(({ id, src, alt }, index) => (
<motion.button
type='button'
className={cn(
'accent-tab relative transition-shadow',
isTweet
? postImageBorderRadius[previewCount][index]
: 'rounded-2xl',
{
'col-span-2 row-span-2': previewCount === 1,
'row-span-2':
previewCount === 2 || (index === 0 && previewCount === 3)
}
)}
{...variants}
onClick={preventBubbling(handleSelectedImage(index))}
layout={!isTweet ? true : false}
key={id}
>
<NextImage
className='relative h-full w-full cursor-pointer transition
hover:brightness-75 hover:duration-200'
imgClassName={cn(
{imagesPreview.map(({ id, src, alt }, index) => {
const isVideo = imagesPreview[index].type?.includes('video');

return (
<motion.button
type='button'
className={cn(
'accent-tab group relative transition-shadow',
isTweet
? postImageBorderRadius[previewCount][index]
: 'rounded-2xl'
: 'rounded-2xl',
{
'col-span-2 row-span-2': previewCount === 1,
'row-span-2':
previewCount === 2 || (index === 0 && previewCount === 3)
}
)}
{...variants}
onClick={preventBubbling(handleSelectedImage(index, isVideo))}
layout={!isTweet ? true : false}
key={id}
>
{isVideo ? (
<>
<Button
className='visible absolute top-0 right-0 z-10 -translate-x-1 translate-y-1
bg-light-primary/75 p-1 opacity-0 backdrop-blur-sm transition
hover:bg-image-preview-hover/75 group-hover:opacity-100 xs:invisible'
>
<HeroIcon className='h-5 w-5' iconName='ArrowUpRightIcon' />
</Button>
<video
ref={videoRef}
className={cn(
`relative h-full w-full cursor-pointer transition
hover:brightness-75 hover:duration-200`,
isTweet
? postImageBorderRadius[previewCount][index]
: 'rounded-2xl'
)}
src={src}
controls
muted
/>
</>
) : (
<NextImage
className='relative h-full w-full cursor-pointer transition
hover:brightness-75 hover:duration-200'
imgClassName={cn(
isTweet
? postImageBorderRadius[previewCount][index]
: 'rounded-2xl'
)}
previewCount={previewCount}
layout='fill'
src={src}
alt={alt}
useSkeleton={isTweet}
/>
)}
previewCount={previewCount}
layout='fill'
src={src}
alt={alt}
useSkeleton={isTweet}
/>
{removeImage && (
<Button
className='group absolute top-0 left-0 translate-x-1 translate-y-1
{removeImage && (
<Button
className='group absolute top-0 left-0 translate-x-1 translate-y-1
bg-light-primary/75 p-1 backdrop-blur-sm
hover:bg-image-preview-hover/75'
onClick={preventBubbling(removeImage(id))}
>
<HeroIcon className='h-5 w-5 text-white' iconName='XMarkIcon' />
<ToolTip className='translate-y-2' tip='Remove' />
</Button>
)}
</motion.button>
))}
onClick={preventBubbling(removeImage(id))}
>
<HeroIcon
className='h-5 w-5 text-white'
iconName='XMarkIcon'
/>
<ToolTip className='translate-y-2' tip='Remove' />
</Button>
)}
</motion.button>
);
})}
</AnimatePresence>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/input/input-options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export function InputOptions({
<input
className='hidden'
type='file'
accept='image/*'
accept='image/*,video/*'
onChange={handleImageUpload}
ref={inputFileRef}
multiple
Expand Down
5 changes: 4 additions & 1 deletion src/components/input/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,10 @@ export function Input({

const files = isClipboardEvent ? e.clipboardData.files : e.target.files;

const imagesData = getImagesData(files, previewCount);
const imagesData = getImagesData(files, {
currentFiles: previewCount,
allowUploadingVideos: true
});

if (!imagesData) {
toast.error('Please choose a GIF or photo up to 4');
Expand Down
76 changes: 50 additions & 26 deletions src/components/modal/image-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ export function ImageModal({
const [indexes, setIndexes] = useState<number[]>([]);
const [loading, setLoading] = useState(true);

const { src, alt } = imageData;
const { src, alt, type } = imageData;

const isVideo = type?.includes('video');

const requireArrows = handleNextIndex && previewCount > 1;

Expand All @@ -51,9 +53,14 @@ export function ImageModal({
setIndexes([...indexes, selectedIndex]);
}

const image = new Image();
image.src = src;
image.onload = (): void => setLoading(false);
const media = isVideo ? document.createElement('video') : new Image();

media.src = src;

const handleLoadingCompleted = (): void => setLoading(false);

if (isVideo) media.onloadeddata = handleLoadingCompleted;
else media.onload = handleLoadingCompleted;
}, [...(tweet && previewCount > 1 ? [src] : [])]);

useEffect(() => {
Expand Down Expand Up @@ -103,28 +110,45 @@ export function ImageModal({
</motion.div>
) : (
<motion.div className='relative mx-auto' {...modal} key={src}>
<picture className='group relative flex max-w-3xl'>
<source srcSet={src} type='image/*' />
<img
className='max-h-[75vh] rounded-md object-contain md:max-h-[80vh]'
src={src}
alt={alt}
onClick={preventBubbling()}
/>
<a
className='trim-alt accent-tab absolute bottom-0 right-0 mx-2 mb-2 translate-y-4
rounded-md bg-main-background/40 px-2 py-1 text-sm text-light-primary/80 opacity-0
transition hover:bg-main-accent hover:text-white focus-visible:translate-y-0
focus-visible:bg-main-accent focus-visible:text-white focus-visible:opacity-100
group-hover:translate-y-0 group-hover:opacity-100 dark:text-dark-primary/80'
href={src}
target='_blank'
rel='noreferrer'
onClick={preventBubbling(null, true)}
>
{alt}
</a>
</picture>
{isVideo ? (
<div className='group relative flex max-w-3xl'>
<video
className={cn(
'max-h-[75vh] rounded-md object-contain md:max-h-[80vh]',
loading ? 'hidden' : 'block'
)}
src={src}
autoPlay
controls
onClick={preventBubbling()}
>
<source srcSet={src} type='video/*' />
</video>
</div>
) : (
<picture className='group relative flex max-w-3xl'>
<source srcSet={src} type='image/*' />
<img
className='max-h-[75vh] rounded-md object-contain md:max-h-[80vh]'
src={src}
alt={alt}
onClick={preventBubbling()}
/>
<a
className='trim-alt accent-tab absolute bottom-0 right-0 mx-2 mb-2 translate-y-4
rounded-md bg-main-background/40 px-2 py-1 text-sm text-light-primary/80 opacity-0
transition hover:bg-main-accent hover:text-white focus-visible:translate-y-0
focus-visible:bg-main-accent focus-visible:text-white focus-visible:opacity-100
group-hover:translate-y-0 group-hover:opacity-100 dark:text-dark-primary/80'
href={src}
target='_blank'
rel='noreferrer'
onClick={preventBubbling(null, true)}
>
{alt}
</a>
</picture>
)}
<a
className='custom-underline absolute left-0 -bottom-7 font-medium text-light-primary/80
decoration-transparent underline-offset-2 transition hover:text-light-primary hover:underline
Expand Down
1 change: 1 addition & 0 deletions src/components/tweet/tweet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export function Tweet(tweet: TweetProps): JSX.Element {
? 'mt-0.5 pt-2.5 pb-0'
: 'border-b border-light-border dark:border-dark-border'
)}
draggable={false}
onClick={delayScroll(200)}
>
<div className='grid grid-cols-[auto,1fr] gap-x-3 gap-y-1'>
Expand Down
7 changes: 4 additions & 3 deletions src/lib/context/auth-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
userBookmarksCollection
} from '@lib/firebase/collections';
import { getRandomId, getRandomInt } from '@lib/random';
import { checkUsernameAvailability } from '@lib/firebase/utils';
import type { ReactNode } from 'react';
import type { User as AuthUser } from 'firebase/auth';
import type { WithFieldValue } from 'firebase/firestore';
Expand Down Expand Up @@ -67,11 +68,11 @@ export function AuthContextProvider({

randomUsername = `${normalizeName as string}${randomInt}`;

const randomUserSnapshot = await getDoc(
doc(usersCollection, randomUsername)
const isUsernameAvailable = await checkUsernameAvailability(
randomUsername
);

if (!randomUserSnapshot.exists()) available = true;
if (isUsernameAvailable) available = true;
}

const userData: WithFieldValue<User> = {
Expand Down
15 changes: 5 additions & 10 deletions src/lib/firebase/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,20 +132,15 @@ export async function uploadImages(

const imagesPreview = await Promise.all(
files.map(async (file) => {
let src: string;
const { id, name: alt, type } = file;

const { id, name: alt } = file;
const storageRef = ref(storage, `images/${userId}/${id}`);

const storageRef = ref(storage, `images/${userId}/${alt}`);
await uploadBytesResumable(storageRef, file);

try {
src = await getDownloadURL(storageRef);
} catch {
await uploadBytesResumable(storageRef, file);
src = await getDownloadURL(storageRef);
}
const src = await getDownloadURL(storageRef);

return { id, src, alt };
return { id, src, alt, type };
})
);

Expand Down
1 change: 1 addition & 0 deletions src/lib/types/file.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export type ImageData = {
src: string;
alt: string;
type?: string;
};

export type ImagesPreview = (ImageData & {
Expand Down
Loading

0 comments on commit 6722c14

Please sign in to comment.