Skip to content

Commit

Permalink
feat: multi squad post approval (#4111)
Browse files Browse the repository at this point in the history
* feat: make squad id optional for sourcePostModerations

* feat: move moderate page to squad folder

* chore: delete old moderate page

* feat: make squad optional in useSourceModerationList

* feat: update squad header bar link

* feat: add squad icon on moderation item

* feat: Update fetching logic

* feat: add pending posts to sidebar

* feat: add pending button to my

* feat: bulk approval for multiple squads

* feat: update squad tabs

* feat: update squadtab props

* fix: pass correct prop

* fix: pass squad correctly

* feat: add SSR

* feat: updated logic to not fetch from sidebar

* fix: remove default disabled in sidebar

* chore: add redirect

---------

Co-authored-by: Lee Hansel Solevilla <13744167+sshanzel@users.noreply.github.com>
  • Loading branch information
AmarTrebinjac and sshanzel authored Feb 6, 2025
1 parent f8ead14 commit f6e6634
Show file tree
Hide file tree
Showing 14 changed files with 275 additions and 170 deletions.
29 changes: 26 additions & 3 deletions packages/shared/src/components/sidebar/sections/NetworkSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import type { ReactElement } from 'react';
import React, { useMemo } from 'react';
import type { SidebarMenuItem } from '../common';
import { ListIcon } from '../common';
import { DefaultSquadIcon, NewSquadIcon, SourceIcon } from '../../icons';
import {
DefaultSquadIcon,
NewSquadIcon,
SourceIcon,
TimerIcon,
} from '../../icons';
import { Section } from '../Section';
import { Origin } from '../../../lib/log';
import { useSquadNavigation } from '../../../hooks';
Expand All @@ -11,13 +16,17 @@ import { SquadImage } from '../../squads/SquadImage';
import { SidebarSettingsFlags } from '../../../graphql/settings';
import { webappUrl } from '../../../lib/constants';
import type { SidebarSectionProps } from './common';
import { useSquadPendingPosts } from '../../../hooks/squads/useSquadPendingPosts';
import { Typography, TypographyColor } from '../../typography/Typography';

export const NetworkSection = ({
isItemsButton,
...defaultRenderSectionProps
}: SidebarSectionProps): ReactElement => {
const { squads } = useAuthContext();
const { count, isModeratorInAnySquad } = useSquadPendingPosts();
const { openNewSquad } = useSquadNavigation();

const menuItems: SidebarMenuItem[] = useMemo(() => {
const squadItems =
squads?.map((squad) => {
Expand All @@ -33,8 +42,22 @@ export const NetworkSection = ({
path: `${webappUrl}squads/${handle}`,
};
}) ?? [];

return [
isModeratorInAnySquad &&
count > 0 && {
icon: () => <ListIcon Icon={() => <TimerIcon />} />,
title: 'Pending Posts',
path: `${webappUrl}squads/moderate`,
rightIcon: () => (
<Typography
color={TypographyColor.Secondary}
bold
className="rounded-6 bg-background-subtle px-1.5"
>
{count}
</Typography>
),
},
{
icon: (active: boolean) => (
<ListIcon Icon={() => <SourceIcon secondary={active} />} />
Expand All @@ -51,7 +74,7 @@ export const NetworkSection = ({
requiresLogin: true,
},
].filter(Boolean);
}, [openNewSquad, squads]);
}, [openNewSquad, squads, count, isModeratorInAnySquad]);

return (
<Section
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/components/squads/SquadHeaderBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ const SquadModerationButton = ({ squad }: SquadBarButtonProps<'a'>) => {
return (
<Button
aria-label={`Check ${count} pending ${postLabel}`}
href={`/squads/${squad.handle}/moderate`}
href={`/squads/moderate?handle=${squad.handle}`}
icon={<TimerIcon aria-hidden role="presentation" />}
size={ButtonSize.Small}
tag="a"
Expand Down
19 changes: 12 additions & 7 deletions packages/shared/src/components/squads/SquadTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import type { ReactElement } from 'react';
import React from 'react';
import { TabContainer, Tab } from '../tabs/TabContainer';
import { webappUrl } from '../../lib/constants';
import type { Squad } from '../../graphql/sources';
import { SourcePermissions } from '../../graphql/sources';
import { verifyPermission } from '../../graphql/squads';
import { useSquad } from '../../hooks';

export enum SquadTab {
Settings = 'Settings',
Expand All @@ -13,15 +13,17 @@ export enum SquadTab {

interface SquadTabsProps {
active: SquadTab;
squad: Squad;
handle: string;
}

export function SquadTabs({ active, squad }: SquadTabsProps): ReactElement {
const { handle, moderationPostCount } = squad;
export function SquadTabs({ active, handle }: SquadTabsProps): ReactElement {
const { squad } = useSquad({
handle,
});
const isModerator = verifyPermission(squad, SourcePermissions.ModeratePost);
const squadLink = `${webappUrl}squads/${handle}`;
const pendingTabLabel = moderationPostCount
? `${SquadTab.PendingPosts} (${moderationPostCount})`
const pendingTabLabel = squad?.moderationPostCount
? `${SquadTab.PendingPosts} (${squad?.moderationPostCount})`
: SquadTab.PendingPosts;

const links = [
Expand All @@ -33,7 +35,10 @@ export function SquadTabs({ active, squad }: SquadTabsProps): ReactElement {
},
]
: []),
{ label: pendingTabLabel, url: `${squadLink}/moderate` },
{
label: pendingTabLabel,
url: `${webappUrl}squads/moderate?handle=${handle}`,
},
];

const controlledActive =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import type { ReactElement } from 'react';
import React from 'react';
import { VIcon } from '../../icons';
import type { Squad } from '../../../graphql/sources';
import { SourcePermissions } from '../../../graphql/sources';
import { SquadEmptyScreen } from './SquadEmptyScreen';
import { ElementPlaceholder } from '../../ElementPlaceholder';
import { verifyPermission } from '../../../graphql/squads';

const ModerationItemSkeleton = () => (
<div className="flex w-full flex-col gap-4 p-6">
Expand Down Expand Up @@ -37,15 +34,13 @@ const ModerationItemSkeleton = () => (
);

export const EmptyModerationList = ({
squad,
isFetched,
isModerator,
}: {
squad: Squad;
isModerator: boolean;
isFetched: boolean;
}): ReactElement => {
const isModerator = verifyPermission(squad, SourcePermissions.ModeratePost);

if (!isFetched || !squad) {
if (!isFetched || !isModerator) {
return (
<div className="flex flex-col gap-4">
<ModerationItemSkeleton />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ReactElement } from 'react';
import React from 'react';
import { useSearchParams } from 'next/navigation';
import {
Typography,
TypographyColor,
Expand All @@ -21,13 +22,16 @@ import { useTruncatedSummary } from '../../../hooks';
import type { SquadModerationItemProps } from './useSourceModerationItem';
import { useSourceModerationItem } from './useSourceModerationItem';
import { SquadModerationItemContextMenu } from './SquadModerationItemContextMenu';
import SourceProfilePicture from '../../profile/SourceProfilePicture';

export function SquadModerationItem(
props: SquadModerationItemProps,
): ReactElement {
const searchParams = useSearchParams();
const handle = searchParams?.get('handle');
const { context, modal, user } = useSourceModerationItem(props);
const { data, squad, onApprove, onReject, isPending } = props;
const { rejectionReason, createdBy, createdAt, image, status } = data;
const { rejectionReason, createdBy, createdAt, image, status, source } = data;

const IconComponent =
status === SourcePostModerationStatus.Rejected ? WarningIcon : TimerIcon;
Expand All @@ -45,6 +49,21 @@ export function SquadModerationItem(
onClick={modal.open}
type="button"
/>
{!handle && (
<div className="flex gap-2">
<SourceProfilePicture
className="pointer-events-none"
source={source}
size={ProfileImageSize.Small}
/>
<Typography
color={TypographyColor.Tertiary}
type={TypographyType.Callout}
>
{source.name}
</Typography>
</div>
)}
<div className="flex flex-row gap-4">
<ProfilePicture
className="pointer-events-none"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,33 @@ import { useSourceModerationList } from '../../../hooks/squads/useSourceModerati
import { useSquadPendingPosts } from '../../../hooks/squads/useSquadPendingPosts';
import { SquadModerationItem } from './SquadModerationItem';
import type { Squad } from '../../../graphql/sources';
import { SourcePermissions } from '../../../graphql/sources';
import InfiniteScrolling from '../../containers/InfiniteScrolling';
import {
SourcePostModerationStatus,
verifyPermission,
} from '../../../graphql/squads';
import { SourcePostModerationStatus } from '../../../graphql/squads';
import { EmptyModerationList } from './SquadModerationEmptyScreen';
import InfiniteScrolling from '../../containers/InfiniteScrolling';

interface SquadModerationListProps {
squad: Squad;
isModerator: boolean;
}

export function SquadModerationList({
squad,
isModerator,
}: SquadModerationListProps): ReactElement {
const moderate = useSourceModerationList({
squad,
});
const isModerator = verifyPermission(squad, SourcePermissions.ModeratePost);

const { data, isFetched, fetchNextPage, hasNextPage, isPending } =
useSquadPendingPosts(
squad?.id,
isModerator
useSquadPendingPosts({
squadId: squad?.id,
status: isModerator
? [SourcePostModerationStatus.Pending]
: [
SourcePostModerationStatus.Pending,
SourcePostModerationStatus.Rejected,
],
);
});

const list = useMemo(
() =>
Expand All @@ -45,7 +43,9 @@ export function SquadModerationList({
);

if (!list.length) {
return <EmptyModerationList squad={squad} isFetched={isFetched} />;
return (
<EmptyModerationList isModerator={isModerator} isFetched={isFetched} />
);
}

return (
Expand All @@ -57,10 +57,7 @@ export function SquadModerationList({
variant={ButtonVariant.Primary}
size={ButtonSize.Small}
onClick={() =>
moderate.onApprove(
list.map((request) => request.id),
squad.id,
)
moderate.onApprove(list.map((request) => request.id))
}
>
Approve all {list.length} posts
Expand All @@ -78,8 +75,8 @@ export function SquadModerationList({
squad={squad}
data={item}
isPending={isPending}
onReject={() => moderate.onReject(item.id, squad.id)}
onApprove={() => moderate.onApprove([item.id], squad.id)}
onReject={() => moderate.onReject(item.id, item.source.id)}
onApprove={() => moderate.onApprove([item.id], item.source.id)}
/>
))}
</InfiniteScrolling>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { MouseEventHandler } from 'react';
import { useId } from 'react';
import { useSearchParams } from 'next/navigation';
import type { SourcePostModeration } from '../../../graphql/squads';
import { verifyPermission } from '../../../graphql/squads';
import useContextMenu from '../../../hooks/useContextMenu';
Expand Down Expand Up @@ -35,16 +36,19 @@ interface UseSourceModerationItem {

export const useSourceModerationItem = ({
data,
squad,
onApprove,
onReject,
}: SquadModerationItemProps): UseSourceModerationItem => {
const squad = data.source as Squad;
const searchParams = useSearchParams();
const contextMenuId = useId();
const { isOpen, onMenuClick } = useContextMenu({ id: contextMenuId });

const { openModal, closeModal } = useLazyModal();

const isModerator = verifyPermission(squad, SourcePermissions.ModeratePost);
const isModerator =
verifyPermission(squad, SourcePermissions.ModeratePost) ||
!searchParams?.get('handle');

const { onDelete } = useSourceModerationList({ squad });

Expand Down
14 changes: 4 additions & 10 deletions packages/shared/src/graphql/squads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,7 @@ const SOURCE_POST_MODERATION_FRAGMENT = gql`

export const SQUAD_PENDING_POSTS_QUERY = gql`
query sourcePostModerations(
$sourceId: ID!
$sourceId: ID
$status: [String]
$first: Int
$after: String
Expand Down Expand Up @@ -662,14 +662,12 @@ export const SQUAD_MODERATE_POST_MUTATION = gql`
mutation ModerateSourcePost(
$postIds: [ID]!
$status: String
$sourceId: ID!
$rejectionReason: String
$moderatorMessage: String
) {
moderateSourcePosts(
postIds: $postIds
status: $status
sourceId: $sourceId
rejectionReason: $rejectionReason
moderatorMessage: $moderatorMessage
) {
Expand Down Expand Up @@ -702,24 +700,21 @@ export interface SquadPostRejectionProps extends SquadPostModerationProps {
note?: string;
}

export const squadApproveMutation = ({
postIds,
sourceId,
}: SquadPostModerationProps): Promise<SourcePostModeration[]> => {
export const squadApproveMutation = (
postIds: string[],
): Promise<SourcePostModeration[]> => {
return gqlClient
.request<{
moderateSourcePosts: SourcePostModeration[];
}>(SQUAD_MODERATE_POST_MUTATION, {
postIds,
sourceId,
status: SourcePostModerationStatus.Approved,
})
.then((res) => res.moderateSourcePosts);
};

export const squadRejectMutation = ({
postIds,
sourceId,
reason,
note,
}: SquadPostRejectionProps): Promise<SourcePostModeration[]> => {
Expand All @@ -728,7 +723,6 @@ export const squadRejectMutation = ({
moderateSourcePosts: SourcePostModeration[];
}>(SQUAD_MODERATE_POST_MUTATION, {
postIds,
sourceId,
status: SourcePostModerationStatus.Rejected,
rejectionReason: reason,
moderatorMessage: note,
Expand Down
Loading

0 comments on commit f6e6634

Please sign in to comment.