gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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 }