process.jsx (8580B)
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 24 const query = require("../../../lib/query"); 25 const { isValidDomainBlock, hasBetterScope } = require("../../../lib/domain-block"); 26 27 const { 28 useTextInput, 29 useBoolInput, 30 useRadioInput, 31 useCheckListInput 32 } = require("../../../lib/form"); 33 34 const useFormSubmit = require("../../../lib/form/submit"); 35 36 const { 37 TextInput, 38 TextArea, 39 Checkbox, 40 Select, 41 RadioGroup 42 } = require("../../../components/form/inputs"); 43 44 const CheckList = require("../../../components/check-list"); 45 const MutationButton = require("../../../components/form/mutation-button"); 46 const FormWithData = require("../../../lib/form/form-with-data"); 47 48 module.exports = React.memo( 49 function ProcessImport({ list }) { 50 return ( 51 <div className="without-border"> 52 <FormWithData 53 dataQuery={query.useInstanceBlocksQuery} 54 DataForm={ImportList} 55 list={list} 56 /> 57 </div> 58 ); 59 } 60 ); 61 62 function ImportList({ list, data: blockedInstances }) { 63 const hasComment = React.useMemo(() => { 64 let hasPublic = false; 65 let hasPrivate = false; 66 67 list.some((entry) => { 68 if (entry.public_comment?.length > 0) { 69 hasPublic = true; 70 } 71 72 if (entry.private_comment?.length > 0) { 73 hasPrivate = true; 74 } 75 76 return hasPublic && hasPrivate; 77 }); 78 79 if (hasPublic && hasPrivate) { 80 return { both: true }; 81 } else if (hasPublic) { 82 return { type: "public_comment" }; 83 } else if (hasPrivate) { 84 return { type: "private_comment" }; 85 } else { 86 return {}; 87 } 88 }, [list]); 89 90 const showComment = useTextInput("showComment", { defaultValue: hasComment.type ?? "public_comment" }); 91 92 const form = { 93 domains: useCheckListInput("domains", { entries: list }), 94 obfuscate: useBoolInput("obfuscate"), 95 privateComment: useTextInput("private_comment", { 96 defaultValue: `Imported on ${new Date().toLocaleString()}` 97 }), 98 privateCommentBehavior: useRadioInput("private_comment_behavior", { 99 defaultValue: "append", 100 options: { 101 append: "Append to", 102 replace: "Replace" 103 } 104 }), 105 publicComment: useTextInput("public_comment"), 106 publicCommentBehavior: useRadioInput("public_comment_behavior", { 107 defaultValue: "append", 108 options: { 109 append: "Append to", 110 replace: "Replace" 111 } 112 }), 113 }; 114 115 const [importDomains, importResult] = useFormSubmit(form, query.useImportDomainListMutation(), { changedOnly: false }); 116 117 return ( 118 <> 119 <form onSubmit={importDomains} className="suspend-import-list"> 120 <span>{list.length} domain{list.length != 1 ? "s" : ""} in this list</span> 121 122 {hasComment.both && 123 <Select field={showComment} options={ 124 <> 125 <option value="public_comment">Show public comments</option> 126 <option value="private_comment">Show private comments</option> 127 </> 128 } /> 129 } 130 131 <div className="checkbox-list-wrapper"> 132 <DomainCheckList 133 field={form.domains} 134 blockedInstances={blockedInstances} 135 commentType={showComment.value} 136 /> 137 </div> 138 139 <TextArea 140 field={form.privateComment} 141 label="Private comment" 142 rows={3} 143 /> 144 <RadioGroup 145 field={form.privateCommentBehavior} 146 label="imported private comment" 147 /> 148 149 <TextArea 150 field={form.publicComment} 151 label="Public comment" 152 rows={3} 153 /> 154 <RadioGroup 155 field={form.publicCommentBehavior} 156 label="imported public comment" 157 /> 158 159 <Checkbox 160 field={form.obfuscate} 161 label="Obfuscate domains in public lists" 162 /> 163 164 <MutationButton label="Import" result={importResult} /> 165 </form> 166 </> 167 ); 168 } 169 170 function DomainCheckList({ field, blockedInstances, commentType }) { 171 const getExtraProps = React.useCallback((entry) => { 172 return { 173 comment: entry[commentType], 174 alreadyExists: blockedInstances[entry.domain] != undefined 175 }; 176 }, [blockedInstances, commentType]); 177 178 const entriesWithSuggestions = React.useMemo(() => ( 179 Object.values(field.value).filter((entry) => entry.suggest) 180 ), [field.value]); 181 182 return ( 183 <> 184 <CheckList 185 field={field} 186 header={<> 187 <b>Domain</b> 188 <b> 189 {commentType == "public_comment" && "Public comment"} 190 {commentType == "private_comment" && "Private comment"} 191 </b> 192 </>} 193 EntryComponent={DomainEntry} 194 getExtraProps={getExtraProps} 195 /> 196 <UpdateHint 197 entries={entriesWithSuggestions} 198 updateEntry={field.onChange} 199 updateMultiple={field.updateMultiple} 200 /> 201 </> 202 ); 203 } 204 205 const UpdateHint = React.memo( 206 function UpdateHint({ entries, updateEntry, updateMultiple }) { 207 if (entries.length == 0) { 208 return null; 209 } 210 211 function changeAll() { 212 updateMultiple( 213 entries.map((entry) => [entry.key, { domain: entry.suggest, suggest: null }]) 214 ); 215 } 216 217 return ( 218 <div className="update-hints"> 219 <p> 220 {entries.length} {entries.length == 1 ? "entry uses" : "entries use"} a specific subdomain, 221 which you might want to change to the main domain, as that includes all it's (future) subdomains. 222 </p> 223 <div className="hints"> 224 {entries.map((entry) => ( 225 <UpdateableEntry key={entry.key} entry={entry} updateEntry={updateEntry} /> 226 ))} 227 </div> 228 {entries.length > 0 && <a onClick={changeAll}>change all</a>} 229 </div> 230 ); 231 } 232 ); 233 234 const UpdateableEntry = React.memo( 235 function UpdateableEntry({ entry, updateEntry }) { 236 return ( 237 <> 238 <span className="text-cutoff">{entry.domain}</span> 239 <i className="fa fa-long-arrow-right" aria-hidden="true"></i> 240 <span>{entry.suggest}</span> 241 <a role="button" onClick={() => 242 updateEntry(entry.key, { domain: entry.suggest, suggest: null }) 243 }>change</a> 244 </> 245 ); 246 } 247 ); 248 249 function domainValidationError(isValid) { 250 return isValid ? "" : "Invalid domain"; 251 } 252 253 function DomainEntry({ entry, onChange, extraProps: { alreadyExists, comment } }) { 254 const domainField = useTextInput("domain", { 255 defaultValue: entry.domain, 256 showValidation: entry.checked, 257 initValidation: domainValidationError(entry.valid), 258 validator: (value) => domainValidationError(isValidDomainBlock(value)) 259 }); 260 261 React.useEffect(() => { 262 if (entry.valid != domainField.valid) { 263 onChange({ valid: domainField.valid }); 264 } 265 }, [onChange, entry.valid, domainField.valid]); 266 267 React.useEffect(() => { 268 if (entry.domain != domainField.value) { 269 domainField.setter(entry.domain); 270 } 271 // domainField.setter is enough, eslint wants domainField 272 // eslint-disable-next-line react-hooks/exhaustive-deps 273 }, [entry.domain, domainField.setter]); 274 275 React.useEffect(() => { 276 onChange({ suggest: hasBetterScope(domainField.value) }); 277 // only need this update if it's the entry.checked that updated, not onChange 278 // eslint-disable-next-line react-hooks/exhaustive-deps 279 }, [domainField.value]); 280 281 function clickIcon(e) { 282 if (entry.suggest) { 283 e.stopPropagation(); 284 e.preventDefault(); 285 domainField.setter(entry.suggest); 286 onChange({ domain: entry.suggest, checked: true }); 287 } 288 } 289 290 return ( 291 <> 292 <div className="domain-input"> 293 <TextInput 294 field={domainField} 295 onChange={(e) => { 296 domainField.onChange(e); 297 onChange({ domain: e.target.value, checked: true }); 298 }} 299 /> 300 <span id="icon" onClick={clickIcon}> 301 <DomainEntryIcon alreadyExists={alreadyExists} suggestion={entry.suggest} onChange={onChange} /> 302 </span> 303 </div> 304 <p>{comment}</p> 305 </> 306 ); 307 } 308 309 function DomainEntryIcon({ alreadyExists, suggestion }) { 310 let icon; 311 let text; 312 313 if (suggestion) { 314 icon = "fa-info-circle suggest-changes"; 315 text = `Entry targets a specific subdomain, consider changing it to '${suggestion}'.`; 316 } else if (alreadyExists) { 317 icon = "fa-history already-blocked"; 318 text = "Domain block already exists."; 319 } 320 321 if (!icon) { 322 return null; 323 } 324 325 return ( 326 <> 327 <i className={`fa fa-fw ${icon}`} aria-hidden="true" title={text}></i> 328 <span className="sr-only">{text}</span> 329 </> 330 ); 331 }