mirror of
https://github.com/irislib/iris-messenger.git
synced 2024-10-18 14:13:21 +00:00
divide Feed into smaller components
This commit is contained in:
parent
ec229788d8
commit
f9e5c60f5a
@ -1,47 +1,20 @@
|
||||
import { memo, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/20/solid';
|
||||
import { Bars3Icon, Squares2X2Icon } from '@heroicons/react/24/outline';
|
||||
import { Filter } from 'nostr-tools';
|
||||
|
||||
import Image from '@/components/embed/Image';
|
||||
import Video from '@/components/embed/Video';
|
||||
import EventComponent, { EventComponentProps } from '@/components/events/EventComponent';
|
||||
import Modal from '@/components/modal/Modal';
|
||||
import ProxyImg from '@/components/SafeImg';
|
||||
import EventComponent from '@/components/events/EventComponent';
|
||||
import ImageGridItem from '@/components/feed/ImageGridItem';
|
||||
import ImageModal from '@/components/feed/ImageModal';
|
||||
import { DisplayAs, FeedProps, ImageOrVideo } from '@/components/feed/types';
|
||||
import Show from '@/components/helpers/Show';
|
||||
import useSubscribe from '@/hooks/useSubscribe';
|
||||
import { useLocalState } from '@/LocalState';
|
||||
|
||||
const PAGE_SIZE = 6;
|
||||
const LOAD_MORE_MARGIN = '0px 0px 2000px 0px';
|
||||
|
||||
const VideoIcon = (
|
||||
<svg width="18" viewBox="0 0 122.88 111.34" fill="currentColor">
|
||||
<path d="M23.59,0h75.7a23.68,23.68,0,0,1,23.59,23.59V87.75A23.56,23.56,0,0,1,116,104.41l-.22.2a23.53,23.53,0,0,1-16.44,6.73H23.59a23.53,23.53,0,0,1-16.66-6.93l-.2-.22A23.46,23.46,0,0,1,0,87.75V23.59A23.66,23.66,0,0,1,23.59,0ZM54,47.73,79.25,65.36a3.79,3.79,0,0,1,.14,6.3L54.22,89.05a3.75,3.75,0,0,1-2.4.87A3.79,3.79,0,0,1,48,86.13V50.82h0A3.77,3.77,0,0,1,54,47.73ZM7.35,26.47h14L30.41,7.35H23.59A16.29,16.29,0,0,0,7.35,23.59v2.88ZM37.05,7.35,28,26.47H53.36L62.43,7.38v0Zm32,0L59.92,26.47h24.7L93.7,7.35Zm31.32,0L91.26,26.47h24.27V23.59a16.32,16.32,0,0,0-15.2-16.21Zm15.2,26.68H7.35V87.75A16.21,16.21,0,0,0,12,99.05l.17.16A16.19,16.19,0,0,0,23.59,104h75.7a16.21,16.21,0,0,0,11.3-4.6l.16-.18a16.17,16.17,0,0,0,4.78-11.46V34.06Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
type Props = {
|
||||
filterOptions: FilterOption[];
|
||||
showDisplayAs?: boolean;
|
||||
filterFn?: (event: any) => boolean;
|
||||
emptyMessage?: string;
|
||||
};
|
||||
|
||||
type DisplayAs = 'feed' | 'grid';
|
||||
|
||||
type ImageOrVideo = {
|
||||
type: 'image' | 'video';
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type FilterOption = {
|
||||
name: string;
|
||||
filter: Filter;
|
||||
filterFn?: (event: any) => boolean;
|
||||
eventProps?: Partial<EventComponentProps>;
|
||||
};
|
||||
|
||||
const Feed = ({ showDisplayAs, filterOptions, emptyMessage }: Props) => {
|
||||
const Feed = ({ showDisplayAs, filterOptions, emptyMessage }: FeedProps) => {
|
||||
if (!filterOptions || filterOptions.length === 0) {
|
||||
throw new Error('Feed requires at least one filter option');
|
||||
}
|
||||
@ -54,16 +27,12 @@ const Feed = ({ showDisplayAs, filterOptions, emptyMessage }: Props) => {
|
||||
|
||||
const { events: allEvents, loadMore } = useSubscribe({
|
||||
filter: filterOption.filter,
|
||||
// in keyword search, relays should be queried for all events, not just sinceLastOpened
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
sinceLastOpened: false,
|
||||
});
|
||||
|
||||
// deduplicate
|
||||
const events = useMemo(() => {
|
||||
const filtered = allEvents
|
||||
.filter((event) => {
|
||||
const filtered = allEvents.filter((event) => {
|
||||
if (mutedUsers[event.pubkey]) {
|
||||
return false;
|
||||
}
|
||||
@ -71,8 +40,7 @@ const Feed = ({ showDisplayAs, filterOptions, emptyMessage }: Props) => {
|
||||
return filterOption.filterFn(event);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => b.created_at - a.created_at);
|
||||
});
|
||||
return filtered;
|
||||
}, [allEvents, filterOption]);
|
||||
|
||||
@ -129,34 +97,6 @@ const Feed = ({ showDisplayAs, filterOptions, emptyMessage }: Props) => {
|
||||
.slice(0, displayCount);
|
||||
}, [events, displayCount, displayAs]) as ImageOrVideo[];
|
||||
|
||||
const goToPrevImage = () => {
|
||||
if (modalItemIndex === null) return;
|
||||
const prevImageIndex = (modalItemIndex - 1 + imagesAndVideos.length) % imagesAndVideos.length;
|
||||
setModalImageIndex(prevImageIndex);
|
||||
};
|
||||
|
||||
const goToNextImage = () => {
|
||||
if (modalItemIndex === null) return;
|
||||
const nextImageIndex = (modalItemIndex + 1) % imagesAndVideos.length;
|
||||
setModalImageIndex(nextImageIndex);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'ArrowRight') {
|
||||
goToNextImage();
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
goToPrevImage();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [modalItemIndex]);
|
||||
|
||||
const renderFilterOptions = () => {
|
||||
return (
|
||||
<div className="flex mb-4 gap-2 mx-2 md:mx-4">
|
||||
@ -211,94 +151,31 @@ const Feed = ({ showDisplayAs, filterOptions, emptyMessage }: Props) => {
|
||||
const renderGrid = () => {
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-px">
|
||||
{imagesAndVideos.map((item, index) => renderGridItem(item, index))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderGridItem = (item: { url: string; type: 'image' | 'video' }, index: number) => {
|
||||
const url =
|
||||
item.type === 'video' ? `https://imgproxy.iris.to/thumbnail/638/${item.url}` : item.url;
|
||||
return (
|
||||
<div
|
||||
key={`feed${url}${index}`}
|
||||
className="aspect-square cursor-pointer relative bg-neutral-300 hover:opacity-80"
|
||||
ref={index === imagesAndVideos.length - 1 ? lastElementRef : null}
|
||||
onClick={() => {
|
||||
setModalImageIndex(index);
|
||||
}}
|
||||
>
|
||||
<ProxyImg
|
||||
square={true}
|
||||
width={319}
|
||||
src={url}
|
||||
alt=""
|
||||
className="w-full h-full object-cover"
|
||||
{imagesAndVideos.map((item, index) => (
|
||||
<ImageGridItem
|
||||
item={item}
|
||||
index={index}
|
||||
setModalImageIndex={setModalImageIndex}
|
||||
lastElementRef={lastElementRef}
|
||||
imagesAndVideosLength={imagesAndVideos.length}
|
||||
/>
|
||||
{item.type === 'video' && (
|
||||
<div className="absolute top-0 right-0 m-2 shadow-md shadow-gray-500 ">{VideoIcon}</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderImageModal = () => {
|
||||
return modalItemIndex !== null ? (
|
||||
<Modal onClose={() => setModalImageIndex(null)}>
|
||||
<div className="relative w-full h-full flex justify-center">
|
||||
{imagesAndVideos[modalItemIndex].type === 'video' ? (
|
||||
<video
|
||||
className="rounded max-h-[90vh] max-w-[90vw] object-contain"
|
||||
src={imagesAndVideos[modalItemIndex].url}
|
||||
controls
|
||||
muted
|
||||
autoPlay
|
||||
loop
|
||||
poster={`https://imgproxy.iris.to/thumbnail/638/${imagesAndVideos[modalItemIndex].url}`}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
className="rounded max-h-[90vh] max-w-[90vw] object-contain"
|
||||
src={imagesAndVideos[modalItemIndex].url}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center justify-between w-full h-full absolute bottom-0 left-0 right-0">
|
||||
<div
|
||||
className="p-4"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
goToPrevImage();
|
||||
}}
|
||||
>
|
||||
<button className="btn btn-circle btn-sm opacity-25 mr-2 flex-shrink-0">
|
||||
<ChevronLeftIcon width={20} />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="p-4 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
goToNextImage();
|
||||
}}
|
||||
>
|
||||
<button className="btn btn-circle btn-sm opacity-25 ml-2 flex-shrink-0">
|
||||
<ChevronRightIcon width={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{filterOptions.length > 1 && renderFilterOptions()}
|
||||
<Show when={filterOptions.length > 1}>{renderFilterOptions()}</Show>
|
||||
{renderDisplayAsSelector()}
|
||||
{renderImageModal()}
|
||||
{isEmpty && <p>{emptyMessage || 'No Posts'}</p>}
|
||||
<ImageModal
|
||||
setModalImageIndex={setModalImageIndex}
|
||||
modalItemIndex={modalItemIndex}
|
||||
imagesAndVideos={imagesAndVideos}
|
||||
/>
|
||||
<Show when={isEmpty}>
|
||||
<div>{emptyMessage || 'No Posts'}</div>
|
||||
</Show>
|
||||
<div ref={lastElementRef}>
|
||||
{displayAs === 'grid'
|
||||
? renderGrid()
|
||||
|
41
src/js/components/feed/ImageGridItem.tsx
Normal file
41
src/js/components/feed/ImageGridItem.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { ImageOrVideo } from '@/components/feed/types';
|
||||
import SafeImg from '@/components/SafeImg';
|
||||
import Icons from '@/Icons';
|
||||
|
||||
type ImageGridItemProps = {
|
||||
item: ImageOrVideo;
|
||||
index: number;
|
||||
setModalImageIndex: (index: number) => void;
|
||||
imagesAndVideosLength: number;
|
||||
lastElementRef?: React.MutableRefObject<HTMLDivElement | null>;
|
||||
};
|
||||
|
||||
export const ImageGridItem = ({
|
||||
item,
|
||||
index,
|
||||
setModalImageIndex,
|
||||
imagesAndVideosLength,
|
||||
lastElementRef,
|
||||
}: ImageGridItemProps) => {
|
||||
const url =
|
||||
item.type === 'video' ? `https://imgproxy.iris.to/thumbnail/638/${item.url}` : item.url;
|
||||
const isLast = index === imagesAndVideosLength - 1;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`feed${url}${index}`}
|
||||
className="aspect-square cursor-pointer relative bg-neutral-300 hover:opacity-80"
|
||||
ref={isLast ? lastElementRef : null}
|
||||
onClick={() => {
|
||||
setModalImageIndex(index);
|
||||
}}
|
||||
>
|
||||
<SafeImg square={true} width={319} src={url} alt="" className="w-full h-full object-cover" />
|
||||
{item.type === 'video' && (
|
||||
<div className="absolute top-0 right-0 m-2 shadow-md shadow-gray-500 ">{Icons.video}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageGridItem;
|
94
src/js/components/feed/ImageModal.tsx
Normal file
94
src/js/components/feed/ImageModal.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import { useEffect } from 'react';
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/20/solid';
|
||||
|
||||
import Modal from '@/components/modal/Modal';
|
||||
import SafeImg from '@/components/SafeImg';
|
||||
|
||||
type ImageModalProps = {
|
||||
imagesAndVideos: Array<{
|
||||
type: 'image' | 'video';
|
||||
url: string;
|
||||
}>;
|
||||
modalItemIndex: number | null;
|
||||
setModalImageIndex: (index: number | null) => void;
|
||||
};
|
||||
|
||||
const ImageModal = ({ imagesAndVideos, modalItemIndex, setModalImageIndex }: ImageModalProps) => {
|
||||
const goToPrevImage = () => {
|
||||
if (modalItemIndex === null) return;
|
||||
const prevImageIndex = (modalItemIndex - 1 + imagesAndVideos.length) % imagesAndVideos.length;
|
||||
setModalImageIndex(prevImageIndex);
|
||||
};
|
||||
|
||||
const goToNextImage = () => {
|
||||
if (modalItemIndex === null) return;
|
||||
const nextImageIndex = (modalItemIndex + 1) % imagesAndVideos.length;
|
||||
setModalImageIndex(nextImageIndex);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'ArrowRight') {
|
||||
goToNextImage();
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
goToPrevImage();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [modalItemIndex, imagesAndVideos]);
|
||||
|
||||
return modalItemIndex !== null ? (
|
||||
<Modal onClose={() => setModalImageIndex(null)}>
|
||||
<div className="relative w-full h-full flex justify-center">
|
||||
{imagesAndVideos[modalItemIndex].type === 'video' ? (
|
||||
<video
|
||||
className="rounded max-h-[90vh] max-w-[90vw] object-contain"
|
||||
src={imagesAndVideos[modalItemIndex].url}
|
||||
controls
|
||||
muted
|
||||
autoPlay
|
||||
loop
|
||||
poster={`https://imgproxy.iris.to/thumbnail/638/${imagesAndVideos[modalItemIndex].url}`}
|
||||
/>
|
||||
) : (
|
||||
<SafeImg
|
||||
key={imagesAndVideos[modalItemIndex].url}
|
||||
className="rounded max-h-[90vh] max-w-[90vw] object-contain"
|
||||
src={imagesAndVideos[modalItemIndex].url}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center justify-between w-full h-full absolute bottom-0 left-0 right-0">
|
||||
<div
|
||||
className="p-4"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
goToPrevImage();
|
||||
}}
|
||||
>
|
||||
<button className="btn btn-circle btn-sm opacity-25 mr-2 flex-shrink-0">
|
||||
<ChevronLeftIcon width={20} />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="p-4 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
goToNextImage();
|
||||
}}
|
||||
>
|
||||
<button className="btn btn-circle btn-sm opacity-25 ml-2 flex-shrink-0">
|
||||
<ChevronRightIcon width={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default ImageModal;
|
24
src/js/components/feed/types.ts
Normal file
24
src/js/components/feed/types.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Filter } from 'nostr-tools';
|
||||
|
||||
import { EventComponentProps } from '@/components/events/EventComponent';
|
||||
|
||||
export type FeedProps = {
|
||||
filterOptions: FilterOption[];
|
||||
showDisplayAs?: boolean;
|
||||
filterFn?: (event: any) => boolean;
|
||||
emptyMessage?: string;
|
||||
};
|
||||
|
||||
export type DisplayAs = 'feed' | 'grid';
|
||||
|
||||
export type ImageOrVideo = {
|
||||
type: 'image' | 'video';
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type FilterOption = {
|
||||
name: string;
|
||||
filter: Filter;
|
||||
filterFn?: (event: any) => boolean;
|
||||
eventProps?: Partial<EventComponentProps>;
|
||||
};
|
@ -124,7 +124,7 @@ export default class EditProfile extends Component {
|
||||
</p>
|
||||
{val && (
|
||||
<p>
|
||||
<SafeImg src={val} />
|
||||
<SafeImg key={val} src={val} />
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
|
Loading…
Reference in New Issue
Block a user