diff --git a/app/actions/ModActionPanel/BlobList.tsx b/app/actions/ModActionPanel/BlobList.tsx index a2615eb5..dcda1e36 100644 --- a/app/actions/ModActionPanel/BlobList.tsx +++ b/app/actions/ModActionPanel/BlobList.tsx @@ -1,25 +1,36 @@ -import { ComponentProps } from 'react' +import { ComponentProps, useState } from 'react' import { ToolsOzoneModerationDefs } from '@atproto/api' import { ShieldExclamationIcon } from '@heroicons/react/20/solid' -import { formatBytes } from '@/lib/util' +import { formatBytes, pluralize } from '@/lib/util' import { ReviewStateIconLink } from '@/subject/ReviewStateMarker' +import { FormLabel } from '@/common/forms' +import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/solid' +import { BlobListLightbox } from '@/common/BlobListLightbox' +import { PropsOf } from '@/lib/types' export function BlobList(props: { name: string disabled?: boolean + authorDid: string blobs: ToolsOzoneModerationDefs.BlobView[] }) { const { name, disabled, blobs } = props + const [lightboxImageIndex, setLightboxImageIndex] = useState(-1) + return (
- {!blobs.length &&
None found
} - {blobs.map((blob) => { + {blobs.length > 0 ? ( + setLightboxImageIndex(-1)} + /> + ) : ( +
No blobs found
+ )} + {blobs.map((blob, i) => { const { subjectStatus } = blob.moderation ?? {} - const actionColorClasses = !!subjectStatus?.takendown - ? 'text-rose-600 hover:text-rose-700' - : 'text-indigo-600 hover:text-indigo-900' - // TODO: May be add better display text here? this only goes into title so not that big of a deal - const displayActionType = subjectStatus?.takendown ? 'Taken down' : '' return (
@@ -58,6 +69,14 @@ export function BlobList(props: { {blob.details.height}x{blob.details.width}px )} +

@@ -67,6 +86,43 @@ export function BlobList(props: { ) } +export const BlobListFormField = ({ + blobs, + authorDid, + ...rest +}: { + authorDid: string + blobs: ToolsOzoneModerationDefs.BlobView[] +} & Omit, 'label'>) => { + const [showBlobList, setShowBlobList] = useState(false) + + return ( + setShowBlobList(!showBlobList)} + > + {pluralize(blobs.length, 'Blob', { + includeCount: false, + })} + {!showBlobList ? ( + + ) : ( + + )} + + } + > + {showBlobList && ( + + )} + + ) +} + function Chip(props: ComponentProps<'span'>) { const { className = '', ...others } = props return ( diff --git a/app/actions/ModActionPanel/QuickAction.tsx b/app/actions/ModActionPanel/QuickAction.tsx index 3b080e24..0cdca2b2 100644 --- a/app/actions/ModActionPanel/QuickAction.tsx +++ b/app/actions/ModActionPanel/QuickAction.tsx @@ -11,7 +11,7 @@ import { ActionPanel } from '@/common/ActionPanel' import { ButtonPrimary, ButtonSecondary } from '@/common/buttons' import { Checkbox, FormLabel, Input, Textarea } from '@/common/forms' import { PropsOf } from '@/lib/types' -import { BlobList } from './BlobList' +import { BlobListFormField } from './BlobList' import { LabelList, LabelListEmpty, @@ -28,9 +28,11 @@ import { ArrowLeftIcon, ArrowRightIcon, CheckCircleIcon, + ChevronDownIcon, + ChevronUpIcon, } from '@heroicons/react/24/outline' import { LabelSelector } from '@/common/labels/Selector' -import { takesKeyboardEvt } from '@/lib/util' +import { pluralize, takesKeyboardEvt } from '@/lib/util' import { Loading } from '@/common/Loader' import { ActionDurationSelector } from '@/reports/ModerationForm/ActionDurationSelector' import { MOD_EVENTS } from '@/mod-event/constants' @@ -572,16 +574,11 @@ function Form( )} {record?.blobs && ( - - - + )} {isSubjectDid && canManageChat && (
diff --git a/components/common/BlobListLightbox.tsx b/components/common/BlobListLightbox.tsx new file mode 100644 index 00000000..e8868a8a --- /dev/null +++ b/components/common/BlobListLightbox.tsx @@ -0,0 +1,81 @@ +import { useServerConfig } from '@/shell/ConfigurationContext' +import { ToolsOzoneModerationDefs } from '@atproto/api' +import { useCallback } from 'react' +import Lightbox from 'yet-another-react-lightbox' +import Captions from 'yet-another-react-lightbox/plugins/captions' +import Thumbnails from 'yet-another-react-lightbox/plugins/thumbnails' + +export const useBlobUrl = ({ authorDid }: { authorDid: string }) => { + const { appview } = useServerConfig() + const cdnUrl = appview?.endsWith('.bsky.app') + ? 'https://cdn.bsky.app' + : appview + + return useCallback( + ({ + cid, + isAvatar, + size = 'fullsize', + }: { + cid: string + isAvatar?: boolean + size?: 'thumbnail' | 'fullsize' + }) => { + let sourcePath = isAvatar ? 'avatar' : 'feed' + + // avatar_fullsize doesn't exist, instead the default avatar/ serves the full size image + if (!isAvatar && size !== 'fullsize') { + sourcePath += `_${size}` + } + + return `${cdnUrl}/img/feed_${size}/plain/${authorDid}/${cid}@jpeg` + }, + [authorDid, cdnUrl], + ) +} + +export const BlobListLightbox = ({ + authorDid, + blobs, + onClose, + slideIndex, +}: { + authorDid: string + blobs: ToolsOzoneModerationDefs.BlobView[] + onClose: () => void + slideIndex: number +}) => { + const getBlobUrl = useBlobUrl({ authorDid }) + const handleKeyDown = (event) => { + if (event.key === 'Escape') { + event.stopPropagation() + } + } + + return ( + = 0} + plugins={[Thumbnails, Captions]} + carousel={{ finite: true }} + controller={{ closeOnBackdropClick: true }} + close={onClose} + slides={blobs.map((blob) => ({ + src: getBlobUrl({ + cid: blob.cid, + }), + }))} + index={slideIndex} + on={{ + // The lightbox may open from other Dialog/modal components + // in that case, we want to make sure that esc button presses + // only close the lightbox and not the parent Dialog/modal underneath + entered: () => { + document.addEventListener('keydown', handleKeyDown) + }, + exited: () => { + document.removeEventListener('keydown', handleKeyDown) + }, + }} + /> + ) +} diff --git a/components/common/posts/ImageList.tsx b/components/common/posts/ImageList.tsx index ffd8d10c..1370b221 100644 --- a/components/common/posts/ImageList.tsx +++ b/components/common/posts/ImageList.tsx @@ -22,7 +22,7 @@ export const ImageList = ({ imageClassName?: string images: AppBskyEmbedImages.ViewImage[] }) => { - const [isImageViewerOpen, setIsImageViewerOpen] = useState(false) + const [lightboxImageIndex, setLightboxImageIndex] = useState(-1) const handleKeyDown = (event) => { if (event.key === 'Escape') { @@ -33,15 +33,16 @@ export const ImageList = ({ return ( <> = 0} plugins={[Thumbnails, Captions]} carousel={{ finite: true }} controller={{ closeOnBackdropClick: true }} - close={() => setIsImageViewerOpen(false)} + close={() => setLightboxImageIndex(-1)} slides={images.map((img) => ({ src: img.fullsize, description: img.alt, }))} + index={lightboxImageIndex} on={{ // The lightbox may open from other Dialog/modal components // in that case, we want to make sure that esc button presses @@ -56,7 +57,7 @@ export const ImageList = ({ /> {images.map((image, i) => (
-
diff --git a/components/common/posts/PostsFeed.tsx b/components/common/posts/PostsFeed.tsx index e33f7cf7..adec7aa6 100644 --- a/components/common/posts/PostsFeed.tsx +++ b/components/common/posts/PostsFeed.tsx @@ -43,7 +43,6 @@ import { } from '@/workspace/hooks' import { ImageList } from './ImageList' import { useGraphicMediaPreferences } from '@/config/useLocalPreferences' -import { HandThumbUpIcon } from '@heroicons/react/24/solid' const VideoPlayer = dynamic(() => import('@/common/video/player'), { ssr: false, }) diff --git a/components/repositories/BlobsTable.tsx b/components/repositories/BlobsTable.tsx index 4fd1e678..0947a3c6 100644 --- a/components/repositories/BlobsTable.tsx +++ b/components/repositories/BlobsTable.tsx @@ -1,23 +1,38 @@ import { formatDistanceToNow } from 'date-fns' import { ToolsOzoneModerationDefs } from '@atproto/api' -import { ComponentProps } from 'react' +import { ComponentProps, useState } from 'react' import { formatBytes } from '@/lib/util' import { ReviewStateIcon } from '@/subject/ReviewStateMarker' +import { BlobListLightbox } from '@/common/BlobListLightbox' -export function BlobsTable(props: { +export function BlobsTable({ + blobs, + authorDid, +}: { + authorDid: string blobs: ToolsOzoneModerationDefs.BlobView[] }) { - const { blobs } = props + const [lightboxImageIndex, setLightboxImageIndex] = useState(-1) return (
+ setLightboxImageIndex(-1)} + /> - {blobs.map((blob) => ( - + {blobs.map((blob, i) => ( + setLightboxImageIndex(i)} + key={blob.cid} + blob={blob} + /> ))}
@@ -26,8 +41,11 @@ export function BlobsTable(props: { ) } -function BlobRow(props: { blob: ToolsOzoneModerationDefs.BlobView }) { - const { blob, ...others } = props +function BlobRow(props: { + onView: () => void + blob: ToolsOzoneModerationDefs.BlobView +}) { + const { blob, onView, ...others } = props const createdAt = new Date(blob.createdAt) const { subjectStatus } = blob.moderation ?? {} @@ -45,6 +63,9 @@ function BlobRow(props: { blob: ToolsOzoneModerationDefs.BlobView }) { {blob.details.height}x{blob.details.width}px )} + diff --git a/components/repositories/RecordView.tsx b/components/repositories/RecordView.tsx index 51338102..fd6a6ffb 100644 --- a/components/repositories/RecordView.tsx +++ b/components/repositories/RecordView.tsx @@ -184,7 +184,10 @@ export function RecordView({ )} {currentView === Views.Blobs && ( - + )} {currentView === Views.ModEvents && (
@@ -322,7 +325,3 @@ function Details({ record }: { record: GetRecord.OutputSchema }) {
) } - -function Blobs({ blobs }: { blobs: ToolsOzoneModerationDefs.BlobView[] }) { - return -} diff --git a/lib/util.ts b/lib/util.ts index 880f9c50..d5a5d841 100644 --- a/lib/util.ts +++ b/lib/util.ts @@ -180,4 +180,4 @@ export function simpleHash(str: string) { hash = ((hash << 5) - hash + chr) | 0 } return hash -} +} \ No newline at end of file