Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

❇️ Language filter on moderation queue #25

Merged
merged 14 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions app/actions/ModActionPanel/QuickAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { SubjectReviewStateBadge } from '@/subject/ReviewStateMarker'
import { getProfileUriForDid } from '@/reports/helpers/subject'
import { Dialog } from '@headlessui/react'
import { SubjectSwitchButton } from '@/common/SubjectSwitchButton'
import { diffTags } from 'components/tags/utils'

const FORM_ID = 'mod-action-panel'
const useBreakpoint = createBreakpoint({ xs: 340, sm: 640 })
Expand Down Expand Up @@ -176,6 +177,7 @@ function Form(
const [modEventType, setModEventType] = useState<string>(
MOD_EVENTS.ACKNOWLEDGE,
)
const isTagEvent = modEventType === MOD_EVENTS.TAG
const isLabelEvent = modEventType === MOD_EVENTS.LABEL
const isMuteEvent = modEventType === MOD_EVENTS.MUTE
const isCommentEvent = modEventType === MOD_EVENTS.COMMENT
Expand Down Expand Up @@ -252,6 +254,15 @@ function Form(
coreEvent.sticky = true
}

if (formData.get('tags')) {
const tags = String(formData.get('tags'))
.split(',')
.map((tag) => tag.trim())
const { add, remove } = diffTags(subjectStatus?.tags || [], tags)
coreEvent.add = add
coreEvent.remove = remove
}

const { subject: subjectInfo, record: recordInfo } =
await createSubjectFromId(subject)

Expand Down Expand Up @@ -528,6 +539,17 @@ function Form(
</LabelList>
</FormLabel>
</div>
{!!subjectStatus?.tags?.length && (
<div className={`mb-3`}>
<FormLabel label="Tags">
<LabelList className="-ml-1">
{subjectStatus.tags.map((tag) => {
return <LabelChip key={tag}>{tag}</LabelChip>
})}
</LabelList>
</FormLabel>
</div>
)}

{/* This is only meant to be switched on in mobile/small screen view */}
{/* The parent component ensures to toggle this based on the screen size */}
Expand Down Expand Up @@ -568,6 +590,19 @@ function Form(
</FormLabel>
)}

{isTagEvent && (
<FormLabel label="Tags" className="mt-2">
<Input
type="text"
id="tags"
name="tags"
className="block w-full"
placeholder="Comma separated tags"
defaultValue={subjectStatus?.tags?.join(',') || ''}
/>
</FormLabel>
)}

<div className="mt-2">
<Textarea
name="comment"
Expand Down
22 changes: 17 additions & 5 deletions app/reports/page-content.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'use client'
import { useContext, useCallback, Suspense, useEffect } from 'react'
import { useContext, useCallback } from 'react'
import {
ReadonlyURLSearchParams,
usePathname,
Expand All @@ -23,6 +23,7 @@ import { ButtonGroup } from '@/common/buttons'
import { useFluentReportSearch } from '@/reports/useFluentReportSearch'
import { SubjectTable } from 'components/subject/table'
import { useTitle } from 'react-use'
import { LanguagePicker } from '@/common/LanguagePicker'

const TABS = [
{
Expand Down Expand Up @@ -156,6 +157,8 @@ export const ReportsPageContent = () => {
const includeMuted = !!params.get('includeMuted')
const appealed = !!params.get('appealed')
const reviewState = params.get('reviewState')
const tags = params.get('tags')
const excludeTags = params.get('excludeTags')
const { sortField, sortDirection } = getSortParams(params)
const { getReportSearchParams } = useFluentReportSearch()
const { lastReviewedBy, subject, reporters } = getReportSearchParams()
Expand Down Expand Up @@ -186,6 +189,8 @@ export const ReportsPageContent = () => {
reporters,
takendown,
appealed,
tags,
excludeTags,
},
],
queryFn: async ({ pageParam }) => {
Expand All @@ -209,6 +214,14 @@ export const ReportsPageContent = () => {
queryParams.appealed = appealed
}

if (tags) {
queryParams.tags = tags.split(',')
}

if (excludeTags) {
queryParams.excludeTags = excludeTags.split(',')
}

// For these fields, we only want to add them to the filter if the values are set, otherwise, defaults will kick in
Object.entries({
sortField,
Expand Down Expand Up @@ -255,10 +268,9 @@ export const ReportsPageContent = () => {
</button>
</div>
</SectionHeader>
<div className="flex mt-2 mb-2 flex-row justify-end px-4 sm:px-6 lg:px-8">
<Suspense fallback={<div></div>}>
<ResolvedFilters />
</Suspense>
<div className="md:flex mt-2 mb-2 flex-row justify-between px-4 sm:px-6 lg:px-8">
<LanguagePicker />
<ResolvedFilters />
</div>
<SubjectTable
subjectStatuses={subjectStatuses}
Expand Down
216 changes: 216 additions & 0 deletions components/common/LanguagePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { LANGUAGES_MAP_CODE2 } from '@/lib/locale/languages'
import { Popover, Transition } from '@headlessui/react'
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/20/solid'
import { useSearchParams, useRouter, usePathname } from 'next/navigation'
import { ActionButton } from './buttons'

// Please make sure that any item added here exists in LANGUAGES_MAP_CODE2 or add it there first
const availableLanguageCodes = [
'en',
'es',
'fr',
'de',
'it',
'ja',
'ko',
'pt',
'ru',
]

const SelectionTitle = ({
includedLanguages,
excludedLanguages,
}: {
includedLanguages: string[]
excludedLanguages: string[]
}) => {
if (includedLanguages.length === 0 && excludedLanguages.length === 0) {
return (
<span className="text-gray-700 dark:text-gray-100">All Languages</span>
)
}

const includedNames = includedLanguages.map(
(lang) => LANGUAGES_MAP_CODE2[lang].name,
)
const excludedNames = excludedLanguages.map(
(lang) => LANGUAGES_MAP_CODE2[lang].name,
)

return (
<>
<span className="text-gray-700 dark:text-gray-100">
{includedNames.join(', ')}
</span>
{includedNames.length > 0 && excludedNames.length > 0 && (
<span className="text-gray-700 dark:text-gray-100 mx-1">|</span>
)}
<span className="text-gray-700 dark:text-gray-100">
{excludedNames.map((name, i) => (
<s key={name}>
{name}
{i < excludedNames.length - 1 && ', '}
</s>
))}
</span>
</>
)
}

// Tags can be any arbitrary string, and lang tags are prefixed with lang:[code2] so we use this to get the lang code from tag string
const getLangFromTag = (tag: string) => tag.split(':')[1]

export const LanguagePicker: React.FC = () => {
const searchParams = useSearchParams()
const router = useRouter()
const pathname = usePathname()

const tagsParam = searchParams.get('tags')
const excludeTagsParam = searchParams.get('excludeTags')
const tags = tagsParam?.split(',') || []
const excludedTags = excludeTagsParam?.split(',') || []
const includedLanguages = tags
.filter((tag) => tag.startsWith('lang:'))
.map(getLangFromTag)
const excludedLanguages = excludedTags
.filter((tag) => tag.startsWith('lang:'))
.map(getLangFromTag)

const toggleLanguage = (section: 'include' | 'exclude', newLang: string) => {
const nextParams = new URLSearchParams(searchParams)
const urlQueryKey = section === 'include' ? 'tags' : 'excludeTags'
const selectedLanguages =
section === 'include' ? includedLanguages : excludedLanguages
const selectedLanguageTags = section === 'include' ? tags : excludedTags

if (selectedLanguages.includes(newLang)) {
const newTags = selectedLanguageTags.filter(
(tag) => `lang:${newLang}` !== tag,
)
if (newTags.length) {
nextParams.set(urlQueryKey, newTags.join(','))
} else {
nextParams.delete(urlQueryKey)
}
} else {
nextParams.set(
urlQueryKey,
[...selectedLanguageTags, `lang:${newLang}`].join(','),
)
}

router.push((pathname ?? '') + '?' + nextParams.toString())
}
const clearLanguages = () => {
const nextParams = new URLSearchParams(searchParams)

nextParams.delete('tags')
nextParams.delete('excludeTags')
router.push((pathname ?? '') + '?' + nextParams.toString())
}

return (
<Popover>
{({ open, close }) => (
<>
<Popover.Button className="text-sm flex flex-row items-center">
<SelectionTitle {...{ includedLanguages, excludedLanguages }} />
<ChevronDownIcon className="dark:text-gray-50 w-4 h-4" />
</Popover.Button>

{/* Use the `Transition` component. */}
<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<Popover.Panel className="absolute left-0 z-10 mt-1 flex w-screen max-w-max -translate-x-1/5 px-4">
<div className="w-fit-content flex-auto rounded bg-white dark:bg-slate-800 p-4 text-sm leading-6 shadow-lg dark:shadow-slate-900 ring-1 ring-gray-900/5">
<div className="flex flex-row gap-4 text-gray-700 dark:text-gray-100">
<LanguageList
disabled={excludedLanguages}
selected={includedLanguages}
header="Include Languages"
onSelect={(lang) => toggleLanguage('include', lang)}
/>
<LanguageList
disabled={includedLanguages}
selected={excludedLanguages}
header="Exclude Languages"
onSelect={(lang) => toggleLanguage('exclude', lang)}
/>
</div>

<p className="py-2 block max-w-xs text-gray-500 dark:text-gray-300 text-xs">
Note: <i>When multiple languages are selected, only subjects that are
tagged with <b>all</b> of those languages will be
included/excluded.</i>
</p>
{(includedLanguages.length > 0 ||
excludedLanguages.length > 0) && (
<div className="flex flex-row mt-2">
<ActionButton
size="xs"
appearance="outlined"
onClick={() => {
clearLanguages()
close()
}}
>
<span className="text-xs">Clear All</span>
</ActionButton>
</div>
)}
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
)
}

const LanguageList = ({
header,
onSelect,
selected = [],
disabled = [],
}: {
selected: string[]
disabled: string[]
header: string
onSelect: (lang: string) => void
}) => {
return (
<div>
<h4 className="text-gray-900 dark:text-gray-200 border-b border-gray-300 mb-2 pb-1">
{header}
</h4>
<div className="flex flex-col items-start">
{availableLanguageCodes.map((code2) => {
const isDisabled = disabled.includes(code2)
return (
<button
className={`w-full flex flex-row items-center justify-between ${
isDisabled
? 'text-gray-400'
: 'text-gray-700 dark:text-gray-100'
}`}
onClick={() => !isDisabled && onSelect(code2)}
key={code2}
>
{LANGUAGES_MAP_CODE2[code2].name}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there anything that guarantees that LANGUAGES_MAP_CODE2[code2] will exist? Maybe we could just document near availableLanguageCodes that it must be a subset of LANGUAGES_MAP_CODE2. Alternately we could mark certain items in LANGUAGES_MAP_CODE2 as "available".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah documenting availableLanguageCodes should do the job because the languages I've added are the ones that bryan suggested and overtime we may add more but even then, the LANGUAGES_MAP_CODE2 is quite comprehensive so I don't see a lang code ever missing there.

{selected.includes(code2) && (
<CheckIcon className="h-4 w-4 text-green-700" />
)}
</button>
)
})}
</div>
</div>
)
}
30 changes: 30 additions & 0 deletions components/mod-event/EventItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,32 @@ const Label = ({
)
}

const Tag = ({
modEvent,
}: {
modEvent: {
event: ComAtprotoAdminDefs.ModEventTag
} & ComAtprotoAdminDefs.ModEventView
}) => {
return (
<div className="shadow dark:shadow-slate-700 bg-white dark:bg-slate-800 rounded-sm p-2">
<p>
<span>
By{' '}
{modEvent.creatorHandle
? `@${modEvent.creatorHandle}`
: modEvent.createdBy}
</span>
</p>{' '}
{modEvent.event.comment ? (
<p className="pb-1">{`${modEvent.event.comment}`}</p>
) : null}
<EventLabels header="Added: " labels={modEvent.event.add} />
<EventLabels header="Removed: " labels={modEvent.event.remove} />
</div>
)
}

const dateFormatter = new Intl.DateTimeFormat('en-US', {
dateStyle: 'medium',
timeStyle: 'short',
Expand Down Expand Up @@ -242,6 +268,10 @@ export const ModEventItem = ({
//@ts-ignore
eventItem = <Label modEvent={modEvent} />
}
if (ComAtprotoAdminDefs.isModEventTag(modEvent.event)) {
//@ts-ignore
eventItem = <Tag modEvent={modEvent} />
}
if (ComAtprotoAdminDefs.isModEventEmail(modEvent.event)) {
//@ts-ignore
eventItem = <Email modEvent={modEvent} />
Expand Down
Loading
Loading