parse-from-toot.js (6031B)
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 26 const { 27 useTextInput, 28 useComboBoxInput, 29 useCheckListInput 30 } = require("../../../lib/form"); 31 32 const useFormSubmit = require("../../../lib/form/submit"); 33 34 const CheckList = require("../../../components/check-list"); 35 const { CategorySelect } = require('../category-select'); 36 37 const { TextInput } = require("../../../components/form/inputs"); 38 const MutationButton = require("../../../components/form/mutation-button"); 39 const { Error } = require("../../../components/error"); 40 41 module.exports = function ParseFromToot({ emojiCodes }) { 42 const [searchStatus, result] = query.useSearchStatusForEmojiMutation(); 43 44 const [onURLChange, _resetURL, { url }] = useTextInput("url"); 45 46 function submitSearch(e) { 47 e.preventDefault(); 48 if (url.trim().length != 0) { 49 searchStatus(url); 50 } 51 } 52 53 return ( 54 <div className="parse-emoji"> 55 <h2>Steal this look</h2> 56 <form onSubmit={submitSearch}> 57 <div className="form-field text"> 58 <label htmlFor="url"> 59 Link to a toot: 60 </label> 61 <div className="row"> 62 <input 63 type="text" 64 id="url" 65 name="url" 66 onChange={onURLChange} 67 value={url} 68 /> 69 <button disabled={result.isLoading}> 70 <i className={[ 71 "fa fa-fw", 72 (result.isLoading 73 ? "fa-refresh fa-spin" 74 : "fa-search") 75 ].join(" ")} aria-hidden="true" title="Search" /> 76 <span className="sr-only">Search</span> 77 </button> 78 </div> 79 </div> 80 </form> 81 <SearchResult result={result} localEmojiCodes={emojiCodes} /> 82 </div> 83 ); 84 }; 85 86 function SearchResult({ result, localEmojiCodes }) { 87 const { error, data, isSuccess, isError } = result; 88 89 if (!(isSuccess || isError)) { 90 return null; 91 } 92 93 if (error == "NONE_FOUND") { 94 return "No results found"; 95 } else if (error == "LOCAL_INSTANCE") { 96 return <b>This is a local user/toot, all referenced emoji are already on your instance</b>; 97 } else if (error != undefined) { 98 return <Error error={result.error} />; 99 } 100 101 if (data.list.length == 0) { 102 return <b>This {data.type == "statuses" ? "toot" : "account"} doesn't use any custom emoji</b>; 103 } 104 105 return ( 106 <CopyEmojiForm 107 localEmojiCodes={localEmojiCodes} 108 type={data.type} 109 domain={data.domain} 110 emojiList={data.list} 111 /> 112 ); 113 } 114 115 function CopyEmojiForm({ localEmojiCodes, type, emojiList }) { 116 const form = { 117 selectedEmoji: useCheckListInput("selectedEmoji", { 118 entries: emojiList, 119 uniqueKey: "id" 120 }), 121 category: useComboBoxInput("category") 122 }; 123 124 const [formSubmit, result] = useFormSubmit( 125 form, 126 query.usePatchRemoteEmojisMutation(), 127 { 128 changedOnly: false, 129 onFinish: ({ data }) => { 130 if (data != undefined) { 131 form.selectedEmoji.updateMultiple( 132 // uncheck all successfully processed emoji 133 data.map(([id]) => [id, { checked: false }]) 134 ); 135 } 136 } 137 } 138 ); 139 140 const buttonsInactive = form.selectedEmoji.someSelected 141 ? {} 142 : { 143 disabled: true, 144 title: "No emoji selected, cannot perform any actions" 145 }; 146 147 const checkListExtraProps = React.useCallback(() => ({ localEmojiCodes }), [localEmojiCodes]); 148 149 return ( 150 <div className="parsed"> 151 <span>This {type == "statuses" ? "toot" : "account"} uses the following custom emoji, select the ones you want to copy/disable:</span> 152 <form onSubmit={formSubmit}> 153 <CheckList 154 field={form.selectedEmoji} 155 EntryComponent={EmojiEntry} 156 getExtraProps={checkListExtraProps} 157 /> 158 159 <CategorySelect 160 field={form.category} 161 /> 162 163 <div className="action-buttons row"> 164 <MutationButton name="copy" label="Copy to local emoji" result={result} showError={false} {...buttonsInactive} /> 165 <MutationButton name="disable" label="Disable" result={result} className="button danger" showError={false} {...buttonsInactive} /> 166 </div> 167 {result.error && ( 168 Array.isArray(result.error) 169 ? <ErrorList errors={result.error} /> 170 : <Error error={result.error} /> 171 )} 172 </form> 173 </div> 174 ); 175 } 176 177 function ErrorList({ errors }) { 178 return ( 179 <div className="error"> 180 One or multiple emoji failed to process: 181 {errors.map(([shortcode, err]) => ( 182 <div key={shortcode}> 183 <b>{shortcode}:</b> {err} 184 </div> 185 ))} 186 </div> 187 ); 188 } 189 190 function EmojiEntry({ entry: emoji, onChange, extraProps: { localEmojiCodes } }) { 191 const shortcodeField = useTextInput("shortcode", { 192 defaultValue: emoji.shortcode, 193 validator: function validateShortcode(code) { 194 return (emoji.checked && localEmojiCodes.has(code)) 195 ? "Shortcode already in use" 196 : ""; 197 } 198 }); 199 200 React.useEffect(() => { 201 if (emoji.valid != shortcodeField.valid) { 202 onChange({ valid: shortcodeField.valid }); 203 } 204 }, [onChange, emoji.valid, shortcodeField.valid]); 205 206 React.useEffect(() => { 207 shortcodeField.validate(); 208 // only need this update if it's the emoji.checked that updated, not shortcodeField 209 // eslint-disable-next-line react-hooks/exhaustive-deps 210 }, [emoji.checked]); 211 212 return ( 213 <> 214 <img className="emoji" src={emoji.url} title={emoji.shortcode} /> 215 216 <TextInput 217 field={shortcodeField} 218 onChange={(e) => { 219 shortcodeField.onChange(e); 220 onChange({ shortcode: e.target.value, checked: true }); 221 }} 222 /> 223 </> 224 ); 225 }