this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

MVP Scheduled Posts implementation

Somehow the CSS got formatted differently

+1156 -367
+2
src/app.jsx
··· 43 43 import Mentions from './pages/mentions'; 44 44 import Notifications from './pages/notifications'; 45 45 import Public from './pages/public'; 46 + import ScheduledPosts from './pages/scheduled-posts'; 46 47 import Search from './pages/search'; 47 48 import StatusRoute from './pages/status-route'; 48 49 import Trending from './pages/trending'; ··· 548 549 <Route path=":id" element={<List />} /> 549 550 </Route> 550 551 <Route path="/fh" element={<FollowedHashtags />} /> 552 + <Route path="/sp" element={<ScheduledPosts />} /> 551 553 <Route path="/ft" element={<Filters />} /> 552 554 <Route path="/catchup" element={<Catchup />} /> 553 555 <Route path="/annual_report/:year" element={<AnnualReport />} />
+3
src/components/ICONS.jsx
··· 175 175 'user-x': () => import('@iconify-icons/mingcute/user-x-line'), 176 176 minimize: () => import('@iconify-icons/mingcute/arrows-down-line'), 177 177 celebrate: () => import('@iconify-icons/mingcute/celebrate-line'), 178 + schedule: () => import('@iconify-icons/mingcute/calendar-time-add-line'), 179 + month: () => import('@iconify-icons/mingcute/calendar-month-line'), 180 + day: () => import('@iconify-icons/mingcute/calendar-day-line'), 178 181 };
+75
src/components/ScheduledAtField.jsx
··· 1 + import { useEffect, useState } from 'preact/hooks'; 2 + 3 + export const MIN_SCHEDULED_AT = 6 * 60 * 1000; // 6 mins 4 + const MAX_SCHEDULED_AT = 90 * 24 * 60 * 60 * 1000; // 90 days 5 + 6 + export default function ScheduledAtField({ scheduledAt, setScheduledAt }) { 7 + if (!scheduledAt || !(scheduledAt instanceof Date)) { 8 + console.warn('scheduledAt is not a Date:', scheduledAt); 9 + return; 10 + } 11 + const [minStr, setMinStr] = useState(); 12 + const [maxStr, setMaxStr] = useState(); 13 + const timezoneOffset = scheduledAt.getTimezoneOffset(); 14 + 15 + useEffect(() => { 16 + function updateMinStr() { 17 + const min = new Date(Date.now() + MIN_SCHEDULED_AT); 18 + const str = new Date(min.getTime() - timezoneOffset * 60000) 19 + .toISOString() 20 + .slice(0, 16); 21 + setMinStr(str); 22 + console.log('MIN', min); 23 + } 24 + updateMinStr(); 25 + 26 + function updateMaxStr() { 27 + const max = new Date(Date.now() + MAX_SCHEDULED_AT); 28 + const str = new Date(max.getTime() - timezoneOffset * 60000) 29 + .toISOString() 30 + .slice(0, 16); 31 + setMaxStr(str); 32 + console.log('MAX', max); 33 + } 34 + updateMaxStr(); 35 + 36 + // Update every 10s 37 + const intervalId = setInterval(() => { 38 + updateMinStr(); 39 + updateMaxStr(); 40 + }, 1000 * 10); 41 + return () => clearInterval(intervalId); 42 + }, []); 43 + 44 + const defaultValue = scheduledAt 45 + ? new Date(scheduledAt.getTime() - scheduledAt.getTimezoneOffset() * 60000) 46 + .toISOString() 47 + .slice(0, 16) 48 + : null; 49 + 50 + return ( 51 + <input 52 + type="datetime-local" 53 + name="scheduledAt" 54 + defaultValue={defaultValue} 55 + min={minStr} 56 + max={maxStr} 57 + required 58 + onChange={(e) => { 59 + setScheduledAt(new Date(e.target.value)); 60 + }} 61 + /> 62 + ); 63 + } 64 + 65 + export function getLocalTimezoneName() { 66 + const date = new Date(); 67 + const formatter = new Intl.DateTimeFormat(undefined, { 68 + timeZoneName: 'long', 69 + }); 70 + const parts = formatter.formatToParts(date); 71 + const timezoneName = parts.find( 72 + (part) => part.type === 'timeZoneName', 73 + )?.value; 74 + return timezoneName; 75 + }
+41 -15
src/components/compose.css
··· 94 94 ); */ 95 95 border-top: var(--hairline-width) solid var(--outline-color); 96 96 backdrop-filter: blur(8px); 97 - text-shadow: 0 1px 10px var(--bg-color), 0 1px 10px var(--bg-color), 98 - 0 1px 10px var(--bg-color), 0 1px 10px var(--bg-color), 97 + text-shadow: 98 + 0 1px 10px var(--bg-color), 99 + 0 1px 10px var(--bg-color), 100 + 0 1px 10px var(--bg-color), 101 + 0 1px 10px var(--bg-color), 99 102 0 1px 10px var(--bg-color); 100 103 z-index: 2; 101 104 ··· 134 137 } 135 138 } 136 139 #compose-container .status-preview ~ form { 137 - box-shadow: var(--drop-shadow), 0 -3px 6px -3px var(--drop-shadow-color); 140 + box-shadow: 141 + var(--drop-shadow), 142 + 0 -3px 6px -3px var(--drop-shadow-color); 138 143 } 139 144 140 145 #compose-container textarea { ··· 412 417 width: 80px; 413 418 height: 80px; 414 419 /* checkerboard background */ 415 - background-image: linear-gradient( 416 - 45deg, 417 - var(--img-bg-color) 25%, 418 - transparent 25% 419 - ), 420 + background-image: 421 + linear-gradient(45deg, var(--img-bg-color) 25%, transparent 25%), 420 422 linear-gradient(-45deg, var(--img-bg-color) 25%, transparent 25%), 421 423 linear-gradient(45deg, transparent 75%, var(--img-bg-color) 75%), 422 424 linear-gradient(-45deg, transparent 75%, var(--img-bg-color) 75%); 423 425 background-size: 10px 10px; 424 - background-position: 0 0, 0 5px, 5px -5px, -5px 0px; 426 + background-position: 427 + 0 0, 428 + 0 5px, 429 + 5px -5px, 430 + -5px 0px; 425 431 } 426 432 #compose-container .media-preview > * { 427 433 width: 80px; ··· 562 568 color: var(--red-color); 563 569 } 564 570 571 + #compose-container { 572 + .scheduled-at { 573 + background-color: var(--bg-faded-color); 574 + border-radius: 8px; 575 + margin: 8px 0 0; 576 + justify-content: flex-end; 577 + text-align: end; 578 + 579 + input[type='datetime-local'] { 580 + max-width: 80vw; 581 + padding: 4px; 582 + 583 + &:invalid { 584 + border-color: var(--red-color); 585 + } 586 + } 587 + } 588 + } 589 + 565 590 .compose-menu-add-media { 566 591 position: relative; 567 592 ··· 660 685 overflow: hidden; 661 686 box-shadow: 0 2px 16px var(--img-bg-color); 662 687 /* checkerboard background */ 663 - background-image: linear-gradient( 664 - 45deg, 665 - var(--img-bg-color) 25%, 666 - transparent 25% 667 - ), 688 + background-image: 689 + linear-gradient(45deg, var(--img-bg-color) 25%, transparent 25%), 668 690 linear-gradient(-45deg, var(--img-bg-color) 25%, transparent 25%), 669 691 linear-gradient(45deg, transparent 75%, var(--img-bg-color) 75%), 670 692 linear-gradient(-45deg, transparent 75%, var(--img-bg-color) 75%); 671 693 background-size: 20px 20px; 672 - background-position: 0 0, 0 10px, 10px -10px, -10px 0px; 694 + background-position: 695 + 0 0, 696 + 0 10px, 697 + 10px -10px, 698 + -10px 0px; 673 699 flex: 0.8; 674 700 } 675 701 #media-sheet .media-preview > * {
+88 -16
src/components/compose.jsx
··· 59 59 import Icon from './icon'; 60 60 import Loader from './loader'; 61 61 import Modal from './modal'; 62 + import ScheduledAtField, { 63 + getLocalTimezoneName, 64 + MIN_SCHEDULED_AT, 65 + } from './ScheduledAtField'; 62 66 import Status from './status'; 63 67 64 68 const { ··· 207 211 customEmoji: msg`Add custom emoji`, 208 212 gif: msg`Add GIF`, 209 213 poll: msg`Add poll`, 214 + scheduledPost: msg`Schedule post`, 210 215 }; 211 216 212 217 function Compose({ ··· 265 270 const prevLanguage = useRef(language); 266 271 const [mediaAttachments, setMediaAttachments] = useState([]); 267 272 const [poll, setPoll] = useState(null); 273 + const [scheduledAt, setScheduledAt] = useState(null); 268 274 269 275 const prefs = store.account.get('preferences') || {}; 270 276 ··· 375 381 sensitive, 376 382 poll, 377 383 mediaAttachments, 384 + scheduledAt, 378 385 } = draftStatus; 379 386 const composablePoll = !!poll?.options && { 380 387 ...poll, ··· 394 401 if (sensitive !== null) setSensitive(sensitive); 395 402 if (composablePoll) setPoll(composablePoll); 396 403 if (mediaAttachments) setMediaAttachments(mediaAttachments); 404 + if (scheduledAt) setScheduledAt(scheduledAt); 397 405 } 398 406 }, [draftStatus, editStatus, replyToStatus]); 399 407 ··· 574 582 sensitive, 575 583 poll, 576 584 mediaAttachments, 585 + scheduledAt, 577 586 }, 578 587 }; 579 588 if ( ··· 773 782 }, 774 783 }); 775 784 785 + const showScheduledAt = !editStatus; 786 + const scheduledAtButtonDisabled = uiState === 'loading' || !!scheduledAt; 787 + const onScheduledAtClick = () => { 788 + const date = new Date(Date.now() + MIN_SCHEDULED_AT); 789 + setScheduledAt(date); 790 + }; 791 + 776 792 return ( 777 793 <div id="compose-container-outer"> 778 794 <div id="compose-container" class={standalone ? 'standalone' : ''}> ··· 827 843 sensitive, 828 844 poll, 829 845 mediaAttachments, 846 + scheduledAt, 830 847 }, 831 848 }); 832 849 ··· 915 932 sensitive, 916 933 poll, 917 934 mediaAttachments, 935 + scheduledAt, 918 936 }, 919 937 }; 920 938 window.opener.__COMPOSE__ = passData; // Pass it here instead of `showCompose` due to some weird proxy issue again ··· 991 1009 const formData = new FormData(e.target); 992 1010 const entries = Object.fromEntries(formData.entries()); 993 1011 console.log('ENTRIES', entries); 994 - let { status, visibility, sensitive, spoilerText } = entries; 1012 + let { status, visibility, sensitive, spoilerText, scheduledAt } = 1013 + entries; 995 1014 996 1015 // Pre-cleanup 997 1016 sensitive = sensitive === 'on'; // checkboxes return "on" if checked 1017 + 1018 + // Convert datetime-local input value to RFC3339 Date string value 1019 + scheduledAt = scheduledAt 1020 + ? new Date(scheduledAt).toISOString() 1021 + : undefined; 998 1022 999 1023 // Validation 1000 1024 /* Let the backend validate this ··· 1125 1149 params.visibility = visibility; 1126 1150 // params.inReplyToId = replyToStatus?.id || undefined; 1127 1151 params.in_reply_to_id = replyToStatus?.id || undefined; 1152 + params.scheduled_at = scheduledAt; 1128 1153 } 1129 1154 params = removeNullUndefined(params); 1130 1155 console.log('POST', params); ··· 1161 1186 type: editStatus ? 'edit' : replyToStatus ? 'reply' : 'post', 1162 1187 newStatus, 1163 1188 instance, 1189 + scheduledAt, 1164 1190 }); 1165 1191 } catch (e) { 1166 1192 states.composerState.publishing = false; ··· 1358 1384 } 1359 1385 }} 1360 1386 /> 1387 + )} 1388 + {scheduledAt && ( 1389 + <div class="toolbar scheduled-at"> 1390 + <button 1391 + type="button" 1392 + class="plain4 small" 1393 + onClick={() => { 1394 + setScheduledAt(null); 1395 + }} 1396 + > 1397 + <Icon icon="x" /> 1398 + </button> 1399 + <label> 1400 + <Trans> 1401 + Posting on{' '} 1402 + <ScheduledAtField 1403 + scheduledAt={scheduledAt} 1404 + setScheduledAt={setScheduledAt} 1405 + /> 1406 + </Trans> 1407 + <br /> 1408 + <small>{getLocalTimezoneName()}</small> 1409 + </label> 1410 + </div> 1361 1411 )} 1362 1412 <div class="toolbar compose-footer"> 1363 1413 <span class="add-toolbar-button-group spacer"> ··· 1418 1468 <span>{_(ADD_LABELS.gif)}</span> 1419 1469 </MenuItem> 1420 1470 )} 1421 - <MenuItem 1422 - disabled={pollButtonDisabled} 1423 - onClick={onPollButtonClick} 1424 - > 1425 - <Icon icon="poll" /> <span>{_(ADD_LABELS.poll)}</span> 1426 - </MenuItem> 1471 + {showPollButton && ( 1472 + <MenuItem 1473 + disabled={pollButtonDisabled} 1474 + onClick={onPollButtonClick} 1475 + > 1476 + <Icon icon="poll" /> <span>{_(ADD_LABELS.poll)}</span> 1477 + </MenuItem> 1478 + )} 1479 + {showScheduledAt && ( 1480 + <MenuItem 1481 + disabled={scheduledAtButtonDisabled} 1482 + onClick={onScheduledAtClick} 1483 + > 1484 + <Icon icon="schedule" />{' '} 1485 + <span>{_(ADD_LABELS.scheduledPost)}</span> 1486 + </MenuItem> 1487 + )} 1427 1488 </Menu2> 1428 1489 )} 1429 1490 <span class="add-sub-toolbar-button-group" ref={addSubToolbarRef}> ··· 1476 1537 /> 1477 1538 </button> 1478 1539 )} 1479 - {} 1480 1540 {showPollButton && ( 1481 1541 <> 1482 1542 <button ··· 1489 1549 </button> 1490 1550 </> 1491 1551 )} 1552 + {showScheduledAt && ( 1553 + <button 1554 + type="button" 1555 + class={`toolbar-button ${scheduledAt ? 'highlight' : ''}`} 1556 + disabled={scheduledAtButtonDisabled} 1557 + onClick={onScheduledAtClick} 1558 + > 1559 + <Icon icon="schedule" alt={_(ADD_LABELS.scheduledPost)} /> 1560 + </button> 1561 + )} 1492 1562 </span> 1493 1563 </span> 1494 1564 {/* <div class="spacer" /> */} ··· 1551 1621 </select> 1552 1622 </label>{' '} 1553 1623 <button type="submit" disabled={uiState === 'loading'}> 1554 - {replyToStatus 1555 - ? t`Reply` 1556 - : editStatus 1557 - ? t`Update` 1558 - : t({ 1559 - message: 'Post', 1560 - context: 'Submit button in composer', 1561 - })} 1624 + {scheduledAt 1625 + ? t`Schedule` 1626 + : replyToStatus 1627 + ? t`Reply` 1628 + : editStatus 1629 + ? t`Update` 1630 + : t({ 1631 + message: 'Post', 1632 + context: 'Submit button in composer', 1633 + })} 1562 1634 </button> 1563 1635 </div> 1564 1636 </form>
+17 -8
src/components/modals.jsx
··· 63 63 null 64 64 } 65 65 onClose={(results) => { 66 - const { newStatus, instance, type } = results || {}; 66 + const { newStatus, instance, type, scheduledAt } = results || {}; 67 67 states.showCompose = false; 68 68 window.__COMPOSE__ = null; 69 69 if (newStatus) { 70 70 states.reloadStatusPage++; 71 + if (scheduledAt) states.reloadScheduledPosts++; 71 72 showToast({ 72 73 text: { 73 - post: t`Post published. Check it out.`, 74 - reply: t`Reply posted. Check it out.`, 74 + post: scheduledAt 75 + ? t`Post scheduled` 76 + : t`Post published. Check it out.`, 77 + reply: scheduledAt 78 + ? t`Reply scheduled` 79 + : t`Reply posted. Check it out.`, 75 80 edit: t`Post updated. Check it out.`, 76 81 }[type || 'post'], 77 82 delay: 1000, ··· 79 84 onClick: (toast) => { 80 85 toast.hideToast(); 81 86 states.prevLocation = location; 82 - navigate( 83 - instance 84 - ? `/${instance}/s/${newStatus.id}` 85 - : `/s/${newStatus.id}`, 86 - ); 87 + if (scheduledAt) { 88 + navigate('/sp'); 89 + } else { 90 + navigate( 91 + instance 92 + ? `/${instance}/s/${newStatus.id}` 93 + : `/s/${newStatus.id}`, 94 + ); 95 + } 87 96 }, 88 97 }); 89 98 }
+1
src/components/name-text.jsx
··· 36 36 onClick, 37 37 }) { 38 38 const { i18n } = useLingui(); 39 + if (!account) return null; 39 40 const { 40 41 acct, 41 42 avatar,
+6
src/components/nav-menu.jsx
··· 254 254 <Trans>Followed Hashtags</Trans> 255 255 </span> 256 256 </MenuLink> 257 + <MenuLink to="/sp"> 258 + <Icon icon="schedule" size="l" />{' '} 259 + <span> 260 + <Trans>Scheduled Posts</Trans> 261 + </span> 262 + </MenuLink> 257 263 <MenuDivider /> 258 264 {supports('@mastodon/filters') && ( 259 265 <MenuLink to="/ft">
+1 -1
src/components/poll.jsx
··· 27 27 ownVotes, 28 28 voted, 29 29 votersCount, 30 - votesCount, 30 + votesCount = 0, 31 31 emojis, 32 32 } = poll; 33 33 const expiresAtDate = !!expiresAt && new Date(expiresAt); // Update poll at point of expiry
+6 -3
src/components/relative-time.jsx
··· 40 40 const seconds = (date.getTime() - Date.now()) / 1000; 41 41 const absSeconds = Math.abs(seconds); 42 42 if (absSeconds < minute) { 43 - return rtf.format(seconds, 'second'); 43 + return rtf.format(Math.floor(seconds), 'second'); 44 44 } else if (absSeconds < hour) { 45 45 return rtf.format(Math.floor(seconds / minute), 'minute'); 46 46 } else if (absSeconds < day) { 47 47 return rtf.format(Math.floor(seconds / hour), 'hour'); 48 - } else { 48 + } else if (absSeconds < 30 * day) { 49 49 return rtf.format(Math.floor(seconds / day), 'day'); 50 + } else { 51 + return rtf.format(Math.floor(seconds / day / 30), 'month'); 50 52 } 51 53 }; 52 54 ··· 76 78 const [renderCount, rerender] = useReducer((x) => x + 1, 0); 77 79 const date = useMemo(() => new Date(datetime), [datetime]); 78 80 const [dateStr, dt, title] = useMemo(() => { 79 - if (!isValidDate(date)) return ['' + datetime, '', '']; 81 + if (!isValidDate(date)) 82 + return ['' + (typeof datetime === 'string' ? datetime : ''), '', '']; 80 83 let str; 81 84 if (format === 'micro') { 82 85 // If date <= 1 day ago or day is within this year
+143 -141
src/components/status.jsx
··· 201 201 } 202 202 } 203 203 divRef.current.replaceChildren(dom.cloneNode(true)); 204 - }, [content, emojis.length]); 204 + }, [content, emojis?.length]); 205 205 206 206 return ( 207 207 <div ··· 358 358 emojis: accountEmojis, 359 359 bot, 360 360 group, 361 - }, 361 + } = {}, 362 362 id, 363 363 repliesCount, 364 364 reblogged, ··· 428 428 return null; 429 429 } 430 430 431 - console.debug('RENDER Status', id, status?.account.displayName, quoted); 431 + console.debug('RENDER Status', id, status?.account?.displayName, quoted); 432 432 433 433 const debugHover = (e) => { 434 434 if (e.shiftKey) { ··· 646 646 [spoilerText, content], 647 647 ); 648 648 649 - const createdDateText = niceDateTime(createdAtDate); 649 + const createdDateText = createdAt && niceDateTime(createdAtDate); 650 650 const editedDateText = editedAt && niceDateTime(editedAtDate); 651 651 652 652 // Can boost if: ··· 1792 1792 </a> 1793 1793 )} 1794 1794 <div class="container"> 1795 - <div class="meta"> 1796 - <span class="meta-name"> 1797 - <NameText 1798 - account={status.account} 1799 - instance={instance} 1800 - showAvatar={size === 's'} 1801 - showAcct={isSizeLarge} 1802 - /> 1803 - </span> 1804 - {/* {inReplyToAccount && !withinContext && size !== 's' && ( 1805 - <> 1806 - {' '} 1807 - <span class="ib"> 1808 - <Icon icon="arrow-right" class="arrow" />{' '} 1809 - <NameText account={inReplyToAccount} instance={instance} short /> 1810 - </span> 1811 - </> 1812 - )} */} 1813 - {/* </span> */}{' '} 1814 - {size !== 'l' && 1815 - (_deleted ? ( 1816 - <span class="status-deleted-tag"> 1817 - <Trans>Deleted</Trans> 1818 - </span> 1819 - ) : url && !previewMode && !readOnly && !quoted ? ( 1820 - <Link 1821 - to={instance ? `/${instance}/s/${id}` : `/s/${id}`} 1822 - onClick={(e) => { 1823 - if ( 1824 - e.metaKey || 1825 - e.ctrlKey || 1826 - e.shiftKey || 1827 - e.altKey || 1828 - e.which === 2 1829 - ) { 1830 - return; 1831 - } 1832 - e.preventDefault(); 1833 - e.stopPropagation(); 1834 - onStatusLinkClick?.(e, status); 1835 - setContextMenuProps({ 1836 - anchorRef: { 1837 - current: e.currentTarget, 1838 - }, 1839 - align: 'end', 1840 - direction: 'bottom', 1841 - gap: 4, 1842 - }); 1843 - setIsContextMenuOpen(true); 1844 - }} 1845 - class={`time ${ 1846 - isContextMenuOpen && contextMenuProps?.anchorRef 1847 - ? 'is-open' 1848 - : '' 1849 - }`} 1850 - > 1851 - {showCommentHint && !showCommentCount ? ( 1852 - <Icon 1853 - icon="comment2" 1854 - size="s" 1855 - // alt={`${repliesCount} ${ 1856 - // repliesCount === 1 ? 'reply' : 'replies' 1857 - // }`} 1858 - alt={plural(repliesCount, { 1859 - one: '# reply', 1860 - other: '# replies', 1861 - })} 1862 - /> 1863 - ) : ( 1864 - visibility !== 'public' && 1865 - visibility !== 'direct' && ( 1795 + {!!(status.account || createdAt) && ( 1796 + <div class="meta"> 1797 + <span class="meta-name"> 1798 + <NameText 1799 + account={status.account} 1800 + instance={instance} 1801 + showAvatar={size === 's'} 1802 + showAcct={isSizeLarge} 1803 + /> 1804 + </span> 1805 + {/* {inReplyToAccount && !withinContext && size !== 's' && ( 1806 + <> 1807 + {' '} 1808 + <span class="ib"> 1809 + <Icon icon="arrow-right" class="arrow" />{' '} 1810 + <NameText account={inReplyToAccount} instance={instance} short /> 1811 + </span> 1812 + </> 1813 + )} */} 1814 + {/* </span> */}{' '} 1815 + {size !== 'l' && 1816 + (_deleted ? ( 1817 + <span class="status-deleted-tag"> 1818 + <Trans>Deleted</Trans> 1819 + </span> 1820 + ) : url && !previewMode && !readOnly && !quoted ? ( 1821 + <Link 1822 + to={instance ? `/${instance}/s/${id}` : `/s/${id}`} 1823 + onClick={(e) => { 1824 + if ( 1825 + e.metaKey || 1826 + e.ctrlKey || 1827 + e.shiftKey || 1828 + e.altKey || 1829 + e.which === 2 1830 + ) { 1831 + return; 1832 + } 1833 + e.preventDefault(); 1834 + e.stopPropagation(); 1835 + onStatusLinkClick?.(e, status); 1836 + setContextMenuProps({ 1837 + anchorRef: { 1838 + current: e.currentTarget, 1839 + }, 1840 + align: 'end', 1841 + direction: 'bottom', 1842 + gap: 4, 1843 + }); 1844 + setIsContextMenuOpen(true); 1845 + }} 1846 + class={`time ${ 1847 + isContextMenuOpen && contextMenuProps?.anchorRef 1848 + ? 'is-open' 1849 + : '' 1850 + }`} 1851 + > 1852 + {showCommentHint && !showCommentCount ? ( 1866 1853 <Icon 1867 - icon={visibilityIconsMap[visibility]} 1868 - alt={_(visibilityText[visibility])} 1854 + icon="comment2" 1869 1855 size="s" 1856 + // alt={`${repliesCount} ${ 1857 + // repliesCount === 1 ? 'reply' : 'replies' 1858 + // }`} 1859 + alt={plural(repliesCount, { 1860 + one: '# reply', 1861 + other: '# replies', 1862 + })} 1870 1863 /> 1871 - ) 1872 - )}{' '} 1873 - <RelativeTime datetime={createdAtDate} format="micro" /> 1874 - {!previewMode && !readOnly && ( 1875 - <Icon icon="more2" class="more" alt={t`More`} /> 1876 - )} 1877 - </Link> 1878 - ) : ( 1879 - // <Menu 1880 - // instanceRef={menuInstanceRef} 1881 - // portal={{ 1882 - // target: document.body, 1883 - // }} 1884 - // containerProps={{ 1885 - // style: { 1886 - // // Higher than the backdrop 1887 - // zIndex: 1001, 1888 - // }, 1889 - // onClick: (e) => { 1890 - // if (e.target === e.currentTarget) 1891 - // menuInstanceRef.current?.closeMenu?.(); 1892 - // }, 1893 - // }} 1894 - // align="end" 1895 - // gap={4} 1896 - // overflow="auto" 1897 - // viewScroll="close" 1898 - // boundingBoxPadding="8 8 8 8" 1899 - // unmountOnClose 1900 - // menuButton={({ open }) => ( 1901 - // <Link 1902 - // to={instance ? `/${instance}/s/${id}` : `/s/${id}`} 1903 - // onClick={(e) => { 1904 - // e.preventDefault(); 1905 - // e.stopPropagation(); 1906 - // onStatusLinkClick?.(e, status); 1907 - // }} 1908 - // class={`time ${open ? 'is-open' : ''}`} 1909 - // > 1910 - // <Icon 1911 - // icon={visibilityIconsMap[visibility]} 1912 - // alt={visibilityText[visibility]} 1913 - // size="s" 1914 - // />{' '} 1915 - // <RelativeTime datetime={createdAtDate} format="micro" /> 1916 - // </Link> 1917 - // )} 1918 - // > 1919 - // {StatusMenuItems} 1920 - // </Menu> 1921 - <span class="time"> 1922 - {visibility !== 'public' && visibility !== 'direct' && ( 1923 - <> 1924 - <Icon 1925 - icon={visibilityIconsMap[visibility]} 1926 - alt={_(visibilityText[visibility])} 1927 - size="s" 1928 - />{' '} 1929 - </> 1930 - )} 1931 - <RelativeTime datetime={createdAtDate} format="micro" /> 1932 - </span> 1933 - ))} 1934 - </div> 1864 + ) : ( 1865 + visibility !== 'public' && 1866 + visibility !== 'direct' && ( 1867 + <Icon 1868 + icon={visibilityIconsMap[visibility]} 1869 + alt={_(visibilityText[visibility])} 1870 + size="s" 1871 + /> 1872 + ) 1873 + )}{' '} 1874 + <RelativeTime datetime={createdAtDate} format="micro" /> 1875 + {!previewMode && !readOnly && ( 1876 + <Icon icon="more2" class="more" alt={t`More`} /> 1877 + )} 1878 + </Link> 1879 + ) : ( 1880 + // <Menu 1881 + // instanceRef={menuInstanceRef} 1882 + // portal={{ 1883 + // target: document.body, 1884 + // }} 1885 + // containerProps={{ 1886 + // style: { 1887 + // // Higher than the backdrop 1888 + // zIndex: 1001, 1889 + // }, 1890 + // onClick: (e) => { 1891 + // if (e.target === e.currentTarget) 1892 + // menuInstanceRef.current?.closeMenu?.(); 1893 + // }, 1894 + // }} 1895 + // align="end" 1896 + // gap={4} 1897 + // overflow="auto" 1898 + // viewScroll="close" 1899 + // boundingBoxPadding="8 8 8 8" 1900 + // unmountOnClose 1901 + // menuButton={({ open }) => ( 1902 + // <Link 1903 + // to={instance ? `/${instance}/s/${id}` : `/s/${id}`} 1904 + // onClick={(e) => { 1905 + // e.preventDefault(); 1906 + // e.stopPropagation(); 1907 + // onStatusLinkClick?.(e, status); 1908 + // }} 1909 + // class={`time ${open ? 'is-open' : ''}`} 1910 + // > 1911 + // <Icon 1912 + // icon={visibilityIconsMap[visibility]} 1913 + // alt={visibilityText[visibility]} 1914 + // size="s" 1915 + // />{' '} 1916 + // <RelativeTime datetime={createdAtDate} format="micro" /> 1917 + // </Link> 1918 + // )} 1919 + // > 1920 + // {StatusMenuItems} 1921 + // </Menu> 1922 + <span class="time"> 1923 + {visibility !== 'public' && visibility !== 'direct' && ( 1924 + <> 1925 + <Icon 1926 + icon={visibilityIconsMap[visibility]} 1927 + alt={_(visibilityText[visibility])} 1928 + size="s" 1929 + />{' '} 1930 + </> 1931 + )} 1932 + <RelativeTime datetime={createdAtDate} format="micro" /> 1933 + </span> 1934 + ))} 1935 + </div> 1936 + )} 1935 1937 {visibility === 'direct' && ( 1936 1938 <> 1937 1939 <div class="status-direct-badge">
+15 -4
src/index.css
··· 11 11 --main-width: 40em; 12 12 text-size-adjust: none; 13 13 --hairline-width: 1px; 14 - --monospace-font: ui-monospace, 'SFMono-Regular', Consolas, 'Liberation Mono', 15 - Menlo, Courier, monospace; 14 + --monospace-font: 15 + ui-monospace, 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, 16 + monospace; 16 17 17 18 --blue-color: royalblue; 18 19 --purple-color: blueviolet; ··· 190 191 } 191 192 192 193 body { 193 - font-family: ui-rounded, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, 194 - Ubuntu, Cantarell, Noto Sans, sans-serif; 194 + font-family: 195 + ui-rounded, 196 + -apple-system, 197 + BlinkMacSystemFont, 198 + Segoe UI, 199 + Roboto, 200 + Ubuntu, 201 + Cantarell, 202 + Noto Sans, 203 + sans-serif; 195 204 font-size: var(--text-size); 196 205 word-wrap: break-word; 197 206 overflow-wrap: break-word; ··· 367 376 368 377 input[type='text'], 369 378 input[type='search'], 379 + input[type='datetime-local'], 370 380 textarea, 371 381 select { 372 382 color: var(--text-color); ··· 377 387 } 378 388 input[type='text']:focus, 379 389 input[type='search']:focus, 390 + input[type='datetime-local']:focus, 380 391 textarea:focus, 381 392 select:focus { 382 393 border-color: var(--outline-color);
+249 -179
src/locales/en.po
··· 108 108 109 109 #: src/components/account-info.jsx:430 110 110 #: src/components/account-info.jsx:1143 111 - #: src/components/compose.jsx:2624 111 + #: src/components/compose.jsx:2696 112 112 #: src/components/media-alt-modal.jsx:46 113 113 #: src/components/media-modal.jsx:358 114 114 #: src/components/status.jsx:1734 115 115 #: src/components/status.jsx:1751 116 - #: src/components/status.jsx:1875 117 - #: src/components/status.jsx:2479 118 - #: src/components/status.jsx:2482 116 + #: src/components/status.jsx:1876 117 + #: src/components/status.jsx:2481 118 + #: src/components/status.jsx:2484 119 119 #: src/pages/account-statuses.jsx:523 120 120 #: src/pages/accounts.jsx:110 121 121 #: src/pages/hashtag.jsx:200 122 122 #: src/pages/list.jsx:158 123 123 #: src/pages/public.jsx:115 124 + #: src/pages/scheduled-posts.jsx:87 124 125 #: src/pages/status.jsx:1214 125 126 #: src/pages/trending.jsx:472 126 127 msgid "More" ··· 196 197 msgstr "" 197 198 198 199 #: src/components/account-info.jsx:887 199 - #: src/components/status.jsx:2265 200 + #: src/components/status.jsx:2267 200 201 #: src/pages/catchup.jsx:71 201 202 #: src/pages/catchup.jsx:1445 202 203 #: src/pages/catchup.jsx:2058 ··· 305 306 #: src/components/account-info.jsx:1336 306 307 #: src/components/shortcuts-settings.jsx:1059 307 308 #: src/components/status.jsx:1183 308 - #: src/components/status.jsx:3258 309 + #: src/components/status.jsx:3260 309 310 msgid "Copy" 310 311 msgstr "" 311 312 ··· 418 419 #: src/components/account-info.jsx:2020 419 420 #: src/components/account-info.jsx:2140 420 421 #: src/components/account-sheet.jsx:38 421 - #: src/components/compose.jsx:859 422 - #: src/components/compose.jsx:2580 423 - #: src/components/compose.jsx:3054 424 - #: src/components/compose.jsx:3263 425 - #: src/components/compose.jsx:3493 422 + #: src/components/compose.jsx:876 423 + #: src/components/compose.jsx:2652 424 + #: src/components/compose.jsx:3126 425 + #: src/components/compose.jsx:3335 426 + #: src/components/compose.jsx:3565 426 427 #: src/components/drafts.jsx:59 427 428 #: src/components/embed-modal.jsx:13 428 429 #: src/components/generic-accounts.jsx:143 ··· 435 436 #: src/components/shortcuts-settings.jsx:230 436 437 #: src/components/shortcuts-settings.jsx:583 437 438 #: src/components/shortcuts-settings.jsx:783 438 - #: src/components/status.jsx:2982 439 - #: src/components/status.jsx:3222 440 - #: src/components/status.jsx:3722 439 + #: src/components/status.jsx:2984 440 + #: src/components/status.jsx:3224 441 + #: src/components/status.jsx:3724 441 442 #: src/pages/accounts.jsx:37 442 443 #: src/pages/catchup.jsx:1581 443 444 #: src/pages/filters.jsx:224 444 445 #: src/pages/list.jsx:276 445 446 #: src/pages/notifications.jsx:915 447 + #: src/pages/scheduled-posts.jsx:257 446 448 #: src/pages/settings.jsx:78 447 449 #: src/pages/status.jsx:1301 448 450 msgid "Close" ··· 559 561 #: src/pages/followed-hashtags.jsx:41 560 562 #: src/pages/home.jsx:53 561 563 #: src/pages/notifications.jsx:560 564 + #: src/pages/scheduled-posts.jsx:72 562 565 msgid "Home" 563 566 msgstr "" 564 567 ··· 567 570 msgid "Compose" 568 571 msgstr "" 569 572 570 - #: src/components/compose.jsx:206 573 + #: src/components/compose.jsx:210 571 574 msgid "Add media" 572 575 msgstr "Add media" 573 576 574 - #: src/components/compose.jsx:207 577 + #: src/components/compose.jsx:211 575 578 msgid "Add custom emoji" 576 579 msgstr "" 577 580 578 - #: src/components/compose.jsx:208 581 + #: src/components/compose.jsx:212 579 582 msgid "Add GIF" 580 583 msgstr "Add GIF" 581 584 582 - #: src/components/compose.jsx:209 585 + #: src/components/compose.jsx:213 583 586 msgid "Add poll" 584 587 msgstr "" 585 588 586 - #: src/components/compose.jsx:402 589 + #: src/components/compose.jsx:214 590 + msgid "Schedule post" 591 + msgstr "Schedule post" 592 + 593 + #: src/components/compose.jsx:410 587 594 msgid "You have unsaved changes. Discard this post?" 588 595 msgstr "You have unsaved changes. Discard this post?" 589 596 590 597 #. placeholder {0}: unsupportedFiles.length 591 598 #. placeholder {1}: unsupportedFiles[0].name 592 599 #. placeholder {2}: lf.format( unsupportedFiles.map((f) => f.name), ) 593 - #: src/components/compose.jsx:630 600 + #: src/components/compose.jsx:639 594 601 msgid "{0, plural, one {File {1} is not supported.} other {Files {2} are not supported.}}" 595 602 msgstr "{0, plural, one {File {1} is not supported.} other {Files {2} are not supported.}}" 596 603 597 - #: src/components/compose.jsx:640 598 - #: src/components/compose.jsx:658 599 - #: src/components/compose.jsx:1674 600 - #: src/components/compose.jsx:1760 604 + #: src/components/compose.jsx:649 605 + #: src/components/compose.jsx:667 606 + #: src/components/compose.jsx:1746 607 + #: src/components/compose.jsx:1832 601 608 msgid "{maxMediaAttachments, plural, one {You can only attach up to 1 file.} other {You can only attach up to # files.}}" 602 609 msgstr "" 603 610 604 - #: src/components/compose.jsx:840 611 + #: src/components/compose.jsx:857 605 612 msgid "Pop out" 606 613 msgstr "Pop out" 607 614 608 - #: src/components/compose.jsx:847 615 + #: src/components/compose.jsx:864 609 616 msgid "Minimize" 610 617 msgstr "Minimize" 611 618 612 - #: src/components/compose.jsx:883 619 + #: src/components/compose.jsx:900 613 620 msgid "Looks like you closed the parent window." 614 621 msgstr "Looks like you closed the parent window." 615 622 616 - #: src/components/compose.jsx:890 623 + #: src/components/compose.jsx:907 617 624 msgid "Looks like you already have a compose field open in the parent window and currently publishing. Please wait for it to be done and try again later." 618 625 msgstr "Looks like you already have a compose field open in the parent window and currently publishing. Please wait for it to be done and try again later." 619 626 620 - #: src/components/compose.jsx:895 627 + #: src/components/compose.jsx:912 621 628 msgid "Looks like you already have a compose field open in the parent window. Popping in this window will discard the changes you made in the parent window. Continue?" 622 629 msgstr "Looks like you already have a compose field open in the parent window. Popping in this window will discard the changes you made in the parent window. Continue?" 623 630 624 - #: src/components/compose.jsx:937 631 + #: src/components/compose.jsx:955 625 632 msgid "Pop in" 626 633 msgstr "Pop in" 627 634 628 635 #. placeholder {0}: replyToStatus.account.acct || replyToStatus.account.username 629 636 #. placeholder {1}: rtf.format(-replyToStatusMonthsAgo, 'month') 630 - #: src/components/compose.jsx:947 637 + #: src/components/compose.jsx:965 631 638 msgid "Replying to @{0}’s post (<0>{1}</0>)" 632 639 msgstr "" 633 640 634 641 #. placeholder {0}: replyToStatus.account.acct || replyToStatus.account.username 635 - #: src/components/compose.jsx:957 642 + #: src/components/compose.jsx:975 636 643 msgid "Replying to @{0}’s post" 637 644 msgstr "" 638 645 639 - #: src/components/compose.jsx:970 646 + #: src/components/compose.jsx:988 640 647 msgid "Editing source post" 641 648 msgstr "" 642 649 643 - #: src/components/compose.jsx:1017 650 + #: src/components/compose.jsx:1041 644 651 msgid "Poll must have at least 2 options" 645 652 msgstr "Poll must have at least 2 options" 646 653 647 - #: src/components/compose.jsx:1021 654 + #: src/components/compose.jsx:1045 648 655 msgid "Some poll choices are empty" 649 656 msgstr "Some poll choices are empty" 650 657 651 - #: src/components/compose.jsx:1034 658 + #: src/components/compose.jsx:1058 652 659 msgid "Some media have no descriptions. Continue?" 653 660 msgstr "Some media have no descriptions. Continue?" 654 661 655 - #: src/components/compose.jsx:1086 662 + #: src/components/compose.jsx:1110 656 663 msgid "Attachment #{i} failed" 657 664 msgstr "Attachment #{i} failed" 658 665 659 - #: src/components/compose.jsx:1180 660 - #: src/components/status.jsx:2060 666 + #: src/components/compose.jsx:1206 667 + #: src/components/status.jsx:2062 661 668 #: src/components/timeline.jsx:989 662 669 msgid "Content warning" 663 670 msgstr "" 664 671 665 - #: src/components/compose.jsx:1196 672 + #: src/components/compose.jsx:1222 666 673 msgid "Content warning or sensitive media" 667 674 msgstr "Content warning or sensitive media" 668 675 669 - #: src/components/compose.jsx:1232 676 + #: src/components/compose.jsx:1258 670 677 #: src/components/status.jsx:93 671 678 #: src/pages/settings.jsx:306 672 679 msgid "Public" 673 680 msgstr "" 674 681 675 - #: src/components/compose.jsx:1237 676 - #: src/components/nav-menu.jsx:338 682 + #: src/components/compose.jsx:1263 683 + #: src/components/nav-menu.jsx:344 677 684 #: src/components/shortcuts-settings.jsx:165 678 685 #: src/components/status.jsx:94 679 686 msgid "Local" 680 687 msgstr "" 681 688 682 - #: src/components/compose.jsx:1241 689 + #: src/components/compose.jsx:1267 683 690 #: src/components/status.jsx:95 684 691 #: src/pages/settings.jsx:309 685 692 msgid "Unlisted" 686 693 msgstr "" 687 694 688 - #: src/components/compose.jsx:1244 695 + #: src/components/compose.jsx:1270 689 696 #: src/components/status.jsx:96 690 697 #: src/pages/settings.jsx:312 691 698 msgid "Followers only" 692 699 msgstr "" 693 700 694 - #: src/components/compose.jsx:1247 701 + #: src/components/compose.jsx:1273 695 702 #: src/components/status.jsx:97 696 - #: src/components/status.jsx:1938 703 + #: src/components/status.jsx:1940 697 704 msgid "Private mention" 698 705 msgstr "" 699 706 700 - #: src/components/compose.jsx:1256 707 + #: src/components/compose.jsx:1282 701 708 msgid "Post your reply" 702 709 msgstr "Post your reply" 703 710 704 - #: src/components/compose.jsx:1258 711 + #: src/components/compose.jsx:1284 705 712 msgid "Edit your post" 706 713 msgstr "Edit your post" 707 714 708 - #: src/components/compose.jsx:1259 715 + #: src/components/compose.jsx:1285 709 716 msgid "What are you doing?" 710 717 msgstr "What are you doing?" 711 718 712 - #: src/components/compose.jsx:1337 719 + #: src/components/compose.jsx:1363 713 720 msgid "Mark media as sensitive" 714 721 msgstr "" 715 722 716 - #: src/components/compose.jsx:1381 717 - #: src/components/compose.jsx:3112 723 + #: src/components/compose.jsx:1400 724 + msgid "Posting on <0/>" 725 + msgstr "Posting on <0/>" 726 + 727 + #: src/components/compose.jsx:1431 728 + #: src/components/compose.jsx:3184 718 729 #: src/components/shortcuts-settings.jsx:715 719 730 #: src/pages/list.jsx:362 720 731 msgid "Add" 721 732 msgstr "" 722 733 723 - #: src/components/compose.jsx:1555 734 + #: src/components/compose.jsx:1625 735 + msgid "Schedule" 736 + msgstr "Schedule" 737 + 738 + #: src/components/compose.jsx:1627 724 739 #: src/components/keyboard-shortcuts-help.jsx:154 725 740 #: src/components/status.jsx:948 726 741 #: src/components/status.jsx:1714 727 742 #: src/components/status.jsx:1715 728 - #: src/components/status.jsx:2383 743 + #: src/components/status.jsx:2385 729 744 msgid "Reply" 730 745 msgstr "" 731 746 732 - #: src/components/compose.jsx:1557 747 + #: src/components/compose.jsx:1629 733 748 msgid "Update" 734 749 msgstr "Update" 735 750 736 - #: src/components/compose.jsx:1558 751 + #: src/components/compose.jsx:1630 737 752 msgctxt "Submit button in composer" 738 753 msgid "Post" 739 754 msgstr "Post" 740 755 741 - #: src/components/compose.jsx:1686 756 + #: src/components/compose.jsx:1758 742 757 msgid "Downloading GIF…" 743 758 msgstr "Downloading GIF…" 744 759 745 - #: src/components/compose.jsx:1714 760 + #: src/components/compose.jsx:1786 746 761 msgid "Failed to download GIF" 747 762 msgstr "Failed to download GIF" 748 763 749 - #: src/components/compose.jsx:1884 750 - #: src/components/compose.jsx:1961 764 + #: src/components/compose.jsx:1956 765 + #: src/components/compose.jsx:2033 751 766 #: src/components/nav-menu.jsx:239 752 767 msgid "More…" 753 768 msgstr "" 754 769 755 - #: src/components/compose.jsx:2393 770 + #: src/components/compose.jsx:2465 756 771 msgid "Uploaded" 757 772 msgstr "" 758 773 759 - #: src/components/compose.jsx:2406 774 + #: src/components/compose.jsx:2478 760 775 msgid "Image description" 761 776 msgstr "Image description" 762 777 763 - #: src/components/compose.jsx:2407 778 + #: src/components/compose.jsx:2479 764 779 msgid "Video description" 765 780 msgstr "Video description" 766 781 767 - #: src/components/compose.jsx:2408 782 + #: src/components/compose.jsx:2480 768 783 msgid "Audio description" 769 784 msgstr "Audio description" 770 785 771 786 #. placeholder {0}: prettyBytes( imageSize, ) 772 787 #. placeholder {1}: prettyBytes(imageSizeLimit) 773 - #: src/components/compose.jsx:2444 788 + #: src/components/compose.jsx:2516 774 789 msgid "File size too large. Uploading might encounter issues. Try reduce the file size from {0} to {1} or lower." 775 790 msgstr "File size too large. Uploading might encounter issues. Try reduce the file size from {0} to {1} or lower." 776 791 ··· 778 793 #. placeholder {3}: i18n.number(height) 779 794 #. placeholder {4}: i18n.number(newWidth) 780 795 #. placeholder {5}: i18n.number( newHeight, ) 781 - #: src/components/compose.jsx:2456 796 + #: src/components/compose.jsx:2528 782 797 msgid "Dimension too large. Uploading might encounter issues. Try reduce dimension from {2}×{3}px to {4}×{5}px." 783 798 msgstr "Dimension too large. Uploading might encounter issues. Try reduce dimension from {2}×{3}px to {4}×{5}px." 784 799 785 800 #. placeholder {6}: prettyBytes( videoSize, ) 786 801 #. placeholder {7}: prettyBytes(videoSizeLimit) 787 - #: src/components/compose.jsx:2464 802 + #: src/components/compose.jsx:2536 788 803 msgid "File size too large. Uploading might encounter issues. Try reduce the file size from {6} to {7} or lower." 789 804 msgstr "File size too large. Uploading might encounter issues. Try reduce the file size from {6} to {7} or lower." 790 805 ··· 792 807 #. placeholder {9}: i18n.number(height) 793 808 #. placeholder {10}: i18n.number(newWidth) 794 809 #. placeholder {11}: i18n.number( newHeight, ) 795 - #: src/components/compose.jsx:2476 810 + #: src/components/compose.jsx:2548 796 811 msgid "Dimension too large. Uploading might encounter issues. Try reduce dimension from {8}×{9}px to {10}×{11}px." 797 812 msgstr "Dimension too large. Uploading might encounter issues. Try reduce dimension from {8}×{9}px to {10}×{11}px." 798 813 799 - #: src/components/compose.jsx:2484 814 + #: src/components/compose.jsx:2556 800 815 msgid "Frame rate too high. Uploading might encounter issues." 801 816 msgstr "Frame rate too high. Uploading might encounter issues." 802 817 803 - #: src/components/compose.jsx:2544 804 - #: src/components/compose.jsx:2794 818 + #: src/components/compose.jsx:2616 819 + #: src/components/compose.jsx:2866 805 820 #: src/components/shortcuts-settings.jsx:726 806 821 #: src/pages/catchup.jsx:1074 807 822 #: src/pages/filters.jsx:412 808 823 msgid "Remove" 809 824 msgstr "" 810 825 811 - #: src/components/compose.jsx:2561 826 + #: src/components/compose.jsx:2633 812 827 #: src/compose.jsx:84 813 828 msgid "Error" 814 829 msgstr "" 815 830 816 - #: src/components/compose.jsx:2586 831 + #: src/components/compose.jsx:2658 817 832 msgid "Edit image description" 818 833 msgstr "Edit image description" 819 834 820 - #: src/components/compose.jsx:2587 835 + #: src/components/compose.jsx:2659 821 836 msgid "Edit video description" 822 837 msgstr "Edit video description" 823 838 824 - #: src/components/compose.jsx:2588 839 + #: src/components/compose.jsx:2660 825 840 msgid "Edit audio description" 826 841 msgstr "Edit audio description" 827 842 828 - #: src/components/compose.jsx:2633 829 - #: src/components/compose.jsx:2682 843 + #: src/components/compose.jsx:2705 844 + #: src/components/compose.jsx:2754 830 845 msgid "Generating description. Please wait…" 831 846 msgstr "Generating description. Please wait…" 832 847 833 848 #. placeholder {12}: e.message 834 - #: src/components/compose.jsx:2653 849 + #: src/components/compose.jsx:2725 835 850 msgid "Failed to generate description: {12}" 836 851 msgstr "Failed to generate description: {12}" 837 852 838 - #: src/components/compose.jsx:2654 853 + #: src/components/compose.jsx:2726 839 854 msgid "Failed to generate description" 840 855 msgstr "Failed to generate description" 841 856 842 - #: src/components/compose.jsx:2666 843 - #: src/components/compose.jsx:2672 844 - #: src/components/compose.jsx:2718 857 + #: src/components/compose.jsx:2738 858 + #: src/components/compose.jsx:2744 859 + #: src/components/compose.jsx:2790 845 860 msgid "Generate description…" 846 861 msgstr "" 847 862 848 863 #. placeholder {13}: e?.message ? `: ${e.message}` : '' 849 - #: src/components/compose.jsx:2705 864 + #: src/components/compose.jsx:2777 850 865 msgid "Failed to generate description{13}" 851 866 msgstr "Failed to generate description{13}" 852 867 853 868 #. placeholder {0}: localeCode2Text(lang) 854 - #: src/components/compose.jsx:2720 869 + #: src/components/compose.jsx:2792 855 870 msgid "({0}) <0>— experimental</0>" 856 871 msgstr "" 857 872 858 - #: src/components/compose.jsx:2739 873 + #: src/components/compose.jsx:2811 859 874 msgid "Done" 860 875 msgstr "" 861 876 862 877 #. placeholder {0}: i + 1 863 - #: src/components/compose.jsx:2775 878 + #: src/components/compose.jsx:2847 864 879 msgid "Choice {0}" 865 880 msgstr "Choice {0}" 866 881 867 - #: src/components/compose.jsx:2822 882 + #: src/components/compose.jsx:2894 868 883 msgid "Multiple choices" 869 884 msgstr "" 870 885 871 - #: src/components/compose.jsx:2825 886 + #: src/components/compose.jsx:2897 872 887 msgid "Duration" 873 888 msgstr "" 874 889 875 - #: src/components/compose.jsx:2856 890 + #: src/components/compose.jsx:2928 876 891 msgid "Remove poll" 877 892 msgstr "" 878 893 879 - #: src/components/compose.jsx:3071 894 + #: src/components/compose.jsx:3143 880 895 msgid "Search accounts" 881 896 msgstr "Search accounts" 882 897 883 - #: src/components/compose.jsx:3125 898 + #: src/components/compose.jsx:3197 884 899 #: src/components/generic-accounts.jsx:228 885 900 msgid "Error loading accounts" 886 901 msgstr "" 887 902 888 - #: src/components/compose.jsx:3269 903 + #: src/components/compose.jsx:3341 889 904 msgid "Custom emojis" 890 905 msgstr "" 891 906 892 - #: src/components/compose.jsx:3289 907 + #: src/components/compose.jsx:3361 893 908 msgid "Search emoji" 894 909 msgstr "Search emoji" 895 910 896 - #: src/components/compose.jsx:3320 911 + #: src/components/compose.jsx:3392 897 912 msgid "Error loading custom emojis" 898 913 msgstr "" 899 914 900 - #: src/components/compose.jsx:3331 915 + #: src/components/compose.jsx:3403 901 916 msgid "Recently used" 902 917 msgstr "Recently used" 903 918 904 - #: src/components/compose.jsx:3332 919 + #: src/components/compose.jsx:3404 905 920 msgid "Others" 906 921 msgstr "Others" 907 922 908 923 #. placeholder {0}: i18n.number(emojis.length - max) 909 - #: src/components/compose.jsx:3370 924 + #: src/components/compose.jsx:3442 910 925 msgid "{0} more…" 911 926 msgstr "" 912 927 913 - #: src/components/compose.jsx:3508 928 + #: src/components/compose.jsx:3580 914 929 msgid "Search GIFs" 915 930 msgstr "Search GIFs" 916 931 917 - #: src/components/compose.jsx:3523 932 + #: src/components/compose.jsx:3595 918 933 msgid "Powered by GIPHY" 919 934 msgstr "Powered by GIPHY" 920 935 921 - #: src/components/compose.jsx:3531 936 + #: src/components/compose.jsx:3603 922 937 msgid "Type to search GIFs" 923 938 msgstr "" 924 939 925 - #: src/components/compose.jsx:3629 940 + #: src/components/compose.jsx:3701 926 941 #: src/components/media-modal.jsx:464 927 942 #: src/components/timeline.jsx:893 928 943 msgid "Previous" 929 944 msgstr "" 930 945 931 - #: src/components/compose.jsx:3647 946 + #: src/components/compose.jsx:3719 932 947 #: src/components/media-modal.jsx:483 933 948 #: src/components/timeline.jsx:910 934 949 msgid "Next" 935 950 msgstr "" 936 951 937 - #: src/components/compose.jsx:3664 952 + #: src/components/compose.jsx:3736 938 953 msgid "Error loading GIFs" 939 954 msgstr "" 940 955 ··· 959 974 #: src/components/list-add-edit.jsx:186 960 975 #: src/components/status.jsx:1349 961 976 #: src/pages/filters.jsx:587 977 + #: src/pages/scheduled-posts.jsx:367 962 978 msgid "Delete…" 963 979 msgstr "" 964 980 ··· 1042 1058 msgstr "" 1043 1059 1044 1060 #: src/components/keyboard-shortcuts-help.jsx:46 1045 - #: src/components/nav-menu.jsx:357 1061 + #: src/components/nav-menu.jsx:363 1046 1062 #: src/pages/catchup.jsx:1619 1047 1063 msgid "Keyboard shortcuts" 1048 1064 msgstr "" ··· 1139 1155 msgstr "" 1140 1156 1141 1157 #: src/components/keyboard-shortcuts-help.jsx:150 1142 - #: src/components/nav-menu.jsx:326 1158 + #: src/components/nav-menu.jsx:332 1143 1159 #: src/components/search-form.jsx:73 1144 1160 #: src/components/shortcuts-settings.jsx:52 1145 1161 #: src/components/shortcuts-settings.jsx:179 ··· 1166 1182 1167 1183 #: src/components/keyboard-shortcuts-help.jsx:175 1168 1184 #: src/components/status.jsx:956 1169 - #: src/components/status.jsx:2410 1170 - #: src/components/status.jsx:2433 1171 - #: src/components/status.jsx:2434 1185 + #: src/components/status.jsx:2412 1186 + #: src/components/status.jsx:2435 1187 + #: src/components/status.jsx:2436 1172 1188 msgid "Boost" 1173 1189 msgstr "" 1174 1190 ··· 1178 1194 1179 1195 #: src/components/keyboard-shortcuts-help.jsx:183 1180 1196 #: src/components/status.jsx:1019 1181 - #: src/components/status.jsx:2458 1182 - #: src/components/status.jsx:2459 1197 + #: src/components/status.jsx:2460 1198 + #: src/components/status.jsx:2461 1183 1199 msgid "Bookmark" 1184 1200 msgstr "" 1185 1201 ··· 1283 1299 msgstr "" 1284 1300 1285 1301 #: src/components/media-post.jsx:134 1286 - #: src/components/status.jsx:3552 1287 - #: src/components/status.jsx:3648 1288 - #: src/components/status.jsx:3726 1302 + #: src/components/status.jsx:3554 1303 + #: src/components/status.jsx:3650 1304 + #: src/components/status.jsx:3728 1289 1305 #: src/components/timeline.jsx:978 1290 1306 #: src/pages/catchup.jsx:75 1291 1307 #: src/pages/catchup.jsx:1877 ··· 1296 1312 msgid "Open file" 1297 1313 msgstr "Open file" 1298 1314 1299 - #: src/components/modals.jsx:73 1315 + #: src/components/modals.jsx:75 1316 + msgid "Post scheduled" 1317 + msgstr "Post scheduled" 1318 + 1319 + #: src/components/modals.jsx:76 1300 1320 msgid "Post published. Check it out." 1301 1321 msgstr "" 1302 1322 1303 - #: src/components/modals.jsx:74 1323 + #: src/components/modals.jsx:78 1324 + msgid "Reply scheduled" 1325 + msgstr "Reply scheduled" 1326 + 1327 + #: src/components/modals.jsx:79 1304 1328 msgid "Reply posted. Check it out." 1305 1329 msgstr "" 1306 1330 1307 - #: src/components/modals.jsx:75 1331 + #: src/components/modals.jsx:80 1308 1332 msgid "Post updated. Check it out." 1309 1333 msgstr "" 1310 1334 ··· 1388 1412 msgid "Followed Hashtags" 1389 1413 msgstr "" 1390 1414 1391 - #: src/components/nav-menu.jsx:262 1415 + #: src/components/nav-menu.jsx:260 1416 + #: src/pages/scheduled-posts.jsx:31 1417 + #: src/pages/scheduled-posts.jsx:76 1418 + msgid "Scheduled Posts" 1419 + msgstr "Scheduled Posts" 1420 + 1421 + #: src/components/nav-menu.jsx:268 1392 1422 #: src/pages/account-statuses.jsx:326 1393 1423 #: src/pages/filters.jsx:54 1394 1424 #: src/pages/filters.jsx:93 ··· 1396 1426 msgid "Filters" 1397 1427 msgstr "" 1398 1428 1399 - #: src/components/nav-menu.jsx:270 1429 + #: src/components/nav-menu.jsx:276 1400 1430 msgid "Muted users" 1401 1431 msgstr "" 1402 1432 1403 - #: src/components/nav-menu.jsx:278 1433 + #: src/components/nav-menu.jsx:284 1404 1434 msgid "Muted users…" 1405 1435 msgstr "" 1406 1436 1407 - #: src/components/nav-menu.jsx:285 1437 + #: src/components/nav-menu.jsx:291 1408 1438 msgid "Blocked users" 1409 1439 msgstr "" 1410 1440 1411 - #: src/components/nav-menu.jsx:293 1441 + #: src/components/nav-menu.jsx:299 1412 1442 msgid "Blocked users…" 1413 1443 msgstr "" 1414 1444 1415 - #: src/components/nav-menu.jsx:305 1445 + #: src/components/nav-menu.jsx:311 1416 1446 msgid "Accounts…" 1417 1447 msgstr "" 1418 1448 1419 - #: src/components/nav-menu.jsx:315 1449 + #: src/components/nav-menu.jsx:321 1420 1450 #: src/pages/login.jsx:27 1421 1451 #: src/pages/login.jsx:190 1422 1452 #: src/pages/status.jsx:837 ··· 1424 1454 msgid "Log in" 1425 1455 msgstr "" 1426 1456 1427 - #: src/components/nav-menu.jsx:332 1457 + #: src/components/nav-menu.jsx:338 1428 1458 #: src/components/shortcuts-settings.jsx:57 1429 1459 #: src/components/shortcuts-settings.jsx:172 1430 1460 #: src/pages/trending.jsx:442 1431 1461 msgid "Trending" 1432 1462 msgstr "" 1433 1463 1434 - #: src/components/nav-menu.jsx:344 1464 + #: src/components/nav-menu.jsx:350 1435 1465 #: src/components/shortcuts-settings.jsx:165 1436 1466 msgid "Federated" 1437 1467 msgstr "" 1438 1468 1439 - #: src/components/nav-menu.jsx:367 1469 + #: src/components/nav-menu.jsx:373 1440 1470 msgid "Shortcuts / Columns…" 1441 1471 msgstr "" 1442 1472 1443 - #: src/components/nav-menu.jsx:377 1444 - #: src/components/nav-menu.jsx:391 1473 + #: src/components/nav-menu.jsx:383 1474 + #: src/components/nav-menu.jsx:397 1445 1475 msgid "Settings…" 1446 1476 msgstr "" 1447 1477 1448 - #: src/components/nav-menu.jsx:421 1449 - #: src/components/nav-menu.jsx:448 1478 + #: src/components/nav-menu.jsx:427 1479 + #: src/components/nav-menu.jsx:454 1450 1480 #: src/components/shortcuts-settings.jsx:50 1451 1481 #: src/components/shortcuts-settings.jsx:158 1452 1482 #: src/pages/list.jsx:127 ··· 1455 1485 msgid "Lists" 1456 1486 msgstr "" 1457 1487 1458 - #: src/components/nav-menu.jsx:429 1488 + #: src/components/nav-menu.jsx:435 1459 1489 #: src/components/shortcuts.jsx:215 1460 1490 #: src/pages/list.jsx:134 1461 1491 msgid "All Lists" ··· 1640 1670 1641 1671 #: src/components/poll.jsx:208 1642 1672 #: src/components/poll.jsx:210 1673 + #: src/pages/scheduled-posts.jsx:98 1643 1674 #: src/pages/status.jsx:1203 1644 1675 #: src/pages/status.jsx:1226 1645 1676 msgid "Refresh" ··· 1654 1685 #. placeholder {1}: shortenNumber(votesCount) 1655 1686 #: src/components/poll.jsx:231 1656 1687 msgid "{votesCount, plural, one {<0>{0}</0> vote} other {<1>{1}</1> votes}}" 1657 - msgstr "" 1688 + msgstr "{votesCount, plural, one {<0>{0}</0> vote} other {<1>{1}</1> votes}}" 1658 1689 1659 1690 #. placeholder {0}: shortenNumber(votersCount) 1660 1691 #. placeholder {1}: shortenNumber(votersCount) ··· 1680 1711 1681 1712 #. Relative time in seconds, as short as possible 1682 1713 #. placeholder {0}: seconds < 1 ? 1 : Math.floor(seconds) 1683 - #: src/components/relative-time.jsx:57 1714 + #: src/components/relative-time.jsx:59 1684 1715 msgid "{0}s" 1685 1716 msgstr "" 1686 1717 1687 1718 #. Relative time in minutes, as short as possible 1688 1719 #. placeholder {0}: Math.floor(seconds / minute) 1689 - #: src/components/relative-time.jsx:62 1720 + #: src/components/relative-time.jsx:64 1690 1721 msgid "{0}m" 1691 1722 msgstr "" 1692 1723 1693 1724 #. Relative time in hours, as short as possible 1694 1725 #. placeholder {0}: Math.floor(seconds / hour) 1695 - #: src/components/relative-time.jsx:67 1726 + #: src/components/relative-time.jsx:69 1696 1727 msgid "{0}h" 1697 1728 msgstr "" 1698 1729 ··· 2155 2186 2156 2187 #: src/components/status.jsx:956 2157 2188 #: src/components/status.jsx:996 2158 - #: src/components/status.jsx:2410 2159 - #: src/components/status.jsx:2433 2189 + #: src/components/status.jsx:2412 2190 + #: src/components/status.jsx:2435 2160 2191 msgid "Unboost" 2161 2192 msgstr "" 2162 2193 2163 2194 #: src/components/status.jsx:972 2164 - #: src/components/status.jsx:2425 2195 + #: src/components/status.jsx:2427 2165 2196 msgid "Quote" 2166 2197 msgstr "" 2167 2198 ··· 2181 2212 2182 2213 #: src/components/status.jsx:1009 2183 2214 #: src/components/status.jsx:1724 2184 - #: src/components/status.jsx:2446 2215 + #: src/components/status.jsx:2448 2185 2216 msgid "Unlike" 2186 2217 msgstr "" 2187 2218 2188 2219 #: src/components/status.jsx:1010 2189 2220 #: src/components/status.jsx:1724 2190 2221 #: src/components/status.jsx:1725 2191 - #: src/components/status.jsx:2446 2192 - #: src/components/status.jsx:2447 2222 + #: src/components/status.jsx:2448 2223 + #: src/components/status.jsx:2449 2193 2224 msgid "Like" 2194 2225 msgstr "" 2195 2226 2196 2227 #: src/components/status.jsx:1019 2197 - #: src/components/status.jsx:2458 2228 + #: src/components/status.jsx:2460 2198 2229 msgid "Unbookmark" 2199 2230 msgstr "" 2200 2231 ··· 2212 2243 msgstr "" 2213 2244 2214 2245 #: src/components/status.jsx:1218 2215 - #: src/components/status.jsx:3227 2246 + #: src/components/status.jsx:3229 2216 2247 msgid "Embed post" 2217 2248 msgstr "" 2218 2249 ··· 2292 2323 2293 2324 #: src/components/status.jsx:1725 2294 2325 #: src/components/status.jsx:1761 2295 - #: src/components/status.jsx:2447 2326 + #: src/components/status.jsx:2449 2296 2327 msgid "Liked" 2297 2328 msgstr "" 2298 2329 2299 2330 #: src/components/status.jsx:1758 2300 - #: src/components/status.jsx:2434 2331 + #: src/components/status.jsx:2436 2301 2332 msgid "Boosted" 2302 2333 msgstr "" 2303 2334 2304 2335 #: src/components/status.jsx:1768 2305 - #: src/components/status.jsx:2459 2336 + #: src/components/status.jsx:2461 2306 2337 msgid "Bookmarked" 2307 2338 msgstr "" 2308 2339 ··· 2310 2341 msgid "Pinned" 2311 2342 msgstr "" 2312 2343 2313 - #: src/components/status.jsx:1817 2314 - #: src/components/status.jsx:2273 2344 + #: src/components/status.jsx:1818 2345 + #: src/components/status.jsx:2275 2315 2346 msgid "Deleted" 2316 2347 msgstr "" 2317 2348 2318 - #: src/components/status.jsx:1858 2349 + #: src/components/status.jsx:1859 2319 2350 msgid "{repliesCount, plural, one {# reply} other {# replies}}" 2320 2351 msgstr "" 2321 2352 2322 2353 #. placeholder {0}: snapStates.statusThreadNumber[sKey] ? ` ${snapStates.statusThreadNumber[sKey]}/X` : '' 2323 - #: src/components/status.jsx:1947 2354 + #: src/components/status.jsx:1949 2324 2355 msgid "Thread{0}" 2325 2356 msgstr "" 2326 2357 2327 - #: src/components/status.jsx:2023 2328 - #: src/components/status.jsx:2085 2329 - #: src/components/status.jsx:2170 2358 + #: src/components/status.jsx:2025 2359 + #: src/components/status.jsx:2087 2360 + #: src/components/status.jsx:2172 2330 2361 msgid "Show less" 2331 2362 msgstr "" 2332 2363 2333 - #: src/components/status.jsx:2023 2334 - #: src/components/status.jsx:2085 2364 + #: src/components/status.jsx:2025 2365 + #: src/components/status.jsx:2087 2335 2366 msgid "Show content" 2336 2367 msgstr "" 2337 2368 2338 - #: src/components/status.jsx:2170 2369 + #: src/components/status.jsx:2172 2339 2370 msgid "Show media" 2340 2371 msgstr "" 2341 2372 2342 - #: src/components/status.jsx:2307 2373 + #: src/components/status.jsx:2309 2343 2374 msgid "Edited" 2344 2375 msgstr "" 2345 2376 2346 - #: src/components/status.jsx:2384 2377 + #: src/components/status.jsx:2386 2347 2378 msgid "Comments" 2348 2379 msgstr "" 2349 2380 2350 2381 #. More from [Author] 2351 - #: src/components/status.jsx:2685 2382 + #: src/components/status.jsx:2687 2352 2383 msgid "More from <0/>" 2353 2384 msgstr "More from <0/>" 2354 2385 2355 - #: src/components/status.jsx:2987 2386 + #: src/components/status.jsx:2989 2356 2387 msgid "Edit History" 2357 2388 msgstr "" 2358 2389 2359 - #: src/components/status.jsx:2991 2390 + #: src/components/status.jsx:2993 2360 2391 msgid "Failed to load history" 2361 2392 msgstr "" 2362 2393 2363 - #: src/components/status.jsx:2996 2394 + #: src/components/status.jsx:2998 2364 2395 #: src/pages/annual-report.jsx:45 2365 2396 msgid "Loading…" 2366 2397 msgstr "" 2367 2398 2368 - #: src/components/status.jsx:3232 2399 + #: src/components/status.jsx:3234 2369 2400 msgid "HTML Code" 2370 2401 msgstr "" 2371 2402 2372 - #: src/components/status.jsx:3249 2403 + #: src/components/status.jsx:3251 2373 2404 msgid "HTML code copied" 2374 2405 msgstr "" 2375 2406 2376 - #: src/components/status.jsx:3252 2407 + #: src/components/status.jsx:3254 2377 2408 msgid "Unable to copy HTML code" 2378 2409 msgstr "" 2379 2410 2380 - #: src/components/status.jsx:3264 2411 + #: src/components/status.jsx:3266 2381 2412 msgid "Media attachments:" 2382 2413 msgstr "" 2383 2414 2384 - #: src/components/status.jsx:3286 2415 + #: src/components/status.jsx:3288 2385 2416 msgid "Account Emojis:" 2386 2417 msgstr "" 2387 2418 2388 - #: src/components/status.jsx:3317 2389 - #: src/components/status.jsx:3362 2419 + #: src/components/status.jsx:3319 2420 + #: src/components/status.jsx:3364 2390 2421 msgid "static URL" 2391 2422 msgstr "" 2392 2423 2393 - #: src/components/status.jsx:3331 2424 + #: src/components/status.jsx:3333 2394 2425 msgid "Emojis:" 2395 2426 msgstr "" 2396 2427 2397 - #: src/components/status.jsx:3376 2428 + #: src/components/status.jsx:3378 2398 2429 msgid "Notes:" 2399 2430 msgstr "" 2400 2431 2401 - #: src/components/status.jsx:3380 2432 + #: src/components/status.jsx:3382 2402 2433 msgid "This is static, unstyled and scriptless. You may need to apply your own styles and edit as needed." 2403 2434 msgstr "" 2404 2435 2405 - #: src/components/status.jsx:3386 2436 + #: src/components/status.jsx:3388 2406 2437 msgid "Polls are not interactive, becomes a list with vote counts." 2407 2438 msgstr "" 2408 2439 2409 - #: src/components/status.jsx:3391 2440 + #: src/components/status.jsx:3393 2410 2441 msgid "Media attachments can be images, videos, audios or any file types." 2411 2442 msgstr "" 2412 2443 2413 - #: src/components/status.jsx:3397 2444 + #: src/components/status.jsx:3399 2414 2445 msgid "Post could be edited or deleted later." 2415 2446 msgstr "" 2416 2447 2417 - #: src/components/status.jsx:3403 2448 + #: src/components/status.jsx:3405 2418 2449 msgid "Preview" 2419 2450 msgstr "" 2420 2451 2421 - #: src/components/status.jsx:3412 2452 + #: src/components/status.jsx:3414 2422 2453 msgid "Note: This preview is lightly styled." 2423 2454 msgstr "" 2424 2455 2425 2456 #. [Name] [Visibility icon] boosted 2426 - #: src/components/status.jsx:3656 2457 + #: src/components/status.jsx:3658 2427 2458 msgid "<0/> <1/> boosted" 2428 2459 msgstr "" 2429 2460 ··· 3424 3455 #: src/pages/public.jsx:131 3425 3456 msgid "Switch to Local" 3426 3457 msgstr "Switch to Local" 3458 + 3459 + #: src/pages/scheduled-posts.jsx:108 3460 + msgid "No scheduled posts." 3461 + msgstr "No scheduled posts." 3462 + 3463 + #. Scheduled [in 1 day] ([Thu, Feb 27, 6:30:00 PM]) 3464 + #. placeholder {0}: niceDateTime(scheduledAt, { formatOpts: { weekday: 'short', second: 'numeric', }, }) 3465 + #: src/pages/scheduled-posts.jsx:205 3466 + msgid "Scheduled <0><1/></0> <2>({0})</2>" 3467 + msgstr "Scheduled <0><1/></0> <2>({0})</2>" 3468 + 3469 + #. Scheduled [in 1 day] 3470 + #: src/pages/scheduled-posts.jsx:261 3471 + msgid "Scheduled <0><1/></0>" 3472 + msgstr "Scheduled <0><1/></0>" 3473 + 3474 + #: src/pages/scheduled-posts.jsx:306 3475 + msgid "Scheduled post rescheduled" 3476 + msgstr "Scheduled post rescheduled" 3477 + 3478 + #: src/pages/scheduled-posts.jsx:313 3479 + msgid "Failed to reschedule post" 3480 + msgstr "Failed to reschedule post" 3481 + 3482 + #: src/pages/scheduled-posts.jsx:336 3483 + msgid "Reschedule" 3484 + msgstr "Reschedule" 3485 + 3486 + #: src/pages/scheduled-posts.jsx:342 3487 + msgid "Delete scheduled post?" 3488 + msgstr "Delete scheduled post?" 3489 + 3490 + #: src/pages/scheduled-posts.jsx:350 3491 + msgid "Scheduled post deleted" 3492 + msgstr "Scheduled post deleted" 3493 + 3494 + #: src/pages/scheduled-posts.jsx:357 3495 + msgid "Failed to delete scheduled post" 3496 + msgstr "Failed to delete scheduled post" 3427 3497 3428 3498 #: src/pages/search.jsx:50 3429 3499 msgid "Search: {q} (Posts)"
+132
src/pages/scheduled-posts.css
··· 1 + #scheduled-posts-page { 2 + .posts-list { 3 + list-style: none; 4 + padding: 0; 5 + margin: 0; 6 + 7 + li { 8 + > button { 9 + text-align: start; 10 + color: inherit; 11 + padding: 16px; 12 + display: flex; 13 + flex-direction: column; 14 + border-bottom: 1px solid var(--outline-color); 15 + gap: 8px; 16 + 17 + > .status { 18 + padding: 8px; 19 + pointer-events: none; 20 + background-color: var(--bg-blur-color); 21 + border-radius: 8px; 22 + border: 1px solid var(--outline-color); 23 + font-size: 80%; 24 + max-height: 160px; 25 + overflow: hidden; 26 + mask-image: linear-gradient( 27 + to bottom, 28 + black calc(160px - 16px), 29 + transparent 30 + ); 31 + 32 + .media-container .media { 33 + width: 80px !important; 34 + height: 80px !important; 35 + } 36 + } 37 + } 38 + 39 + .post-schedule-meta { 40 + display: flex; 41 + align-items: center; 42 + gap: 4px; 43 + 44 + &.post-schedule-time { 45 + .icon, 46 + b { 47 + color: var(--red-text-color); 48 + } 49 + } 50 + &.post-schedule-month b { 51 + opacity: 0.8; 52 + } 53 + } 54 + } 55 + 56 + h2 { 57 + font-weight: 500; 58 + margin: 0; 59 + padding: 0; 60 + font-size: 1em; 61 + } 62 + } 63 + } 64 + 65 + #scheduled-post-sheet { 66 + header h2 { 67 + font-weight: normal; 68 + 69 + small { 70 + font-size: var(--text-size); 71 + } 72 + } 73 + main > .status { 74 + background-color: var(--bg-blur-color); 75 + border-radius: 8px; 76 + border: 1px solid var(--outline-color); 77 + overflow: auto; 78 + max-height: 50svh; 79 + 80 + .media-container .media { 81 + width: 80px !important; 82 + height: 80px !important; 83 + } 84 + } 85 + .status-reply { 86 + border-radius: 16px 16px 0 0; 87 + max-height: 160px; 88 + background-color: var(--bg-color); 89 + margin: 0 12px; 90 + border: 1px solid var(--outline-color); 91 + border-bottom: 0; 92 + -webkit-animation: appear-up 1sease-in-out; 93 + animation: appear-up 1sease-in-out; 94 + overflow: auto; 95 + 96 + > .status { 97 + font-size: 90%; 98 + } 99 + } 100 + footer { 101 + display: flex; 102 + flex-direction: column; 103 + gap: 8px; 104 + padding: 8px 0; 105 + 106 + .row { 107 + display: flex; 108 + gap: 8px; 109 + justify-content: space-between; 110 + align-items: center; 111 + } 112 + 113 + input[type='datetime-local'] { 114 + max-width: calc(100vw - 32px); 115 + min-width: 0; /* Adding a min-width to prevent overflow */ 116 + 117 + &:user-invalid { 118 + border-color: var(--red-color); 119 + } 120 + 121 + @supports not selector(:user-invalid) { 122 + &:invalid { 123 + border-color: var(--red-color); 124 + } 125 + } 126 + } 127 + 128 + .grow { 129 + flex-grow: 1; 130 + } 131 + } 132 + }
+376
src/pages/scheduled-posts.jsx
··· 1 + import './scheduled-posts.css'; 2 + 3 + import { Trans, useLingui } from '@lingui/react/macro'; 4 + import { MenuItem } from '@szhsin/react-menu'; 5 + import { useEffect, useMemo, useReducer, useState } from 'preact/hooks'; 6 + import { useSnapshot } from 'valtio'; 7 + 8 + import Icon from '../components/icon'; 9 + import Link from '../components/link'; 10 + import Loader from '../components/loader'; 11 + import MenuConfirm from '../components/menu-confirm'; 12 + import Menu2 from '../components/menu2'; 13 + import Modal from '../components/modal'; 14 + import NavMenu from '../components/nav-menu'; 15 + import RelativeTime from '../components/relative-time'; 16 + import ScheduledAtField, { 17 + getLocalTimezoneName, 18 + } from '../components/ScheduledAtField'; 19 + import Status from '../components/status'; 20 + import { api } from '../utils/api'; 21 + import niceDateTime from '../utils/nice-date-time'; 22 + import showToast from '../utils/show-toast'; 23 + import states from '../utils/states'; 24 + import useTitle from '../utils/useTitle'; 25 + 26 + const LIMIT = 40; 27 + 28 + export default function ScheduledPosts() { 29 + const { t } = useLingui(); 30 + const snapStates = useSnapshot(states); 31 + useTitle(t`Scheduled Posts`, '/sp'); 32 + const { masto } = api(); 33 + const [scheduledPosts, setScheduledPosts] = useState([]); 34 + const [uiState, setUIState] = useState('default'); 35 + const [reloadCount, reload] = useReducer((c) => c + 1, 0); 36 + const [showScheduledPostModal, setShowScheduledPostModal] = useState(false); 37 + 38 + useEffect(reload, [snapStates.reloadScheduledPosts]); 39 + 40 + useEffect(() => { 41 + setUIState('loading'); 42 + (async () => { 43 + try { 44 + const postsIterator = masto.v1.scheduledStatuses.list({ limit: LIMIT }); 45 + const allPosts = []; 46 + let posts; 47 + do { 48 + const result = await postsIterator.next(); 49 + posts = result.value; 50 + if (posts?.length) { 51 + allPosts.push(...posts); 52 + } 53 + } while (posts?.length); 54 + setScheduledPosts(allPosts); 55 + } catch (e) { 56 + console.error(e); 57 + setUIState('error'); 58 + } finally { 59 + setUIState('default'); 60 + } 61 + })(); 62 + }, [reloadCount]); 63 + 64 + return ( 65 + <div id="scheduled-posts-page" class="deck-container" tabIndex="-1"> 66 + <div class="timeline-deck deck"> 67 + <header> 68 + <div class="header-grid"> 69 + <div class="header-side"> 70 + <NavMenu /> 71 + <Link to="/" class="button plain"> 72 + <Icon icon="home" size="l" alt={t`Home`} /> 73 + </Link> 74 + </div> 75 + <h1> 76 + <Trans>Scheduled Posts</Trans> 77 + </h1> 78 + <div class="header-side"> 79 + <Menu2 80 + portal 81 + setDownOverflow 82 + overflow="auto" 83 + viewScroll="close" 84 + position="anchor" 85 + menuButton={ 86 + <button type="button" class="plain"> 87 + <Icon icon="more" size="l" alt={t`More`} /> 88 + </button> 89 + } 90 + > 91 + <MenuItem 92 + onClick={() => { 93 + reload(); 94 + }} 95 + > 96 + <Icon icon="refresh" size="l" /> 97 + <span> 98 + <Trans>Refresh</Trans> 99 + </span> 100 + </MenuItem> 101 + </Menu2> 102 + </div> 103 + </div> 104 + </header> 105 + <main> 106 + {!scheduledPosts.length ? ( 107 + <p class="ui-state"> 108 + {uiState === 'loading' ? <Loader /> : t`No scheduled posts.`} 109 + </p> 110 + ) : ( 111 + <ul class="posts-list"> 112 + {scheduledPosts.map((post) => { 113 + const { id, params, scheduledAt, mediaAttachments } = post; 114 + const { 115 + inReplyToId, 116 + language, 117 + poll, 118 + sensitive, 119 + spoilerText, 120 + text, 121 + visibility, 122 + } = params; 123 + const status = { 124 + // account: account.info, 125 + id, 126 + inReplyToId, 127 + language, 128 + mediaAttachments, 129 + poll: poll 130 + ? { 131 + ...poll, 132 + expiresAt: new Date(Date.now() + poll.expiresIn * 1000), 133 + options: poll.options.map((option) => ({ 134 + title: option, 135 + votesCount: 0, 136 + })), 137 + } 138 + : undefined, 139 + sensitive, 140 + spoilerText, 141 + text, 142 + visibility, 143 + content: `<p>${text}</p>`, 144 + // createdAt: scheduledAt, 145 + }; 146 + 147 + return ( 148 + <li key={id}> 149 + <ScheduledPostPreview 150 + status={status} 151 + scheduledAt={scheduledAt} 152 + onClick={() => { 153 + setShowScheduledPostModal({ 154 + post: status, 155 + scheduledAt: new Date(scheduledAt), 156 + }); 157 + }} 158 + /> 159 + </li> 160 + ); 161 + })} 162 + </ul> 163 + )} 164 + {showScheduledPostModal && ( 165 + <Modal 166 + onClick={(e) => { 167 + if (e.target === e.currentTarget) { 168 + setShowScheduledPostModal(false); 169 + } 170 + }} 171 + > 172 + <ScheduledPostEdit 173 + post={showScheduledPostModal.post} 174 + scheduledAt={showScheduledPostModal.scheduledAt} 175 + onClose={() => setShowScheduledPostModal(false)} 176 + /> 177 + </Modal> 178 + )} 179 + </main> 180 + </div> 181 + </div> 182 + ); 183 + } 184 + 185 + function ScheduledPostPreview({ status, scheduledAt, onClick }) { 186 + // Look at scheduledAt, if it's months away, ICON = 'month'. If it's days away, ICON = 'day', else ICON = 'time' 187 + const icon = useMemo(() => { 188 + const hours = 189 + (new Date(scheduledAt).getTime() - Date.now()) / (1000 * 60 * 60); 190 + if (hours < 24) { 191 + return 'time'; 192 + } else if (hours < 720) { 193 + // 30 days 194 + return 'day'; 195 + } else { 196 + return 'month'; 197 + } 198 + }, [scheduledAt]); 199 + 200 + return ( 201 + <button type="button" class="textual block" onClick={onClick}> 202 + <div class={`post-schedule-meta post-schedule-${icon}`}> 203 + <Icon icon={icon} class="insignificant" />{' '} 204 + <span> 205 + <Trans comment="Scheduled [in 1 day] ([Thu, Feb 27, 6:30:00 PM])"> 206 + Scheduled{' '} 207 + <b> 208 + <RelativeTime datetime={scheduledAt} /> 209 + </b>{' '} 210 + <small> 211 + ( 212 + {niceDateTime(scheduledAt, { 213 + formatOpts: { 214 + weekday: 'short', 215 + second: 'numeric', 216 + }, 217 + })} 218 + ) 219 + </small> 220 + </Trans> 221 + </span> 222 + </div> 223 + <Status status={status} size="s" previewMode readOnly /> 224 + </button> 225 + ); 226 + } 227 + 228 + function ScheduledPostEdit({ post, scheduledAt, onClose }) { 229 + const { masto } = api(); 230 + const { t } = useLingui(); 231 + const [uiState, setUIState] = useState('default'); 232 + const [newScheduledAt, setNewScheduledAt] = useState(); 233 + const differentScheduledAt = 234 + newScheduledAt && newScheduledAt.getTime() !== scheduledAt.getTime(); 235 + const localTZ = getLocalTimezoneName(); 236 + const pastSchedule = scheduledAt && scheduledAt <= Date.now(); 237 + 238 + const { inReplyToId } = post; 239 + const [replyToStatus, setReplyToStatus] = useState(null); 240 + // TODO: Uncomment this once https://github.com/mastodon/mastodon/issues/34000 is fixed 241 + // useEffect(() => { 242 + // if (inReplyToId) { 243 + // (async () => { 244 + // try { 245 + // const status = await masto.v1.statuses.$select(inReplyToId).fetch(); 246 + // setReplyToStatus(status); 247 + // } catch (e) { 248 + // console.error(e); 249 + // } 250 + // })(); 251 + // } 252 + // }, [inReplyToId]); 253 + 254 + return ( 255 + <div id="scheduled-post-sheet" class="sheet"> 256 + <button type="button" class="sheet-close" onClick={onClose}> 257 + <Icon icon="x" size="l" alt={t`Close`} /> 258 + </button> 259 + <header> 260 + <h2> 261 + <Trans comment="Scheduled [in 1 day]"> 262 + Scheduled{' '} 263 + <b> 264 + <RelativeTime datetime={scheduledAt} /> 265 + </b> 266 + </Trans> 267 + <br /> 268 + <small> 269 + {niceDateTime(scheduledAt, { 270 + formatOpts: { 271 + weekday: 'short', 272 + second: 'numeric', 273 + }, 274 + })} 275 + </small> 276 + </h2> 277 + </header> 278 + <main tabIndex="-1"> 279 + {!!replyToStatus && ( 280 + <div class="status-reply"> 281 + <Status status={replyToStatus} size="s" previewMode readOnly /> 282 + </div> 283 + )} 284 + <Status 285 + status={post} 286 + size="s" 287 + previewMode 288 + readOnly 289 + onMediaClick={(e, i, media, status) => { 290 + e.preventDefault(); 291 + states.showMediaModal = { 292 + mediaAttachments: post.mediaAttachments, 293 + mediaIndex: i, 294 + }; 295 + }} 296 + /> 297 + <form 298 + onSubmit={(e) => { 299 + e.preventDefault(); 300 + setUIState('loading'); 301 + (async () => { 302 + try { 303 + await masto.v1.scheduledStatuses.$select(post.id).update({ 304 + scheduledAt: newScheduledAt.toISOString(), 305 + }); 306 + showToast(t`Scheduled post rescheduled`); 307 + onClose(); 308 + setUIState('default'); 309 + states.reloadScheduledPosts++; 310 + } catch (e) { 311 + setUIState('error'); 312 + console.error(e); 313 + showToast(t`Failed to reschedule post`); 314 + } 315 + })(); 316 + }} 317 + > 318 + <footer> 319 + <div class="row"> 320 + <span> 321 + <ScheduledAtField 322 + scheduledAt={scheduledAt} 323 + setScheduledAt={(date) => { 324 + setNewScheduledAt(date); 325 + }} 326 + />{' '} 327 + <small class="ib">{localTZ}</small> 328 + </span> 329 + </div> 330 + <div class="row"> 331 + <button 332 + disabled={ 333 + !differentScheduledAt || uiState === 'loading' || pastSchedule 334 + } 335 + > 336 + <Trans>Reschedule</Trans> 337 + </button> 338 + <span class="grow" /> 339 + <MenuConfirm 340 + align="end" 341 + menuItemClassName="danger" 342 + confirmLabel={t`Delete scheduled post?`} 343 + onClick={() => { 344 + setUIState('loading'); 345 + (async () => { 346 + try { 347 + await api() 348 + .masto.v1.scheduledStatuses.$select(post.id) 349 + .remove(); 350 + showToast(t`Scheduled post deleted`); 351 + onClose(); 352 + setUIState('default'); 353 + states.reloadScheduledPosts++; 354 + } catch (e) { 355 + setUIState('error'); 356 + console.error(e); 357 + showToast(t`Failed to delete scheduled post`); 358 + } 359 + })(); 360 + }} 361 + > 362 + <button 363 + type="button" 364 + class="light danger" 365 + disabled={uiState === 'loading' || pastSchedule} 366 + > 367 + <Trans>Delete…</Trans> 368 + </button> 369 + </MenuConfirm> 370 + </div> 371 + </footer> 372 + </form> 373 + </main> 374 + </div> 375 + ); 376 + }
+1
src/utils/states.js
··· 31 31 id: null, 32 32 counter: 0, 33 33 }, 34 + reloadScheduledPosts: 0, 34 35 spoilers: {}, 35 36 spoilersMedia: {}, 36 37 scrollPositions: {},