this repo has no description
at main 263 lines 8.9 kB view raw
1import './drafts.css'; 2 3import { Trans, useLingui } from '@lingui/react/macro'; 4import { useEffect, useMemo, useReducer, useState } from 'react'; 5 6import { api } from '../utils/api'; 7import db from '../utils/db'; 8import niceDateTime from '../utils/nice-date-time'; 9import states from '../utils/states'; 10import { getCurrentAccountNS } from '../utils/store-utils'; 11 12import Icon from './icon'; 13import Loader from './loader'; 14import MenuConfirm from './menu-confirm'; 15 16function Drafts({ onClose }) { 17 const { t } = useLingui(); 18 const { masto } = api(); 19 const [uiState, setUIState] = useState('default'); 20 const [drafts, setDrafts] = useState([]); 21 const [reloadCount, reload] = useReducer((c) => c + 1, 0); 22 23 useEffect(() => { 24 setUIState('loading'); 25 (async () => { 26 try { 27 const keys = await db.drafts.keys(); 28 if (keys.length) { 29 const ns = getCurrentAccountNS(); 30 const ownKeys = keys.filter((key) => key.startsWith(ns)); 31 if (ownKeys.length) { 32 const drafts = await db.drafts.getMany(ownKeys); 33 drafts.sort( 34 (a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt), 35 ); 36 setDrafts(drafts); 37 } else { 38 setDrafts([]); 39 } 40 } else { 41 setDrafts([]); 42 } 43 setUIState('default'); 44 } catch (e) { 45 console.error(e); 46 setUIState('error'); 47 } 48 })(); 49 }, [reloadCount]); 50 51 const hasDrafts = drafts?.length > 0; 52 53 return ( 54 <div class="sheet"> 55 {!!onClose && ( 56 <button type="button" class="sheet-close" onClick={onClose}> 57 <Icon icon="x" alt={t`Close`} /> 58 </button> 59 )} 60 <header> 61 <h2> 62 <Trans>Unsent drafts</Trans>{' '} 63 <Loader abrupt hidden={uiState !== 'loading'} /> 64 </h2> 65 {hasDrafts && ( 66 <div class="insignificant"> 67 <Trans> 68 Looks like you have unsent drafts. Let's continue where you left 69 off. 70 </Trans> 71 </div> 72 )} 73 </header> 74 <main> 75 {hasDrafts ? ( 76 <> 77 <ul class="drafts-list"> 78 {drafts.map((draft) => { 79 const { updatedAt, key, draftStatus, replyTo } = draft; 80 const updatedAtDate = new Date(updatedAt); 81 return ( 82 <li key={updatedAt}> 83 <div class="mini-draft-meta"> 84 <b> 85 <Icon icon={replyTo ? 'reply' : 'quill'} size="s" />{' '} 86 <time> 87 {!!replyTo && ( 88 <> 89 <span class="bidi-isolate"> 90 @{replyTo.account.acct} 91 </span> 92 <br /> 93 </> 94 )} 95 {niceDateTime(updatedAtDate)} 96 </time> 97 </b> 98 <MenuConfirm 99 confirmLabel={ 100 <span> 101 <Trans>Delete this draft?</Trans> 102 </span> 103 } 104 menuItemClassName="danger" 105 align="end" 106 disabled={uiState === 'loading'} 107 onClick={() => { 108 (async () => { 109 try { 110 // const yes = confirm('Delete this draft?'); 111 // if (yes) { 112 await db.drafts.del(key); 113 reload(); 114 // } 115 } catch (e) { 116 alert(t`Error deleting draft! Please try again.`); 117 } 118 })(); 119 }} 120 > 121 <button 122 type="button" 123 class="small light" 124 disabled={uiState === 'loading'} 125 > 126 <Trans>Delete…</Trans> 127 </button> 128 </MenuConfirm> 129 </div> 130 <button 131 type="button" 132 disabled={uiState === 'loading'} 133 class="draft-item" 134 onClick={async () => { 135 // console.log({ draftStatus }); 136 let replyToStatus; 137 if (replyTo) { 138 setUIState('loading'); 139 try { 140 replyToStatus = await masto.v1.statuses 141 .$select(replyTo.id) 142 .fetch(); 143 } catch (e) { 144 console.error(e); 145 alert(t`Error fetching reply-to status!`); 146 setUIState('default'); 147 return; 148 } 149 setUIState('default'); 150 } 151 window.__COMPOSE__ = { 152 draftStatus, 153 replyToStatus, 154 }; 155 states.showCompose = true; 156 states.showDrafts = false; 157 }} 158 > 159 <MiniDraft draft={draft} /> 160 </button> 161 </li> 162 ); 163 })} 164 </ul> 165 {drafts.length > 1 && ( 166 <p> 167 <MenuConfirm 168 confirmLabel={ 169 <span> 170 <Trans>Delete all drafts?</Trans> 171 </span> 172 } 173 menuItemClassName="danger" 174 disabled={uiState === 'loading'} 175 onClick={() => { 176 (async () => { 177 // const yes = confirm('Delete all drafts?'); 178 // if (yes) { 179 setUIState('loading'); 180 try { 181 await db.drafts.delMany( 182 drafts.map((draft) => draft.key), 183 ); 184 setUIState('default'); 185 reload(); 186 } catch (e) { 187 console.error(e); 188 alert(t`Error deleting drafts! Please try again.`); 189 setUIState('error'); 190 } 191 // } 192 })(); 193 }} 194 > 195 <button 196 type="button" 197 class="light danger" 198 disabled={uiState === 'loading'} 199 > 200 <Trans>Delete all…</Trans> 201 </button> 202 </MenuConfirm> 203 </p> 204 )} 205 </> 206 ) : ( 207 <p> 208 <Trans>No drafts found.</Trans> 209 </p> 210 )} 211 </main> 212 </div> 213 ); 214} 215 216function MiniDraft({ draft }) { 217 const { t } = useLingui(); 218 const { draftStatus, replyTo } = draft; 219 const { status, spoilerText, poll, mediaAttachments } = draftStatus; 220 const hasPoll = poll?.options?.length > 0; 221 const hasMedia = mediaAttachments?.length > 0; 222 const hasPollOrMedia = hasPoll || hasMedia; 223 const firstImageMedia = useMemo(() => { 224 if (!hasMedia) return; 225 const image = mediaAttachments.find((media) => /image/.test(media.type)); 226 if (!image) return; 227 const { file } = image; 228 const objectURL = URL.createObjectURL(file); 229 return objectURL; 230 }, [hasMedia, mediaAttachments]); 231 return ( 232 <> 233 <div class="mini-draft"> 234 {hasPollOrMedia && ( 235 <div 236 class={`mini-draft-aside ${firstImageMedia ? 'has-image' : ''}`} 237 style={ 238 firstImageMedia 239 ? { 240 '--bg-image': `url(${firstImageMedia})`, 241 } 242 : {} 243 } 244 > 245 {hasPoll && <Icon icon="poll" alt={t`Poll`} />} 246 {hasMedia && ( 247 <span> 248 <Icon icon="attachment" alt={t`Media`} />{' '} 249 <small>{mediaAttachments?.length}</small> 250 </span> 251 )} 252 </div> 253 )} 254 <div class="mini-draft-main"> 255 {!!spoilerText && <div class="mini-draft-spoiler">{spoilerText}</div>} 256 {!!status && <div class="mini-draft-status">{status}</div>} 257 </div> 258 </div> 259 </> 260 ); 261} 262 263export default Drafts;