Skip to content

Commit

Permalink
💄 Hide blob list by default and add image viewer (#252)
Browse files Browse the repository at this point in the history
* ✨ Allow reverseTakedown from workspace

* 💄 Adjust z-index in various places to fix content overlapping

* 💄 Hide blob list by default and add image viewer

* ♻️ Refactor per review feedback
  • Loading branch information
foysalit authored Dec 5, 2024
1 parent bdc3468 commit c3cf1b2
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 39 deletions.
74 changes: 65 additions & 9 deletions app/actions/ModActionPanel/BlobList.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<fieldset className="space-y-5 min-w-0">
{!blobs.length && <div className="text-sm text-gray-400">None found</div>}
{blobs.map((blob) => {
{blobs.length > 0 ? (
<BlobListLightbox
blobs={blobs}
authorDid={props.authorDid}
slideIndex={lightboxImageIndex}
onClose={() => setLightboxImageIndex(-1)}
/>
) : (
<div className="text-sm text-gray-400">No blobs found</div>
)}
{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 (
<div key={blob.cid} className="relative flex items-start">
<div className="flex h-5 items-center ml-1">
Expand Down Expand Up @@ -58,6 +69,14 @@ export function BlobList(props: {
{blob.details.height}x{blob.details.width}px
</Chip>
)}
<button
onClick={(e) => {
e.preventDefault()
setLightboxImageIndex(i)
}}
>
<Chip>View</Chip>
</button>
</p>
</div>
</div>
Expand All @@ -67,6 +86,43 @@ export function BlobList(props: {
)
}

export const BlobListFormField = ({
blobs,
authorDid,
...rest
}: {
authorDid: string
blobs: ToolsOzoneModerationDefs.BlobView[]
} & Omit<PropsOf<typeof FormLabel>, 'label'>) => {
const [showBlobList, setShowBlobList] = useState(false)

return (
<FormLabel
{...rest}
label={
<button
type="button"
className="flex flex-row items-center"
onClick={() => setShowBlobList(!showBlobList)}
>
{pluralize(blobs.length, 'Blob', {
includeCount: false,
})}
{!showBlobList ? (
<ChevronDownIcon className="w-4 h-4 ml-1 text-white" />
) : (
<ChevronUpIcon className="w-4 h-4 ml-1 text-white" />
)}
</button>
}
>
{showBlobList && (
<BlobList blobs={blobs} authorDid={authorDid} name="subjectBlobCids" />
)}
</FormLabel>
)
}

function Chip(props: ComponentProps<'span'>) {
const { className = '', ...others } = props
return (
Expand Down
21 changes: 9 additions & 12 deletions app/actions/ModActionPanel/QuickAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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'
Expand Down Expand Up @@ -572,16 +574,11 @@ function Form(
)}

{record?.blobs && (
<FormLabel
label="Blobs"
className={`mb-3 ${subjectStatus ? 'opacity-75' : ''}`}
>
<BlobList
blobs={record.blobs}
name="subjectBlobCids"
disabled={false}
/>
</FormLabel>
<BlobListFormField
blobs={record.blobs}
authorDid={record.repo.did}
className="mb-3"
/>
)}
{isSubjectDid && canManageChat && (
<div className="mb-3">
Expand Down
81 changes: 81 additions & 0 deletions components/common/BlobListLightbox.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Lightbox
open={slideIndex >= 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)
},
}}
/>
)
}
9 changes: 5 additions & 4 deletions components/common/posts/ImageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand All @@ -33,15 +33,16 @@ export const ImageList = ({
return (
<>
<Lightbox
open={isImageViewerOpen}
open={lightboxImageIndex >= 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
Expand All @@ -56,7 +57,7 @@ export const ImageList = ({
/>
{images.map((image, i) => (
<figure key={image.thumb}>
<button type="button" onClick={() => setIsImageViewerOpen(true)}>
<button type="button" onClick={() => setLightboxImageIndex(i)}>
<img className={imageClassName} src={image.thumb} alt={image.alt} />
</button>
<figcaption>
Expand Down
1 change: 0 additions & 1 deletion components/common/posts/PostsFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
Expand Down
35 changes: 28 additions & 7 deletions components/repositories/BlobsTable.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="px-4 sm:px-6 lg:px-8">
<div className="-mx-4 mt-8 overflow-hidden border border-gray-300 sm:-mx-6 md:mx-0 md:rounded-lg">
<BlobListLightbox
blobs={blobs}
authorDid={authorDid}
slideIndex={lightboxImageIndex}
onClose={() => setLightboxImageIndex(-1)}
/>
<table className="min-w-full divide-y divide-gray-300">
<thead className="bg-white dark:bg-slate-800">
<BlobRowHead />
</thead>
<tbody className="divide-y divide-gray-200 bg-white dark:bg-slate-800">
{blobs.map((blob) => (
<BlobRow key={blob.cid} blob={blob} />
{blobs.map((blob, i) => (
<BlobRow
onView={() => setLightboxImageIndex(i)}
key={blob.cid}
blob={blob}
/>
))}
</tbody>
</table>
Expand All @@ -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 ?? {}

Expand All @@ -45,6 +63,9 @@ function BlobRow(props: { blob: ToolsOzoneModerationDefs.BlobView }) {
{blob.details.height}x{blob.details.width}px
</Chip>
)}
<button type="button" onClick={onView}>
<Chip>View</Chip>
</button>
</td>
<td className="px-3 py-4 text-sm text-gray-500 dark:text-gray-50">
<span title={createdAt.toLocaleString()}>
Expand Down
9 changes: 4 additions & 5 deletions components/repositories/RecordView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,10 @@ export function RecordView({
<Thread thread={thread.thread} />
)}
{currentView === Views.Blobs && (
<Blobs blobs={record.blobs} />
<BlobsTable
authorDid={record.repo.did}
blobs={record.blobs}
/>
)}
{currentView === Views.ModEvents && (
<div className="flex flex-col mx-auto mt-6 max-w-5xl px-4 sm:px-6 lg:px-8 text-gray-500 dark:text-gray-50 text-sm">
Expand Down Expand Up @@ -322,7 +325,3 @@ function Details({ record }: { record: GetRecord.OutputSchema }) {
</div>
)
}

function Blobs({ blobs }: { blobs: ToolsOzoneModerationDefs.BlobView[] }) {
return <BlobsTable blobs={blobs} />
}
2 changes: 1 addition & 1 deletion lib/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,4 @@ export function simpleHash(str: string) {
hash = ((hash << 5) - hash + chr) | 0
}
return hash
}
}

0 comments on commit c3cf1b2

Please sign in to comment.