blob: 843472cd7ded6afbcb2af0eadbd9b8775dfaa3e0 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { useState, useRef, useEffect, useLayoutEffect } from 'react';
import { Modal, Form, Button, FormCheck } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import ReactDOM from 'react-dom/client';
import { useToast } from '@/hooks';
import { useCaptchaPlugin } from '@/utils/pluginKit';
import type * as Type from '@/common/interface';
import {
reportList,
postReport,
closeQuestion,
putReport,
putFlagReviewAction,
} from '@/services';
interface Params {
isBackend?: boolean;
type: Type.ReportType;
id: string;
title?: string;
action: Type.ReportAction;
source?: string;
content?: string;
reportType?: any;
}
const useReportModal = (callback?: () => void) => {
const { t } = useTranslation('translation', { keyPrefix: 'report_modal' });
const toast = useToast();
const [params, setParams] = useState<Params | null>(null);
const [isInvalid, setInvalidState] = useState(false);
const [reportType, setReportType] = useState({
type: -1,
haveContent: false,
});
const rootRef = useRef<{ root: ReactDOM.Root | null }>({
root: null,
});
const [content, setContent] = useState({
value: '',
isInvalid: false,
errorMsg: '',
});
const [show, setShow] = useState(false);
const [list, setList] = useState<any[]>([]);
const rCaptcha = useCaptchaPlugin('report');
useEffect(() => {
const div = document.createElement('div');
rootRef.current.root = ReactDOM.createRoot(div);
}, []);
const getList = ({ type, action, isBackend, ...otherParams }: Params) => {
// @ts-ignore
reportList({
type,
action,
isBackend,
}).then((res) => {
setList(res);
if (otherParams.reportType) {
const findType = res.find(
(v) => v.reason_type === otherParams.reportType,
);
if (findType) {
setReportType({
type: findType.reason_type,
haveContent: Boolean(findType.content_type),
});
setContent({
value: otherParams.content || '',
isInvalid: false,
errorMsg: '',
});
}
}
setShow(true);
});
};
const asyncCallback = () => {
setTimeout(() => {
callback?.();
});
};
const handleRadio = (val) => {
setInvalidState(false);
setContent({
value:
val.reason_type === params?.reportType ? String(params?.content) : '',
isInvalid: false,
errorMsg: '',
});
setReportType({
type: val.reason_type,
haveContent: Boolean(val.content_type),
});
};
const onClose = () => {
setReportType({
type: -1,
haveContent: false,
});
setContent({
value: '',
isInvalid: false,
errorMsg: '',
});
setShow(false);
};
const checkValidate = () => {
if (reportType.haveContent && !content.value) {
setContent({
value: content.value,
isInvalid: true,
errorMsg: t('remark.empty'),
});
return false;
}
if (reportType.type === 60) {
// a duplicate
let url: URL | undefined;
try {
url = new URL(content.value);
} catch {
setContent({
value: content.value,
isInvalid: true,
errorMsg: t('msg.not_a_url'),
});
}
if (!url) return false;
if (url.origin !== window.location.origin) {
setContent({
value: content.value,
isInvalid: true,
errorMsg: t('msg.url_not_match'),
});
return false;
}
}
return true;
};
const submitReport = (data) => {
const flagReq = {
source: data.type,
report_type: reportType.type,
object_id: data.id,
content: content.value,
captcha_code: undefined,
captcha_id: undefined,
};
rCaptcha?.resolveCaptchaReq(flagReq);
postReport(flagReq)
.then(async () => {
await rCaptcha?.close();
toast.onShow({
msg: t('flag_success', { keyPrefix: 'toast' }),
variant: 'warning',
});
onClose();
asyncCallback();
})
.catch((ex) => {
if (ex.isError) {
rCaptcha?.handleCaptchaError(ex.list);
}
});
};
const handleSubmit = () => {
if (!params) {
return;
}
if (reportType.type === -1) {
setInvalidState(true);
return;
}
if (!checkValidate()) {
return;
}
if (params.type === 'question' && params.action === 'close') {
if (params?.source === 'review') {
putFlagReviewAction({
flag_id: params.id,
operation_type: 'close_post',
close_type: reportType.type,
close_msg: content.value,
}).then(() => {
onClose();
asyncCallback();
});
return;
}
closeQuestion({
id: params.id,
close_type: reportType.type,
close_msg: content.value,
}).then(() => {
onClose();
asyncCallback();
});
return;
}
if (!params.isBackend && params.action === 'flag') {
if (!rCaptcha) {
submitReport(params);
return;
}
rCaptcha.check(() => {
submitReport(params);
});
}
if (params.isBackend && params.action === 'review') {
putReport({
action: params.type,
flagged_content: content.value,
flagged_type: reportType.type,
id: params.id,
}).then(() => {
onClose();
asyncCallback();
});
}
};
const onShow = (obj: Params) => {
setParams(obj);
getList(obj);
};
let title = '';
if (typeof params === 'object' && params) {
title = params.title || t(`${params.action}_title`);
if (params.action === 'review') {
title = t(`${params.action}_${params.type}_title`);
}
}
useLayoutEffect(() => {
rootRef.current.root?.render(
<Modal show={show} onHide={onClose}>
<Modal.Header closeButton>
<Modal.Title as="h5">{title}</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form>
{list.map((item) => {
return (
<div key={item?.reason_type}>
<Form.Group
controlId={`report_${item?.reason_type}`}
className={`${
item.have_content && reportType === item.type
? 'mb-2'
: 'mb-3'
}`}>
<FormCheck>
<FormCheck.Input
id={item.reason_type}
type="radio"
checked={reportType.type === item.reason_type}
onChange={() => handleRadio(item)}
isInvalid={isInvalid}
/>
<FormCheck.Label htmlFor={item.reason_type}>
<span className="fw-bold">{item?.name}</span>
<br />
<span className="text-secondary">
{item?.description}
</span>
</FormCheck.Label>
<Form.Control.Feedback type="invalid">
{t('msg.empty')}
</Form.Control.Feedback>
</FormCheck>
</Form.Group>
{reportType.haveContent &&
reportType.type === item.reason_type && (
<Form.Group controlId="content" className="ps-4 mb-3">
<Form.Control
type="text"
as={
item.content_type === 'text' ? 'input' : 'textarea'
}
value={content.value}
isInvalid={content.isInvalid}
placeholder={item.placeholder}
onChange={(e) =>
setContent({
value: e.target.value,
isInvalid: false,
errorMsg: '',
})
}
/>
<Form.Control.Feedback type="invalid">
{content.errorMsg}
</Form.Control.Feedback>
</Form.Group>
)}
</div>
);
})}
</Form>
</Modal.Body>
<Modal.Footer>
<Button variant="link" onClick={() => onClose()}>
{t('btn_cancel')}
</Button>
<Button variant="primary" onClick={handleSubmit}>
{t('btn_submit')}
</Button>
</Modal.Footer>
</Modal>,
);
});
return {
onClose,
onShow,
};
};
export default useReportModal;