Skip to content

Commit

Permalink
✨ Optionally ack all subjects of an account when acknowledgi… (#254)
Browse files Browse the repository at this point in the history
* ✨ Optionally ack all subjects of an account when acknowledging reports on account

* 🐛 Show subject status for records in event stream

* ✨ Make repositories page work more like the queue page

* ⬆️ Update @atproto/api version

* ⬆️ Update @atproto/ozone version
  • Loading branch information
foysalit authored Dec 5, 2024
1 parent a646d5a commit 7aab439
Show file tree
Hide file tree
Showing 10 changed files with 125 additions and 71 deletions.
5 changes: 3 additions & 2 deletions app/actions/ModActionPanel/QuickAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ function Form(
const isMuteReporterEvent = modEventType === MOD_EVENTS.MUTE_REPORTER
const isCommentEvent = modEventType === MOD_EVENTS.COMMENT
const isTakedownEvent = modEventType === MOD_EVENTS.TAKEDOWN
const isAckEvent = modEventType === MOD_EVENTS.ACKNOWLEDGE
const shouldShowDurationInHoursField =
isTakedownEvent || isMuteEvent || isMuteReporterEvent
const canManageChat = usePermission('canManageChat')
Expand Down Expand Up @@ -264,7 +265,7 @@ function Form(
}

if (
modEventType === MOD_EVENTS.TAKEDOWN &&
(isTakedownEvent || isAckEvent) &&
formData.get('acknowledgeAccountSubjects')
) {
coreEvent.acknowledgeAccountSubjects = true
Expand Down Expand Up @@ -737,7 +738,7 @@ function Form(
/>
)}

{isTakedownEvent && isSubjectDid && (
{(isTakedownEvent || isAckEvent) && isSubjectDid && (
<Checkbox
value="true"
id="acknowledgeAccountSubjects"
Expand Down
77 changes: 57 additions & 20 deletions app/repositories/page-content.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { SectionHeader } from '../../components/SectionHeader'
import { RepositoriesTable } from '@/repositories/RepositoriesTable'
import { useSearchParams } from 'next/navigation'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { useInfiniteQuery } from '@tanstack/react-query'
import { useTitle } from 'react-use'
import {
Agent,
ToolsOzoneModerationDefs,
ComAtprotoAdminSearchAccounts,
ToolsOzoneModerationEmitEvent,
} from '@atproto/api'
import { useLabelerAgent } from '@/shell/ConfigurationContext'
import { ActionButton } from '@/common/buttons'
Expand All @@ -16,6 +17,9 @@ import { toast } from 'react-toastify'
import { ConfirmationModal } from '@/common/modals/confirmation'
import { WorkspacePanel } from '@/workspace/Panel'
import { useWorkspaceOpener } from '@/common/useWorkspaceOpener'
import { chunkArray } from '@/lib/util'
import { ModActionPanelQuick } from 'app/actions/ModActionPanel/QuickAction'
import { useEmitEvent } from '@/mod-event/helpers/emitEvent'

const isEmailSearch = (q: string) => q.startsWith('email:')
const isSignatureSearch = (q: string) => q.startsWith('sig:')
Expand Down Expand Up @@ -91,17 +95,17 @@ const getRepos =
})

if (!excludeRepo) {
await Promise.allSettled(
data.accounts.map(async (account) => {
const { data } = await labelerAgent.tools.ozone.moderation.getRepo(
{
did: account.did,
},
options,
)
repos[account.did] = { ...repos[account.did], ...data }
}),
)
for (const accounts of chunkArray(data.accounts, 100)) {
const { data } = await labelerAgent.tools.ozone.moderation.getRepos(
{ dids: accounts.map(({ did }) => did) },
options,
)
for (const repo of data.repos) {
if (ToolsOzoneModerationDefs.isRepoViewDetail(repo)) {
repos[repo.did] = { ...repos[repo.did], ...repo }
}
}
}
}

return { repos: Object.values(repos), cursor: data.cursor }
Expand All @@ -115,12 +119,13 @@ function useSearchResultsQuery(q: string) {
const labelerAgent = useLabelerAgent()
const getRepoPage = getRepos({ q, labelerAgent })

const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ['repositories', { q }],
queryFn: getRepoPage,
refetchOnWindowFocus: false,
getNextPageParam: (lastPage) => lastPage.cursor,
})
const { data, fetchNextPage, hasNextPage, isLoading, refetch } =
useInfiniteQuery({
queryKey: ['repositories', { q }],
queryFn: getRepoPage,
refetchOnWindowFocus: false,
getNextPageParam: (lastPage) => lastPage.cursor,
})
const repos = data?.pages.flatMap((page) => page.repos) ?? []

const confirmAddToWorkspace = async () => {
Expand Down Expand Up @@ -176,6 +181,8 @@ function useSearchResultsQuery(q: string) {
repos,
fetchNextPage,
hasNextPage,
isLoading,
refetch,
confirmAddToWorkspace,
isConfirmationOpen,
setIsConfirmationOpen,
Expand All @@ -185,13 +192,28 @@ function useSearchResultsQuery(q: string) {
}

export default function RepositoriesListPage() {
const emitEvent = useEmitEvent()
const { toggleWorkspacePanel, isWorkspaceOpen } = useWorkspaceOpener()
const params = useSearchParams()
const q = params.get('term') ?? ''
const searchParams = useSearchParams()
const q = searchParams.get('term') ?? ''
const router = useRouter()
const pathname = usePathname()
const quickOpenParam = searchParams.get('quickOpen') ?? ''
const setQuickActionPanelSubject = (subject: string) => {
const newParams = new URLSearchParams(document.location.search)
if (!subject) {
newParams.delete('quickOpen')
} else {
newParams.set('quickOpen', subject)
}
router.push((pathname ?? '') + '?' + newParams.toString())
}
const {
repos,
refetch,
fetchNextPage,
hasNextPage,
isLoading,
setIsConfirmationOpen,
isAdding,
setIsAdding,
Expand Down Expand Up @@ -264,12 +286,27 @@ export default function RepositoriesListPage() {
showEmail={isEmailSearch(q) || isSignatureSearch(q)}
onLoadMore={fetchNextPage}
showLoadMore={!!hasNextPage}
isLoading={isLoading}
showEmptySearch={!q?.length && !repos.length}
/>
<WorkspacePanel
open={isWorkspaceOpen}
onClose={() => toggleWorkspacePanel()}
/>
<ModActionPanelQuick
open={!!quickOpenParam}
onClose={() => setQuickActionPanelSubject('')}
setSubject={setQuickActionPanelSubject}
subject={quickOpenParam} // select first subject if there are multiple
subjectOptions={
repos.length ? repos.map((repo) => repo.did) : [quickOpenParam]
}
isInitialLoading={isLoading}
onSubmit={async (vals: ToolsOzoneModerationEmitEvent.InputSchema) => {
await emitEvent(vals)
refetch()
}}
/>
</>
)
}
12 changes: 7 additions & 5 deletions components/mod-event/ItemTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ export const ItemTitle = ({
eventColor = 'text-blue-400'
eventTitle = 'Email sent'
}
const subjectStatus = modEvent.repo
? modEvent.repo.moderation.subjectStatus
: modEvent.record
? modEvent.record.moderation.subjectStatus
: undefined

return (
<div className="text-gray-500 dark:text-gray-50 flex flex-row justify-between">
Expand All @@ -119,11 +124,8 @@ export const ItemTitle = ({
hideActor={!showContentAuthor}
subjectRepoHandle={modEvent.subjectHandle}
/>
{modEvent.repo?.moderation.subjectStatus && (
<ReviewStateIcon
size="sm"
subjectStatus={modEvent.repo.moderation.subjectStatus}
/>
{subjectStatus && (
<ReviewStateIcon size="sm" subjectStatus={subjectStatus} />
)}
</div>
)}
Expand Down
63 changes: 37 additions & 26 deletions components/repositories/RepositoriesTable.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
import Link from 'next/link'
import { UserGroupIcon } from '@heroicons/react/20/solid'
import { formatDistanceToNow } from 'date-fns'
import { AppBskyActorProfile } from '@atproto/api'
import { Repo } from '@/lib/types'
import { LoadMoreButton } from '../common/LoadMoreButton'
import { ReviewStateIcon } from '@/subject/ReviewStateMarker'
import { SubjectOverview } from '@/reports/SubjectOverview'
import { Loading } from '@/common/Loader'

export function RepositoriesTable(props: {
repos: Repo[]
showLoadMore: boolean
showEmail: boolean
isLoading: boolean
showEmptySearch: boolean
onLoadMore: () => void
}) {
const { repos, showEmail, showLoadMore, onLoadMore, showEmptySearch } = props
const {
repos,
showEmail,
showLoadMore,
onLoadMore,
showEmptySearch,
isLoading,
} = props
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">
Expand All @@ -30,12 +39,18 @@ export function RepositoriesTable(props: {
<tr>
<td colSpan={showEmail ? 5 : 4}>
<div className="flex flex-col items-center py-10">
<UserGroupIcon className="h-10 w-10" />
<p className="text-gray-500 dark:text-gray-50 text-base">
{showEmptySearch
? `Please insert a full or partial handle in the search box above to see matching repositories`
: `No repositories found!`}
</p>
{isLoading ? (
<Loading />
) : (
<>
<UserGroupIcon className="h-10 w-10 dark:text-gray-200" />
<p className="text-gray-500 dark:text-gray-50 text-base">
{showEmptySearch
? `Please insert a full or partial handle in the search box above to see matching repositories`
: `No repositories found!`}
</p>
</>
)}
</div>
</td>
</tr>
Expand All @@ -60,13 +75,20 @@ function RepoRow(props: { repo: Repo; showEmail: boolean }) {
const { subjectStatus } = repo.moderation
return (
<tr {...others}>
<td className="w-full max-w-0 py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:w-auto sm:max-w-none sm:pl-6">
<Link
href={`/repositories/${repo.did}`}
className="text-indigo-600 hover:text-indigo-900 dark:text-teal-400 dark:hover:text-teal-600"
>
{repo.handle}
</Link>
<td className="w-full max-w-0 py-4 pl-4 pr-3 text-sm font-medium text-gray-900 dark:text-gray-200 sm:w-auto sm:max-w-none sm:pl-6">
<div className="flex flex-row items-center pb-1">
<SubjectOverview
subject={{ did: repo.did }}
subjectRepoHandle={repo.handle}
withTruncation={false}
/>
{subjectStatus && (
<ReviewStateIcon
subjectStatus={subjectStatus}
className="ml-1 h-5 w-5 inline-block align-bottom"
/>
)}
</div>
<dl className="font-normal lg:hidden">
<dt className="sr-only">Name</dt>
<dd className="mt-1 truncate text-gray-700 dark:text-gray-100">
Expand All @@ -88,14 +110,6 @@ function RepoRow(props: { repo: Repo; showEmail: boolean }) {
{formatDistanceToNow(indexedAt, { addSuffix: true })}
</span>
</td>
<td className="py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
{subjectStatus && (
<ReviewStateIcon
subjectStatus={subjectStatus}
className="h-5 w-5 inline-block align-bottom"
/>
)}
</td>
</tr>
)
}
Expand Down Expand Up @@ -129,9 +143,6 @@ function RepoRowHead({ showEmail = false }) {
>
Indexed
</th>
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span className="sr-only">Moderation</span>
</th>
</tr>
)
}
3 changes: 2 additions & 1 deletion components/workspace/Panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,8 @@ export function WorkspacePanel(props: PropsOf<typeof ActionPanel>) {
}

if (
coreEvent.$type === MOD_EVENTS.TAKEDOWN &&
(coreEvent.$type === MOD_EVENTS.TAKEDOWN ||
coreEvent.$type === MOD_EVENTS.ACKNOWLEDGE) &&
formData.get('acknowledgeAccountSubjects')
) {
coreEvent.acknowledgeAccountSubjects = true
Expand Down
6 changes: 4 additions & 2 deletions components/workspace/PanelActionForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const WorkspacePanelActionForm = ({
setModEventType: (action: string) => void
onCancel: () => void
}) => {
const isAckEvent = modEventType === MOD_EVENTS.ACKNOWLEDGE
const isTakedownEvent = modEventType === MOD_EVENTS.TAKEDOWN
const isCommentEvent = modEventType === MOD_EVENTS.COMMENT
const isMuteEvent = modEventType === MOD_EVENTS.MUTE
Expand Down Expand Up @@ -105,7 +106,7 @@ export const WorkspacePanelActionForm = ({
label="Update the subject's persistent note with this comment"
/>
)}
{isTakedownEvent && (
{(isTakedownEvent || isAckEvent) && (
<Checkbox
value="true"
id="acknowledgeAccountSubjects"
Expand All @@ -115,7 +116,8 @@ export const WorkspacePanelActionForm = ({
label={
<span className="leading-4">
Acknowledge all open/escalated/appealed reports on subjects
created by accounts that you are taking down.
created by accounts that you are{' '}
{isAckEvent ? 'acknowledging' : 'taking down'}.
</span>
}
/>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"e2e:run": "$(yarn bin)/cypress run --browser chrome"
},
"dependencies": {
"@atproto/api": "^0.13.18",
"@atproto/api": "^0.13.19",
"@atproto/oauth-client-browser": "^0.2.0",
"@atproto/oauth-types": "^0.1.4",
"@atproto/xrpc": "^0.6.1",
Expand Down
2 changes: 1 addition & 1 deletion service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"description": "Ozone service entrypoint",
"main": "index.js",
"dependencies": {
"@atproto/ozone": "0.1.57",
"@atproto/ozone": "0.1.58",
"next": "14.2.5"
}
}
18 changes: 9 additions & 9 deletions service/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
# yarn lockfile v1


"@atproto/api@^0.13.18":
version "0.13.18"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.18.tgz#cc537cc3b4c8d03f258a373f4d893fea11a77cdd"
integrity sha512-rrl5HhzGYWZ7fiC965TPBUOVItq9M4dxMb6qz8IvAVQliSkrJrKc7UD0QWL89QiiXaOBuX8w+4i5r4wrfBGddg==
"@atproto/api@^0.13.19":
version "0.13.19"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.19.tgz#a3b47847bfe00d3f83da2bd3f6dc5566f43887b0"
integrity sha512-rLWQBZaOIk3ds1Fx9CwrdyX3X2GbdSEvVJ9mdSPNX40joiEaE1ljGMOcziFipbvZacXynozE4E0Sb1CgOhzfmA==
dependencies:
"@atproto/common-web" "^0.3.1"
"@atproto/lexicon" "^0.4.3"
Expand Down Expand Up @@ -88,12 +88,12 @@
multiformats "^9.9.0"
zod "^3.23.8"

"@atproto/ozone@0.1.57":
version "0.1.57"
resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.57.tgz#141d66b213710575c7859d691586fd44c731f7ca"
integrity sha512-P2YKeRFPbxKc2e2yftUoMTTcWYuFV0qU1/Nkd4GxuHnBnDJcbtMPglXd7kyLf0p8plCCFau/wZ8QdY8KSDLM9Q==
"@atproto/ozone@0.1.58":
version "0.1.58"
resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.58.tgz#578d1cd2d0ce53648c633859f0ea5545971cf583"
integrity sha512-GdW/vZQGsPf3R+p/APPIdxKggfkiHl38+HVfykqxbCTvPKC0jheRXg8I93hsCPNWXg50A2Aei1YEQg15SOCNwQ==
dependencies:
"@atproto/api" "^0.13.18"
"@atproto/api" "^0.13.19"
"@atproto/common" "^0.4.4"
"@atproto/crypto" "^0.4.2"
"@atproto/identity" "^0.4.3"
Expand Down
Loading

0 comments on commit 7aab439

Please sign in to comment.