detail.jsx (6790B)
1 /* 2 GoToSocial 3 Copyright (C) GoToSocial Authors admin@gotosocial.org 4 SPDX-License-Identifier: AGPL-3.0-or-later 5 6 This program is free software: you can redistribute it and/or modify 7 it under the terms of the GNU Affero General Public License as published by 8 the Free Software Foundation, either version 3 of the License, or 9 (at your option) any later version. 10 11 This program is distributed in the hope that it will be useful, 12 but WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU Affero General Public License for more details. 15 16 You should have received a copy of the GNU Affero General Public License 17 along with this program. If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 "use strict"; 21 22 const React = require("react"); 23 const { useRoute, Redirect } = require("wouter"); 24 25 const query = require("../../lib/query"); 26 27 const FormWithData = require("../../lib/form/form-with-data"); 28 const BackButton = require("../../components/back-button"); 29 30 const { useValue, useTextInput } = require("../../lib/form"); 31 const useFormSubmit = require("../../lib/form/submit"); 32 33 const { TextArea } = require("../../components/form/inputs"); 34 35 const MutationButton = require("../../components/form/mutation-button"); 36 const Username = require("./username"); 37 const { useBaseUrl } = require("../../lib/navigation/util"); 38 39 module.exports = function ReportDetail({ }) { 40 const baseUrl = useBaseUrl(); 41 let [_match, params] = useRoute(`${baseUrl}/:reportId`); 42 if (params?.reportId == undefined) { 43 return <Redirect to={baseUrl} />; 44 } else { 45 return ( 46 <div className="report-detail"> 47 <h1> 48 <BackButton to={baseUrl} /> Report Details 49 </h1> 50 <FormWithData 51 dataQuery={query.useGetReportQuery} 52 queryArg={params.reportId} 53 DataForm={ReportDetailForm} 54 /> 55 </div> 56 ); 57 } 58 }; 59 60 function ReportDetailForm({ data: report }) { 61 const from = report.account; 62 const target = report.target_account; 63 64 return ( 65 <div className="report detail"> 66 <div className="usernames"> 67 <Username user={from} /> reported <Username user={target} /> 68 </div> 69 70 {report.action_taken && 71 <div className="info"> 72 <h3>Resolved by @{report.action_taken_by_account.account.acct}</h3> 73 <span className="timestamp">at {new Date(report.action_taken_at).toLocaleString()}</span> 74 <br /> 75 <b>Comment: </b><span>{report.action_taken_comment}</span> 76 </div> 77 } 78 79 <div className="info-block"> 80 <h3>Report info:</h3> 81 <div className="details"> 82 <b>Created: </b> 83 <span>{new Date(report.created_at).toLocaleString()}</span> 84 85 <b>Forwarded: </b> <span>{report.forwarded ? "Yes" : "No"}</span> 86 <b>Category: </b> <span>{report.category}</span> 87 88 <b>Reason: </b> 89 {report.comment.length > 0 90 ? <p>{report.comment}</p> 91 : <i className="no-comment">none provided</i> 92 } 93 94 </div> 95 </div> 96 97 {!report.action_taken && <ReportActionForm report={report} />} 98 99 { 100 report.statuses.length > 0 && 101 <div className="info-block"> 102 <h3>Reported toots ({report.statuses.length}):</h3> 103 <div className="reported-toots"> 104 {report.statuses.map((status) => ( 105 <ReportedToot key={status.id} toot={status} /> 106 ))} 107 </div> 108 </div> 109 } 110 </div> 111 ); 112 } 113 114 function ReportActionForm({ report }) { 115 const form = { 116 id: useValue("id", report.id), 117 comment: useTextInput("action_taken_comment") 118 }; 119 120 const [submit, result] = useFormSubmit(form, query.useResolveReportMutation(), { changedOnly: false }); 121 122 return ( 123 <form onSubmit={submit} className="info-block"> 124 <h3>Resolving this report</h3> 125 <p> 126 An optional comment can be included while resolving this report. 127 Useful for providing an explanation about what action was taken (if any) before the report was marked as resolved.<br /> 128 <b>This will be visible to the user that created the report!</b> 129 </p> 130 <TextArea 131 field={form.comment} 132 label="Comment" 133 /> 134 <MutationButton label="Resolve" result={result} /> 135 </form> 136 ); 137 } 138 139 function ReportedToot({ toot }) { 140 const account = toot.account; 141 142 return ( 143 <article className="toot expanded"> 144 <section className="author"> 145 <a> 146 <img className="avatar" src={account.avatar} alt="" /> 147 <span className="displayname"> 148 {account.display_name.trim().length > 0 ? account.display_name : account.username} 149 <span className="sr-only">.</span> 150 </span> 151 <span className="username">@{account.username}</span> 152 </a> 153 </section> 154 <section className="body"> 155 <div className="text"> 156 <div className="content"> 157 {toot.spoiler_text?.length > 0 158 ? <TootCW content={toot.content} note={toot.spoiler_text} /> 159 : toot.content 160 } 161 </div> 162 </div> 163 {toot.media_attachments?.length > 0 && 164 <TootMedia media={toot.media_attachments} sensitive={toot.sensitive} /> 165 } 166 </section> 167 <aside className="info"> 168 <time dateTime={toot.created_at}>{new Date(toot.created_at).toLocaleString()}</time> 169 </aside> 170 </article> 171 ); 172 } 173 174 function TootCW({ note, content }) { 175 const [visible, setVisible] = React.useState(false); 176 177 function toggleVisible() { 178 setVisible(!visible); 179 } 180 181 return ( 182 <> 183 <div className="spoiler"> 184 <span>{note}</span> 185 <label className="button spoiler-label" onClick={toggleVisible}>Show {visible ? "less" : "more"}</label> 186 </div> 187 {visible && content} 188 </> 189 ); 190 } 191 192 function TootMedia({ media, sensitive }) { 193 let classes = (media.length % 2 == 0) ? "even" : "odd"; 194 if (media.length == 1) { 195 classes += " single"; 196 } 197 198 return ( 199 <div className={`media photoswipe-gallery ${classes}`}> 200 {media.map((m) => ( 201 <div key={m.id} className="media-wrapper"> 202 {sensitive && <> 203 <input id={`sensitiveMedia-${m.id}`} type="checkbox" className="sensitive-checkbox hidden" /> 204 <div className="sensitive"> 205 <div className="open"> 206 <label htmlFor={`sensitiveMedia-${m.id}`} className="button" role="button" tabIndex="0"> 207 <i className="fa fa-eye-slash" title="Hide sensitive media"></i> 208 </label> 209 </div> 210 <div className="closed" title={m.description}> 211 <label htmlFor={`sensitiveMedia-${m.id}`} className="button" role="button" tabIndex="0"> 212 Show sensitive media 213 </label> 214 </div> 215 </div> 216 </>} 217 <a 218 href={m.url} 219 title={m.description} 220 target="_blank" 221 rel="noreferrer" 222 data-cropped="true" 223 data-pswp-width={`${m.meta?.original.width}px`} 224 data-pswp-height={`${m.meta?.original.height}px`} 225 > 226 <img 227 alt={m.description} 228 src={m.url} 229 // thumb={m.preview_url} 230 size={m.meta?.original} 231 type={m.type} 232 /> 233 </a> 234 </div> 235 ))} 236 </div> 237 ); 238 }