mirror of
https://github.com/wassname/talk.git
synced 2026-08-13 12:40:11 +08:00
[CORL-156] Manage user suspension status (#2419)
* wire up suspension modal * show user suspended callout on stream * prevent comment actions for suspended users * set default to 3 hour suspension * add message field to suspension * allow custom message for suspension * show suspend success modal * fix type errors * remove unused code * update styles * fix fixture for streams * add suspension ui tests * fix types * remove warnings? * remove snapshot * fix merge conflicts * allow custom email message when banning users * fix typo * correct message type * use final-form in suspend modal * refactor suspend modal to use final-form * refactor ban modal to use final-form * refactor userStatusChangeContainer to use useCallback * feat: improve translated form * fix: addressed issue caused by i18n refactor * update getMessage to accept arguments, remove format method * translate suspend info * change hour format * make message a mandatory input for suspend and ban user * fix types in user table * Revert "fix types in user table" This reverts commit d396e90b88bb1bd354c5cdbdd72b6d8f1ab72929. * fix types for user table * fix: small review tweaks
This commit is contained in:
committed by
Wyatt Johnson
parent
4e548e8fbf
commit
5df2de6afc
@@ -92,8 +92,13 @@ export class CommentContainer extends Component<Props, State> {
|
||||
this.props.viewer &&
|
||||
this.props.viewer.status.current.includes(GQLUSER_STATUS.BANNED)
|
||||
);
|
||||
const suspended = Boolean(
|
||||
this.props.viewer &&
|
||||
this.props.viewer.status.current.includes(GQLUSER_STATUS.SUSPENDED)
|
||||
);
|
||||
return (
|
||||
!banned &&
|
||||
!suspended &&
|
||||
isMyComment &&
|
||||
isBeforeDate(this.props.comment.editing.editableUntil)
|
||||
);
|
||||
@@ -181,6 +186,10 @@ export class CommentContainer extends Component<Props, State> {
|
||||
this.props.viewer &&
|
||||
this.props.viewer.status.current.includes(GQLUSER_STATUS.BANNED)
|
||||
);
|
||||
const suspended = Boolean(
|
||||
this.props.viewer &&
|
||||
this.props.viewer.status.current.includes(GQLUSER_STATUS.SUSPENDED)
|
||||
);
|
||||
const showCaret =
|
||||
this.props.viewer &&
|
||||
roleIsAtLeast(this.props.viewer.role, GQLUSER_ROLE.MODERATOR);
|
||||
@@ -263,9 +272,9 @@ export class CommentContainer extends Component<Props, State> {
|
||||
comment={comment}
|
||||
settings={settings}
|
||||
viewer={viewer}
|
||||
readOnly={banned}
|
||||
readOnly={banned || suspended}
|
||||
/>
|
||||
{!disableReplies && !banned && (
|
||||
{!disableReplies && !banned && !suspended && (
|
||||
<ReplyButton
|
||||
id={`comments-commentContainer-replyButton-${
|
||||
comment.id
|
||||
@@ -283,7 +292,7 @@ export class CommentContainer extends Component<Props, State> {
|
||||
/>
|
||||
</ButtonsBar>
|
||||
<ButtonsBar>
|
||||
{!banned && (
|
||||
{!banned && !suspended && (
|
||||
<ReportButtonContainer
|
||||
comment={comment}
|
||||
viewer={viewer}
|
||||
|
||||
@@ -32,6 +32,7 @@ import { PostCommentFormContainer } from "./PostCommentForm";
|
||||
import SortMenu from "./SortMenu";
|
||||
import StoryClosedTimeoutContainer from "./StoryClosedTimeout";
|
||||
import styles from "./StreamContainer.css";
|
||||
import { SuspendedInfoContainer } from "./SuspendedInfo/index";
|
||||
|
||||
interface Props {
|
||||
story: StoryData;
|
||||
@@ -75,6 +76,10 @@ export const StreamContainer: FunctionComponent<Props> = props => {
|
||||
const banned = Boolean(
|
||||
props.viewer && props.viewer.status.current.includes(GQLUSER_STATUS.BANNED)
|
||||
);
|
||||
const suspended = Boolean(
|
||||
props.viewer &&
|
||||
props.viewer.status.current.includes(GQLUSER_STATUS.SUSPENDED)
|
||||
);
|
||||
|
||||
const allCommentsCount = props.story.commentCounts.totalVisible;
|
||||
const featuredCommentsCount = props.story.commentCounts.tags.FEATURED;
|
||||
@@ -100,7 +105,7 @@ export const StreamContainer: FunctionComponent<Props> = props => {
|
||||
<HorizontalGutter className={styles.root} size="double">
|
||||
<UserBoxContainer viewer={props.viewer} settings={props.settings} />
|
||||
<CommunityGuidelinesContainer settings={props.settings} />
|
||||
{!banned && (
|
||||
{!banned && !suspended && (
|
||||
<PostCommentFormContainer
|
||||
settings={props.settings}
|
||||
story={props.story}
|
||||
@@ -110,6 +115,12 @@ export const StreamContainer: FunctionComponent<Props> = props => {
|
||||
/>
|
||||
)}
|
||||
{banned && <BannedInfo />}
|
||||
{suspended && (
|
||||
<SuspendedInfoContainer
|
||||
viewer={props.viewer}
|
||||
settings={props.settings}
|
||||
/>
|
||||
)}
|
||||
<HorizontalGutter spacing={4} className={styles.tabBarContainer}>
|
||||
<SortMenu
|
||||
className={styles.sortMenu}
|
||||
@@ -193,6 +204,7 @@ const enhanced = withFragmentContainer<Props>({
|
||||
...CreateCommentReplyMutation_viewer
|
||||
...CreateCommentMutation_viewer
|
||||
...PostCommentFormContainer_viewer
|
||||
...SuspendedInfoContainer_viewer
|
||||
status {
|
||||
current
|
||||
}
|
||||
@@ -206,6 +218,7 @@ const enhanced = withFragmentContainer<Props>({
|
||||
...PostCommentFormContainer_settings
|
||||
...UserBoxContainer_settings
|
||||
...CommunityGuidelinesContainer_settings
|
||||
...SuspendedInfoContainer_settings
|
||||
}
|
||||
`,
|
||||
})(StreamContainer);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, useMemo } from "react";
|
||||
|
||||
import { useCoralContext } from "coral-framework/lib/bootstrap";
|
||||
|
||||
import { CallOut, HorizontalGutter, Typography } from "coral-ui/components";
|
||||
|
||||
interface Props {
|
||||
organization: string;
|
||||
until: string;
|
||||
}
|
||||
|
||||
const SuspendedInfo: FunctionComponent<Props> = ({ until, organization }) => {
|
||||
const { locales } = useCoralContext();
|
||||
const untilDate = useMemo(() => {
|
||||
const formatter = new Intl.DateTimeFormat(locales, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
});
|
||||
return formatter.format(new Date(until));
|
||||
}, [locales, until]);
|
||||
return (
|
||||
<CallOut fullWidth>
|
||||
<HorizontalGutter>
|
||||
<Localized id="suspendInfo-heading">
|
||||
<Typography variant="heading3">
|
||||
Your account has been temporarily suspended from commenting.
|
||||
</Typography>
|
||||
</Localized>
|
||||
<Localized
|
||||
$organization={organization}
|
||||
$until={untilDate}
|
||||
id="suspendInfo-info"
|
||||
>
|
||||
<Typography variant="bodyCopy">
|
||||
In accordance with {organization}'s community guidelines your
|
||||
account has been temporarily suspended. While suspended you will not
|
||||
be able to comment, respect or report comments. Please rejoin the
|
||||
conversation on {untilDate}.
|
||||
</Typography>
|
||||
</Localized>
|
||||
</HorizontalGutter>
|
||||
</CallOut>
|
||||
);
|
||||
};
|
||||
|
||||
export default SuspendedInfo;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { SuspendedInfoContainer_settings as SettingsData } from "coral-stream/__generated__/SuspendedInfoContainer_settings.graphql";
|
||||
import { SuspendedInfoContainer_viewer as ViewerData } from "coral-stream/__generated__/SuspendedInfoContainer_viewer.graphql";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import SuspendedInfo from "./SuspendedInfo";
|
||||
|
||||
interface Props {
|
||||
settings: SettingsData;
|
||||
viewer: ViewerData | null;
|
||||
}
|
||||
|
||||
export const SuspendedInfoContainer: FunctionComponent<Props> = ({
|
||||
settings,
|
||||
viewer,
|
||||
}) => {
|
||||
if (!viewer) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<SuspendedInfo
|
||||
until={viewer.status.suspension.until}
|
||||
organization={settings.organization.name}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
viewer: graphql`
|
||||
fragment SuspendedInfoContainer_viewer on User {
|
||||
status {
|
||||
suspension {
|
||||
until
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment SuspendedInfoContainer_settings on Settings {
|
||||
organization {
|
||||
name
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(SuspendedInfoContainer);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
default,
|
||||
default as SuspendedInfoContainer,
|
||||
} from "./SuspendedInfoContainer";
|
||||
@@ -0,0 +1,105 @@
|
||||
import timekeeper from "timekeeper";
|
||||
|
||||
import { pureMerge } from "coral-common/utils";
|
||||
import { GQLResolver, GQLUSER_STATUS } from "coral-framework/schema";
|
||||
import {
|
||||
createResolversStub,
|
||||
CreateTestRendererParams,
|
||||
waitForElement,
|
||||
within,
|
||||
} from "coral-framework/testHelpers";
|
||||
|
||||
import { comments, settings, stories } from "../../fixtures";
|
||||
import create from "./create";
|
||||
|
||||
const story = stories[0];
|
||||
const firstComment = story.comments.edges[0].node;
|
||||
const viewer = firstComment.author!;
|
||||
|
||||
async function createTestRenderer(
|
||||
params: CreateTestRendererParams<GQLResolver> = {}
|
||||
) {
|
||||
const { testRenderer, context } = create({
|
||||
...params,
|
||||
resolvers: pureMerge(
|
||||
createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
settings: () => settings,
|
||||
viewer: () =>
|
||||
pureMerge<typeof viewer>(viewer, {
|
||||
status: {
|
||||
current: [GQLUSER_STATUS.SUSPENDED],
|
||||
suspension: {
|
||||
until: new Date(Date.now() + 600000).toISOString(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
story: () =>
|
||||
pureMerge<typeof story>(story, {
|
||||
comments: {
|
||||
edges: [
|
||||
...story.comments.edges,
|
||||
{
|
||||
node: pureMerge<typeof comments[2]>(comments[2], {
|
||||
actionCounts: { reaction: { total: 1 } },
|
||||
}),
|
||||
cursor: comments[2].createdAt,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
params.resolvers
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue(story.id, "storyID");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const tabPane = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("current-tab-pane")
|
||||
);
|
||||
|
||||
return {
|
||||
testRenderer,
|
||||
context,
|
||||
tabPane,
|
||||
};
|
||||
}
|
||||
|
||||
afterAll(() => {
|
||||
timekeeper.reset();
|
||||
});
|
||||
|
||||
it("disables comment stream", async () => {
|
||||
timekeeper.freeze(firstComment.createdAt);
|
||||
const { testRenderer, tabPane } = await createTestRenderer();
|
||||
await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("comments-allComments-log")
|
||||
);
|
||||
within(tabPane).getAllByText(
|
||||
"Your account has been temporarily suspended from commenting.",
|
||||
{
|
||||
exact: false,
|
||||
}
|
||||
);
|
||||
within(tabPane).getAllByText("In accordance with Acme Co", {
|
||||
exact: false,
|
||||
});
|
||||
expect(
|
||||
within(tabPane).queryByText("Reply", { selector: "button" })
|
||||
).toBeNull();
|
||||
expect(
|
||||
within(tabPane).queryByText("Report", { selector: "button" })
|
||||
).toBeNull();
|
||||
expect(
|
||||
within(tabPane).queryByText("Edit", { selector: "button" })
|
||||
).toBeNull();
|
||||
expect(
|
||||
within(tabPane).getByText("Respect", { selector: "button" }).props.disabled
|
||||
).toBe(true);
|
||||
});
|
||||
@@ -40,6 +40,11 @@ export const settings = createFixture<GQLSettings>({
|
||||
message: "Story is closed",
|
||||
timeout: undefined,
|
||||
},
|
||||
organization: {
|
||||
name: "Acme Co",
|
||||
contactEmail: "acme@acme.co",
|
||||
url: "https://acme.co",
|
||||
},
|
||||
auth: {
|
||||
integrations: {
|
||||
facebook: {
|
||||
@@ -110,6 +115,9 @@ export const baseUser = createFixture<GQLUser>({
|
||||
createdAt: "2018-02-06T18:24:00.000Z",
|
||||
status: {
|
||||
current: [GQLUSER_STATUS.ACTIVE],
|
||||
suspension: {
|
||||
active: false,
|
||||
},
|
||||
},
|
||||
ignoredUsers: [],
|
||||
comments: {
|
||||
|
||||
Reference in New Issue
Block a user