this repo has no description
0
fork

Configure Feed

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

Refactor components from account-info

+2015 -2036
+29
src/components/account-handle-info.jsx
··· 1 + import { Trans } from '@lingui/react/macro'; 2 + import punycode from 'punycode/'; 3 + 4 + function AccountHandleInfo({ acct, instance }) { 5 + // acct = username or username@server 6 + let [username, server] = acct.split('@'); 7 + if (!server) server = instance; 8 + const encodedAcct = punycode.toASCII(acct); 9 + return ( 10 + <div class="handle-info"> 11 + <span class="handle-handle" title={encodedAcct}> 12 + <b class="handle-username">{username}</b> 13 + <span class="handle-at">@</span> 14 + <b class="handle-server">{server}</b> 15 + </span> 16 + <div class="handle-legend"> 17 + <span class="ib"> 18 + <span class="handle-legend-icon username" /> <Trans>username</Trans> 19 + </span>{' '} 20 + <span class="ib"> 21 + <span class="handle-legend-icon server" />{' '} 22 + <Trans>server domain name</Trans> 23 + </span> 24 + </div> 25 + </div> 26 + ); 27 + } 28 + 29 + export default AccountHandleInfo;
+5 -1677
src/components/account-info.jsx
··· 1 1 import './account-info.css'; 2 2 3 - import { msg, plural } from '@lingui/core/macro'; 3 + import { plural } from '@lingui/core/macro'; 4 4 import { Plural, Trans, useLingui } from '@lingui/react/macro'; 5 5 import { MenuDivider, MenuItem } from '@szhsin/react-menu'; 6 6 import { 7 7 useCallback, 8 8 useEffect, 9 9 useMemo, 10 - useReducer, 11 10 useRef, 12 11 useState, 13 12 } from 'preact/hooks'; 14 - import punycode from 'punycode/'; 15 13 16 14 import { api } from '../utils/api'; 17 15 import enhanceContent from '../utils/enhance-content'; 18 16 import getDomain from '../utils/get-domain'; 19 - import getHTMLText from '../utils/getHTMLText'; 20 17 import handleContentLinks from '../utils/handle-content-links'; 21 - import i18nDuration from '../utils/i18n-duration'; 22 - import { getLists } from '../utils/lists'; 23 18 import niceDateTime from '../utils/nice-date-time'; 24 19 import pmem from '../utils/pmem'; 25 - import { fetchRelationships } from '../utils/relationships'; 26 20 import shortenNumber from '../utils/shorten-number'; 27 - import showCompose from '../utils/show-compose'; 28 21 import showToast from '../utils/show-toast'; 29 22 import states from '../utils/states'; 30 - import store from '../utils/store'; 31 23 import { 32 24 getAccounts, 33 25 getCurrentAccountID, 34 26 saveAccounts, 35 - updateAccount, 36 27 } from '../utils/store-utils'; 37 28 import supports from '../utils/supports'; 38 29 39 30 import AccountBlock from './account-block'; 31 + import AccountHandleInfo from './account-handle-info'; 40 32 import Avatar from './avatar'; 33 + import EditProfileSheet from './edit-profile-sheet'; 41 34 import EmojiText from './emoji-text'; 35 + import Endorsements from './endorsements'; 42 36 import Icon from './icon'; 43 37 import Link from './link'; 44 - import ListAddEdit from './list-add-edit'; 45 - import Loader from './loader'; 46 - import MenuConfirm from './menu-confirm'; 47 38 import Menu2 from './menu2'; 48 39 import Modal from './modal'; 49 - import SubMenu2 from './submenu2'; 50 - import TranslationBlock from './translation-block'; 51 - 52 - const MUTE_DURATIONS = [ 53 - 60 * 5, // 5 minutes 54 - 60 * 30, // 30 minutes 55 - 60 * 60, // 1 hour 56 - 60 * 60 * 6, // 6 hours 57 - 60 * 60 * 24, // 1 day 58 - 60 * 60 * 24 * 3, // 3 days 59 - 60 * 60 * 24 * 7, // 1 week 60 - 60 * 60 * 24 * 30, // 30 days 61 - 0, // forever 62 - ]; 63 - const MUTE_DURATIONS_LABELS = { 64 - 0: msg`Forever`, 65 - 300: i18nDuration(5, 'minute'), 66 - 1_800: i18nDuration(30, 'minute'), 67 - 3_600: i18nDuration(1, 'hour'), 68 - 21_600: i18nDuration(6, 'hour'), 69 - 86_400: i18nDuration(1, 'day'), 70 - 259_200: i18nDuration(3, 'day'), 71 - 604_800: i18nDuration(1, 'week'), 72 - 2592_000: i18nDuration(30, 'day'), 73 - }; 40 + import RelatedActions from './related-actions'; 74 41 75 42 const LIMIT = 80; 76 43 ··· 134 101 const memFetchPostingStats = pmem(fetchPostingStats, { 135 102 maxAge: ACCOUNT_INFO_MAX_AGE, 136 103 }); 137 - 138 - const ENDORSEMENTS_LIMIT = 80; 139 104 140 105 function AccountInfo({ 141 106 account, ··· 1075 1040 1076 1041 const FAMILIAR_FOLLOWERS_LIMIT = 3; 1077 1042 1078 - function RelatedActions({ 1079 - info, 1080 - instance, 1081 - standalone, 1082 - authenticated, 1083 - onRelationshipChange = () => {}, 1084 - onProfileUpdate = () => {}, 1085 - setShowEditProfile = () => {}, 1086 - showEndorsements = false, 1087 - renderEndorsements = false, 1088 - setRenderEndorsements = () => {}, 1089 - }) { 1090 - if (!info) return null; 1091 - const { _, t } = useLingui(); 1092 - const { 1093 - masto: currentMasto, 1094 - instance: currentInstance, 1095 - authenticated: currentAuthenticated, 1096 - } = api(); 1097 - const sameInstance = instance === currentInstance; 1098 - 1099 - const [relationshipUIState, setRelationshipUIState] = useState('default'); 1100 - const [relationship, setRelationship] = useState(null); 1101 - 1102 - const { id, acct, url, username, locked, lastStatusAt, note, fields, moved } = 1103 - info; 1104 - const accountID = useRef(id); 1105 - 1106 - const { 1107 - following, 1108 - showingReblogs, 1109 - notifying, 1110 - followedBy, 1111 - blocking, 1112 - blockedBy, 1113 - muting, 1114 - mutingNotifications, 1115 - requested, 1116 - domainBlocking, 1117 - endorsed, 1118 - note: privateNote, 1119 - } = relationship || {}; 1120 - 1121 - const [currentInfo, setCurrentInfo] = useState(null); 1122 - const [isSelf, setIsSelf] = useState(false); 1123 - 1124 - const acctWithInstance = acct.includes('@') ? acct : `${acct}@${instance}`; 1125 - 1126 - const supportsEndorsements = supports('@mastodon/endorsements'); 1127 - 1128 - useEffect(() => { 1129 - if (info) { 1130 - const currentAccount = getCurrentAccountID(); 1131 - let currentID; 1132 - (async () => { 1133 - if (sameInstance && authenticated) { 1134 - currentID = id; 1135 - } else if (!sameInstance && currentAuthenticated) { 1136 - // Grab this account from my logged-in instance 1137 - const acctHasInstance = info.acct.includes('@'); 1138 - try { 1139 - const results = await currentMasto.v2.search.list({ 1140 - q: acctHasInstance ? info.acct : `${info.username}@${instance}`, 1141 - type: 'accounts', 1142 - limit: 1, 1143 - resolve: true, 1144 - }); 1145 - console.log('🥏 Fetched account from logged-in instance', results); 1146 - if (results.accounts.length) { 1147 - currentID = results.accounts[0].id; 1148 - setCurrentInfo(results.accounts[0]); 1149 - } 1150 - } catch (e) { 1151 - console.error(e); 1152 - } 1153 - } 1154 - 1155 - if (!currentID) return; 1156 - 1157 - if (currentAccount === currentID) { 1158 - // It's myself! 1159 - setIsSelf(true); 1160 - return; 1161 - } 1162 - 1163 - accountID.current = currentID; 1164 - 1165 - // if (moved) return; 1166 - 1167 - setRelationshipUIState('loading'); 1168 - 1169 - const fetchRelationships = currentMasto.v1.accounts.relationships.fetch( 1170 - { 1171 - id: [currentID], 1172 - }, 1173 - ); 1174 - 1175 - try { 1176 - const relationships = await fetchRelationships; 1177 - console.log('fetched relationship', relationships); 1178 - setRelationshipUIState('default'); 1179 - 1180 - if (relationships.length) { 1181 - const relationship = relationships[0]; 1182 - setRelationship(relationship); 1183 - onRelationshipChange({ relationship, currentID }); 1184 - } 1185 - } catch (e) { 1186 - console.error(e); 1187 - setRelationshipUIState('error'); 1188 - } 1189 - })(); 1190 - } 1191 - }, [info, authenticated]); 1192 - 1193 - useEffect(() => { 1194 - if (info && isSelf) { 1195 - updateAccount(info); 1196 - } 1197 - }, [info, isSelf]); 1198 - 1199 - const loading = relationshipUIState === 'loading'; 1200 - 1201 - const [showTranslatedBio, setShowTranslatedBio] = useState(false); 1202 - const [showAddRemoveLists, setShowAddRemoveLists] = useState(false); 1203 - const [showPrivateNoteModal, setShowPrivateNoteModal] = useState(false); 1204 - const [lists, setLists] = useState([]); 1205 - 1206 - return ( 1207 - <> 1208 - <div class="actions"> 1209 - <span> 1210 - {followedBy ? ( 1211 - <span class="tag"> 1212 - <Trans>Follows you</Trans> 1213 - </span> 1214 - ) : !!lastStatusAt ? ( 1215 - <small class="insignificant"> 1216 - <Trans> 1217 - Last post:{' '} 1218 - <span class="ib"> 1219 - {niceDateTime(lastStatusAt, { 1220 - hideTime: true, 1221 - })} 1222 - </span> 1223 - </Trans> 1224 - </small> 1225 - ) : ( 1226 - <span /> 1227 - )} 1228 - {muting && ( 1229 - <span class="tag danger"> 1230 - <Trans>Muted</Trans> 1231 - </span> 1232 - )} 1233 - {blocking && ( 1234 - <span class="tag danger"> 1235 - <Trans>Blocked</Trans> 1236 - </span> 1237 - )} 1238 - </span>{' '} 1239 - <span class="buttons"> 1240 - {!!privateNote && ( 1241 - <button 1242 - type="button" 1243 - class="private-note-tag" 1244 - title={t`Private note`} 1245 - onClick={() => { 1246 - setShowPrivateNoteModal(true); 1247 - }} 1248 - dir="auto" 1249 - > 1250 - <span>{privateNote}</span> 1251 - </button> 1252 - )} 1253 - <Menu2 1254 - portal={{ 1255 - target: document.body, 1256 - }} 1257 - containerProps={{ 1258 - style: { 1259 - // Higher than the backdrop 1260 - zIndex: 1001, 1261 - }, 1262 - }} 1263 - align="center" 1264 - position="anchor" 1265 - overflow="auto" 1266 - menuButton={ 1267 - <button type="button" class="plain" disabled={loading}> 1268 - <Icon icon="more" size="l" alt={t`More`} /> 1269 - </button> 1270 - } 1271 - onMenuChange={(e) => { 1272 - if (following && e.open) { 1273 - // Fetch lists that have this account 1274 - (async () => { 1275 - try { 1276 - const lists = await currentMasto.v1.accounts 1277 - .$select(accountID.current) 1278 - .lists.list(); 1279 - console.log('fetched account lists', lists); 1280 - setLists(lists); 1281 - } catch (e) { 1282 - console.error(e); 1283 - } 1284 - })(); 1285 - } 1286 - }} 1287 - > 1288 - {currentAuthenticated && !isSelf ? ( 1289 - <> 1290 - <MenuItem 1291 - onClick={() => { 1292 - showCompose({ 1293 - draftStatus: { 1294 - status: `@${currentInfo?.acct || acct} `, 1295 - }, 1296 - }); 1297 - }} 1298 - > 1299 - <Icon icon="at" /> 1300 - <span> 1301 - <Trans> 1302 - Mention <span class="bidi-isolate">@{username}</span> 1303 - </Trans> 1304 - </span> 1305 - </MenuItem> 1306 - <MenuItem 1307 - onClick={() => { 1308 - setShowTranslatedBio(true); 1309 - }} 1310 - > 1311 - <Icon icon="translate" /> 1312 - <span> 1313 - <Trans>Translate bio</Trans> 1314 - </span> 1315 - </MenuItem> 1316 - {supports('@mastodon/profile-private-note') && ( 1317 - <MenuItem 1318 - onClick={() => { 1319 - setShowPrivateNoteModal(true); 1320 - }} 1321 - > 1322 - <Icon icon="pencil" /> 1323 - <span> 1324 - {privateNote ? t`Edit private note` : t`Add private note`} 1325 - </span> 1326 - </MenuItem> 1327 - )} 1328 - {following && !!relationship && ( 1329 - <> 1330 - <MenuItem 1331 - onClick={() => { 1332 - setRelationshipUIState('loading'); 1333 - (async () => { 1334 - try { 1335 - const rel = await currentMasto.v1.accounts 1336 - .$select(accountID.current) 1337 - .follow({ 1338 - notify: !notifying, 1339 - }); 1340 - if (rel) setRelationship(rel); 1341 - setRelationshipUIState('default'); 1342 - showToast( 1343 - rel.notifying 1344 - ? t`Notifications enabled for @${username}'s posts.` 1345 - : t` Notifications disabled for @${username}'s posts.`, 1346 - ); 1347 - } catch (e) { 1348 - alert(e); 1349 - setRelationshipUIState('error'); 1350 - } 1351 - })(); 1352 - }} 1353 - > 1354 - <Icon icon="notification" /> 1355 - <span> 1356 - {notifying 1357 - ? t`Disable notifications` 1358 - : t`Enable notifications`} 1359 - </span> 1360 - </MenuItem> 1361 - <MenuItem 1362 - onClick={() => { 1363 - setRelationshipUIState('loading'); 1364 - (async () => { 1365 - try { 1366 - const rel = await currentMasto.v1.accounts 1367 - .$select(accountID.current) 1368 - .follow({ 1369 - reblogs: !showingReblogs, 1370 - }); 1371 - if (rel) setRelationship(rel); 1372 - setRelationshipUIState('default'); 1373 - showToast( 1374 - rel.showingReblogs 1375 - ? t`Boosts from @${username} enabled.` 1376 - : t`Boosts from @${username} disabled.`, 1377 - ); 1378 - } catch (e) { 1379 - alert(e); 1380 - setRelationshipUIState('error'); 1381 - } 1382 - })(); 1383 - }} 1384 - > 1385 - <Icon icon="rocket" /> 1386 - <span> 1387 - {showingReblogs ? t`Disable boosts` : t`Enable boosts`} 1388 - </span> 1389 - </MenuItem> 1390 - </> 1391 - )} 1392 - {supportsEndorsements && following && ( 1393 - <MenuItem 1394 - onClick={() => { 1395 - setRelationshipUIState('loading'); 1396 - (async () => { 1397 - try { 1398 - if (endorsed) { 1399 - const newRelationship = 1400 - await currentMasto.v1.accounts 1401 - .$select(currentInfo?.id || id) 1402 - .unpin(); 1403 - setRelationship(newRelationship); 1404 - setRelationshipUIState('default'); 1405 - showToast( 1406 - t`@${username} is no longer featured on your profile.`, 1407 - ); 1408 - } else { 1409 - const newRelationship = 1410 - await currentMasto.v1.accounts 1411 - .$select(currentInfo?.id || id) 1412 - .pin(); 1413 - setRelationship(newRelationship); 1414 - setRelationshipUIState('default'); 1415 - showToast( 1416 - t`@${username} is now featured on your profile.`, 1417 - ); 1418 - } 1419 - } catch (e) { 1420 - console.error(e); 1421 - setRelationshipUIState('error'); 1422 - if (endorsed) { 1423 - showToast( 1424 - t`Unable to unfeature @${username} on your profile.`, 1425 - ); 1426 - } else { 1427 - showToast( 1428 - t`Unable to feature @${username} on your profile.`, 1429 - ); 1430 - } 1431 - } 1432 - })(); 1433 - }} 1434 - > 1435 - <Icon icon="endorsement" /> 1436 - {endorsed 1437 - ? t`Don't feature on profile` 1438 - : t`Feature on profile`} 1439 - </MenuItem> 1440 - )} 1441 - {showEndorsements && 1442 - supportsEndorsements && 1443 - !renderEndorsements && ( 1444 - <MenuItem onClick={() => setRenderEndorsements(true)}> 1445 - <Icon icon="endorsement" /> 1446 - <span> 1447 - <Trans>Show featured profiles</Trans> 1448 - </span> 1449 - </MenuItem> 1450 - )} 1451 - {/* Add/remove from lists is only possible if following the account */} 1452 - {following && ( 1453 - <MenuItem 1454 - onClick={() => { 1455 - setShowAddRemoveLists(true); 1456 - }} 1457 - > 1458 - <Icon icon="list" /> 1459 - {lists.length ? ( 1460 - <> 1461 - <small class="menu-grow"> 1462 - <Trans>Add/Remove from Lists</Trans> 1463 - <br /> 1464 - <span class="more-insignificant"> 1465 - {lists.map((list) => list.title).join(', ')} 1466 - </span> 1467 - </small> 1468 - <small class="more-insignificant">{lists.length}</small> 1469 - </> 1470 - ) : ( 1471 - <span> 1472 - <Trans>Add/Remove from Lists</Trans> 1473 - </span> 1474 - )} 1475 - </MenuItem> 1476 - )} 1477 - <MenuDivider /> 1478 - </> 1479 - ) : ( 1480 - supportsEndorsements && 1481 - !renderEndorsements && ( 1482 - <> 1483 - <MenuItem onClick={() => setRenderEndorsements(true)}> 1484 - <Icon icon="endorsement" /> 1485 - Show featured profiles 1486 - </MenuItem> 1487 - <MenuDivider /> 1488 - </> 1489 - ) 1490 - )} 1491 - <MenuItem 1492 - onClick={() => { 1493 - const handle = `@${currentInfo?.acct || acctWithInstance}`; 1494 - try { 1495 - navigator.clipboard.writeText(handle); 1496 - showToast(t`Handle copied`); 1497 - } catch (e) { 1498 - console.error(e); 1499 - showToast(t`Unable to copy handle`); 1500 - } 1501 - }} 1502 - > 1503 - <Icon icon="copy" /> 1504 - <small> 1505 - <Trans>Copy handle</Trans> 1506 - <br /> 1507 - <span class="more-insignificant bidi-isolate"> 1508 - @{currentInfo?.acct || acctWithInstance} 1509 - </span> 1510 - </small> 1511 - </MenuItem> 1512 - <MenuItem href={url} target="_blank"> 1513 - <Icon icon="external" /> 1514 - <small class="menu-double-lines">{niceAccountURL(url)}</small> 1515 - </MenuItem> 1516 - <div class="menu-horizontal"> 1517 - <MenuItem 1518 - onClick={() => { 1519 - // Copy url to clipboard 1520 - try { 1521 - navigator.clipboard.writeText(url); 1522 - showToast(t`Link copied`); 1523 - } catch (e) { 1524 - console.error(e); 1525 - showToast(t`Unable to copy link`); 1526 - } 1527 - }} 1528 - > 1529 - <Icon icon="link" /> 1530 - <span> 1531 - <Trans>Copy</Trans> 1532 - </span> 1533 - </MenuItem> 1534 - {navigator?.share && 1535 - navigator?.canShare?.({ 1536 - url, 1537 - }) && ( 1538 - <MenuItem 1539 - onClick={() => { 1540 - try { 1541 - navigator.share({ 1542 - url, 1543 - }); 1544 - } catch (e) { 1545 - console.error(e); 1546 - alert(t`Sharing doesn't seem to work.`); 1547 - } 1548 - }} 1549 - > 1550 - <Icon icon="share" /> 1551 - <span> 1552 - <Trans>Share…</Trans> 1553 - </span> 1554 - </MenuItem> 1555 - )} 1556 - </div> 1557 - {!!relationship && ( 1558 - <> 1559 - <MenuDivider /> 1560 - {muting ? ( 1561 - <MenuItem 1562 - onClick={() => { 1563 - setRelationshipUIState('loading'); 1564 - (async () => { 1565 - try { 1566 - const newRelationship = await currentMasto.v1.accounts 1567 - .$select(currentInfo?.id || id) 1568 - .unmute(); 1569 - console.log('unmuting', newRelationship); 1570 - setRelationship(newRelationship); 1571 - setRelationshipUIState('default'); 1572 - showToast(t`Unmuted @${username}`); 1573 - states.reloadGenericAccounts.id = 'mute'; 1574 - states.reloadGenericAccounts.counter++; 1575 - } catch (e) { 1576 - console.error(e); 1577 - setRelationshipUIState('error'); 1578 - } 1579 - })(); 1580 - }} 1581 - > 1582 - <Icon icon="unmute" /> 1583 - <span> 1584 - <Trans> 1585 - Unmute <span class="bidi-isolate">@{username}</span> 1586 - </Trans> 1587 - </span> 1588 - </MenuItem> 1589 - ) : ( 1590 - <SubMenu2 1591 - menuClassName="menu-blur" 1592 - openTrigger="clickOnly" 1593 - direction="bottom" 1594 - overflow="auto" 1595 - shift={16} 1596 - label={ 1597 - <> 1598 - <Icon icon="mute" /> 1599 - <span class="menu-grow"> 1600 - <Trans> 1601 - Mute <span class="bidi-isolate">@{username}</span>… 1602 - </Trans> 1603 - </span> 1604 - <span 1605 - style={{ 1606 - textOverflow: 'clip', 1607 - }} 1608 - > 1609 - <Icon icon="time" /> 1610 - <Icon icon="chevron-right" /> 1611 - </span> 1612 - </> 1613 - } 1614 - > 1615 - <div class="menu-wrap"> 1616 - {MUTE_DURATIONS.map((duration) => ( 1617 - <MenuItem 1618 - onClick={() => { 1619 - setRelationshipUIState('loading'); 1620 - (async () => { 1621 - try { 1622 - const newRelationship = 1623 - await currentMasto.v1.accounts 1624 - .$select(currentInfo?.id || id) 1625 - .mute({ 1626 - duration, 1627 - }); 1628 - console.log('muting', newRelationship); 1629 - setRelationship(newRelationship); 1630 - setRelationshipUIState('default'); 1631 - showToast( 1632 - t`Muted @${username} for ${ 1633 - typeof MUTE_DURATIONS_LABELS[duration] === 1634 - 'function' 1635 - ? MUTE_DURATIONS_LABELS[duration]() 1636 - : _(MUTE_DURATIONS_LABELS[duration]) 1637 - }`, 1638 - ); 1639 - states.reloadGenericAccounts.id = 'mute'; 1640 - states.reloadGenericAccounts.counter++; 1641 - } catch (e) { 1642 - console.error(e); 1643 - setRelationshipUIState('error'); 1644 - showToast(t`Unable to mute @${username}`); 1645 - } 1646 - })(); 1647 - }} 1648 - > 1649 - {typeof MUTE_DURATIONS_LABELS[duration] === 'function' 1650 - ? MUTE_DURATIONS_LABELS[duration]() 1651 - : _(MUTE_DURATIONS_LABELS[duration])} 1652 - </MenuItem> 1653 - ))} 1654 - </div> 1655 - </SubMenu2> 1656 - )} 1657 - {followedBy && ( 1658 - <MenuConfirm 1659 - subMenu 1660 - menuItemClassName="danger" 1661 - confirmLabel={ 1662 - <> 1663 - <Icon icon="user-x" /> 1664 - <span> 1665 - <Trans> 1666 - Remove <span class="bidi-isolate">@{username}</span>{' '} 1667 - from followers? 1668 - </Trans> 1669 - </span> 1670 - </> 1671 - } 1672 - onClick={() => { 1673 - setRelationshipUIState('loading'); 1674 - (async () => { 1675 - try { 1676 - const newRelationship = await currentMasto.v1.accounts 1677 - .$select(currentInfo?.id || id) 1678 - .removeFromFollowers(); 1679 - console.log( 1680 - 'removing from followers', 1681 - newRelationship, 1682 - ); 1683 - setRelationship(newRelationship); 1684 - setRelationshipUIState('default'); 1685 - showToast(t`@${username} removed from followers`); 1686 - states.reloadGenericAccounts.id = 'followers'; 1687 - states.reloadGenericAccounts.counter++; 1688 - } catch (e) { 1689 - console.error(e); 1690 - setRelationshipUIState('error'); 1691 - } 1692 - })(); 1693 - }} 1694 - > 1695 - <Icon icon="user-x" /> 1696 - <span> 1697 - <Trans>Remove follower…</Trans> 1698 - </span> 1699 - </MenuConfirm> 1700 - )} 1701 - <MenuConfirm 1702 - subMenu 1703 - confirm={!blocking} 1704 - confirmLabel={ 1705 - <> 1706 - <Icon icon="block" /> 1707 - <span> 1708 - <Trans> 1709 - Block <span class="bidi-isolate">@{username}</span>? 1710 - </Trans> 1711 - </span> 1712 - </> 1713 - } 1714 - itemProps={{ 1715 - className: 'danger', 1716 - }} 1717 - menuItemClassName="danger" 1718 - onClick={() => { 1719 - // if (!blocking && !confirm(`Block @${username}?`)) { 1720 - // return; 1721 - // } 1722 - setRelationshipUIState('loading'); 1723 - (async () => { 1724 - try { 1725 - if (blocking) { 1726 - const newRelationship = await currentMasto.v1.accounts 1727 - .$select(currentInfo?.id || id) 1728 - .unblock(); 1729 - console.log('unblocking', newRelationship); 1730 - setRelationship(newRelationship); 1731 - setRelationshipUIState('default'); 1732 - showToast(t`Unblocked @${username}`); 1733 - } else { 1734 - const newRelationship = await currentMasto.v1.accounts 1735 - .$select(currentInfo?.id || id) 1736 - .block(); 1737 - console.log('blocking', newRelationship); 1738 - setRelationship(newRelationship); 1739 - setRelationshipUIState('default'); 1740 - showToast(t`Blocked @${username}`); 1741 - } 1742 - states.reloadGenericAccounts.id = 'block'; 1743 - states.reloadGenericAccounts.counter++; 1744 - } catch (e) { 1745 - console.error(e); 1746 - setRelationshipUIState('error'); 1747 - if (blocking) { 1748 - showToast(t`Unable to unblock @${username}`); 1749 - } else { 1750 - showToast(t`Unable to block @${username}`); 1751 - } 1752 - } 1753 - })(); 1754 - }} 1755 - > 1756 - {blocking ? ( 1757 - <> 1758 - <Icon icon="unblock" /> 1759 - <span> 1760 - <Trans> 1761 - Unblock <span class="bidi-isolate">@{username}</span> 1762 - </Trans> 1763 - </span> 1764 - </> 1765 - ) : ( 1766 - <> 1767 - <Icon icon="block" /> 1768 - <span> 1769 - <Trans> 1770 - Block <span class="bidi-isolate">@{username}</span>… 1771 - </Trans> 1772 - </span> 1773 - </> 1774 - )} 1775 - </MenuConfirm> 1776 - <MenuItem 1777 - className="danger" 1778 - onClick={() => { 1779 - states.showReportModal = { 1780 - account: currentInfo || info, 1781 - }; 1782 - }} 1783 - > 1784 - <Icon icon="flag" /> 1785 - <span> 1786 - <Trans> 1787 - Report <span class="bidi-isolate">@{username}</span>… 1788 - </Trans> 1789 - </span> 1790 - </MenuItem> 1791 - </> 1792 - )} 1793 - {currentAuthenticated && 1794 - isSelf && 1795 - standalone && 1796 - supports('@mastodon/profile-edit') && ( 1797 - <> 1798 - <MenuDivider /> 1799 - <MenuItem 1800 - onClick={() => { 1801 - setShowEditProfile(true); 1802 - }} 1803 - > 1804 - <Icon icon="pencil" /> 1805 - <span> 1806 - <Trans>Edit profile</Trans> 1807 - </span> 1808 - </MenuItem> 1809 - </> 1810 - )} 1811 - {import.meta.env.DEV && currentAuthenticated && isSelf && ( 1812 - <> 1813 - <MenuDivider /> 1814 - <MenuItem 1815 - onClick={async () => { 1816 - const relationships = 1817 - await currentMasto.v1.accounts.relationships.fetch({ 1818 - id: [accountID.current], 1819 - }); 1820 - const { note } = relationships[0] || {}; 1821 - if (note) { 1822 - alert(note); 1823 - console.log(note); 1824 - } 1825 - }} 1826 - > 1827 - <Icon icon="pencil" /> 1828 - <span>See note</span> 1829 - </MenuItem> 1830 - </> 1831 - )} 1832 - </Menu2> 1833 - {!relationship && relationshipUIState === 'loading' && ( 1834 - <Loader abrupt /> 1835 - )} 1836 - {!!relationship && !moved && ( 1837 - <MenuConfirm 1838 - confirm={following || requested} 1839 - confirmLabel={ 1840 - <span> 1841 - {requested 1842 - ? t`Withdraw follow request?` 1843 - : t`Unfollow @${info.acct || info.username}?`} 1844 - </span> 1845 - } 1846 - menuItemClassName="danger" 1847 - align="end" 1848 - disabled={loading} 1849 - onClick={() => { 1850 - setRelationshipUIState('loading'); 1851 - (async () => { 1852 - try { 1853 - let newRelationship; 1854 - 1855 - if (following || requested) { 1856 - // const yes = confirm( 1857 - // requested 1858 - // ? 'Withdraw follow request?' 1859 - // : `Unfollow @${info.acct || info.username}?`, 1860 - // ); 1861 - 1862 - // if (yes) { 1863 - newRelationship = await currentMasto.v1.accounts 1864 - .$select(accountID.current) 1865 - .unfollow(); 1866 - // } 1867 - } else { 1868 - newRelationship = await currentMasto.v1.accounts 1869 - .$select(accountID.current) 1870 - .follow(); 1871 - } 1872 - 1873 - if (newRelationship) { 1874 - setRelationship(newRelationship); 1875 - 1876 - // Show endorsements if start following 1877 - if ( 1878 - showEndorsements && 1879 - supportsEndorsements && 1880 - !renderEndorsements && 1881 - newRelationship.following 1882 - ) { 1883 - setRenderEndorsements('onlyOpenIfHasEndorsements'); 1884 - } 1885 - } 1886 - setRelationshipUIState('default'); 1887 - } catch (e) { 1888 - alert(e); 1889 - setRelationshipUIState('error'); 1890 - } 1891 - })(); 1892 - }} 1893 - > 1894 - <button 1895 - type="button" 1896 - class={`${following || requested ? 'light swap' : ''}`} 1897 - data-swap-state={following || requested ? 'danger' : ''} 1898 - disabled={loading} 1899 - > 1900 - {following ? ( 1901 - <> 1902 - <span> 1903 - <Trans>Following</Trans> 1904 - </span> 1905 - <span> 1906 - <Trans>Unfollow…</Trans> 1907 - </span> 1908 - </> 1909 - ) : requested ? ( 1910 - <> 1911 - <span> 1912 - <Trans>Requested</Trans> 1913 - </span> 1914 - <span> 1915 - <Trans>Withdraw…</Trans> 1916 - </span> 1917 - </> 1918 - ) : locked ? ( 1919 - <> 1920 - <Icon icon="lock" />{' '} 1921 - <span> 1922 - <Trans>Follow</Trans> 1923 - </span> 1924 - </> 1925 - ) : ( 1926 - t`Follow` 1927 - )} 1928 - </button> 1929 - </MenuConfirm> 1930 - )} 1931 - </span> 1932 - </div> 1933 - {!!showTranslatedBio && ( 1934 - <Modal 1935 - onClose={() => { 1936 - setShowTranslatedBio(false); 1937 - }} 1938 - > 1939 - <TranslatedBioSheet 1940 - note={note} 1941 - fields={fields} 1942 - onClose={() => setShowTranslatedBio(false)} 1943 - /> 1944 - </Modal> 1945 - )} 1946 - {!!showAddRemoveLists && ( 1947 - <Modal 1948 - onClose={() => { 1949 - setShowAddRemoveLists(false); 1950 - }} 1951 - > 1952 - <AddRemoveListsSheet 1953 - accountID={accountID.current} 1954 - onClose={() => setShowAddRemoveLists(false)} 1955 - /> 1956 - </Modal> 1957 - )} 1958 - {!!showPrivateNoteModal && ( 1959 - <Modal 1960 - onClose={() => { 1961 - setShowPrivateNoteModal(false); 1962 - }} 1963 - > 1964 - <PrivateNoteSheet 1965 - account={info} 1966 - note={privateNote} 1967 - onRelationshipChange={(relationship) => { 1968 - setRelationship(relationship); 1969 - // onRelationshipChange({ relationship, currentID: accountID.current }); 1970 - }} 1971 - onClose={() => setShowPrivateNoteModal(false)} 1972 - /> 1973 - </Modal> 1974 - )} 1975 - </> 1976 - ); 1977 - } 1978 - 1979 - // Apply more alpha if high luminence 1980 1043 function lightenRGB([r, g, b]) { 1981 1044 const luminence = 0.2126 * r + 0.7152 * g + 0.0722 * b; 1982 1045 console.log('luminence', luminence); ··· 1990 1053 } 1991 1054 alpha = Math.min(1, alpha); 1992 1055 return [r, g, b, alpha]; 1993 - } 1994 - 1995 - function niceAccountURL(url) { 1996 - if (!url) return; 1997 - const urlObj = URL.parse(url); 1998 - if (!urlObj) return; 1999 - const { host, pathname } = urlObj; 2000 - const path = pathname.replace(/\/$/, '').replace(/^\//, ''); 2001 - return ( 2002 - <> 2003 - <span class="more-insignificant">{punycode.toUnicode(host)}/</span> 2004 - <wbr /> 2005 - <span>{path}</span> 2006 - </> 2007 - ); 2008 - } 2009 - 2010 - function TranslatedBioSheet({ note, fields, onClose }) { 2011 - const { t } = useLingui(); 2012 - const fieldsText = 2013 - fields 2014 - ?.map(({ name, value }) => `${name}\n${getHTMLText(value)}`) 2015 - .join('\n\n') || ''; 2016 - 2017 - const text = getHTMLText(note) + (fieldsText ? `\n\n${fieldsText}` : ''); 2018 - 2019 - return ( 2020 - <div class="sheet"> 2021 - {!!onClose && ( 2022 - <button type="button" class="sheet-close" onClick={onClose}> 2023 - <Icon icon="x" alt={t`Close`} /> 2024 - </button> 2025 - )} 2026 - <header> 2027 - <h2> 2028 - <Trans>Translated Bio</Trans> 2029 - </h2> 2030 - </header> 2031 - <main> 2032 - <p 2033 - style={{ 2034 - whiteSpace: 'pre-wrap', 2035 - }} 2036 - > 2037 - {text} 2038 - </p> 2039 - <TranslationBlock forceTranslate text={text} /> 2040 - </main> 2041 - </div> 2042 - ); 2043 - } 2044 - 2045 - function AddRemoveListsSheet({ accountID, onClose }) { 2046 - const { t } = useLingui(); 2047 - const { masto } = api(); 2048 - const [uiState, setUIState] = useState('default'); 2049 - const [lists, setLists] = useState([]); 2050 - const [listsContainingAccount, setListsContainingAccount] = useState([]); 2051 - const [reloadCount, reload] = useReducer((c) => c + 1, 0); 2052 - 2053 - useEffect(() => { 2054 - setUIState('loading'); 2055 - (async () => { 2056 - try { 2057 - const lists = await getLists(); 2058 - setLists(lists); 2059 - const listsContainingAccount = await masto.v1.accounts 2060 - .$select(accountID) 2061 - .lists.list(); 2062 - console.log({ lists, listsContainingAccount }); 2063 - setListsContainingAccount(listsContainingAccount); 2064 - setUIState('default'); 2065 - } catch (e) { 2066 - console.error(e); 2067 - setUIState('error'); 2068 - } 2069 - })(); 2070 - }, [reloadCount]); 2071 - 2072 - const [showListAddEditModal, setShowListAddEditModal] = useState(false); 2073 - 2074 - return ( 2075 - <div class="sheet" id="list-add-remove-container"> 2076 - {!!onClose && ( 2077 - <button type="button" class="sheet-close" onClick={onClose}> 2078 - <Icon icon="x" alt={t`Close`} /> 2079 - </button> 2080 - )} 2081 - <header> 2082 - <h2> 2083 - <Trans>Add/Remove from Lists</Trans> 2084 - </h2> 2085 - </header> 2086 - <main> 2087 - {lists.length > 0 ? ( 2088 - <ul class="list-add-remove"> 2089 - {lists.map((list) => { 2090 - const inList = listsContainingAccount.some( 2091 - (l) => l.id === list.id, 2092 - ); 2093 - return ( 2094 - <li> 2095 - <button 2096 - type="button" 2097 - class={`light ${inList ? 'checked' : ''}`} 2098 - disabled={uiState === 'loading'} 2099 - onClick={() => { 2100 - setUIState('loading'); 2101 - (async () => { 2102 - try { 2103 - if (inList) { 2104 - await masto.v1.lists 2105 - .$select(list.id) 2106 - .accounts.remove({ 2107 - accountIds: [accountID], 2108 - }); 2109 - } else { 2110 - await masto.v1.lists 2111 - .$select(list.id) 2112 - .accounts.create({ 2113 - accountIds: [accountID], 2114 - }); 2115 - } 2116 - // setUIState('default'); 2117 - reload(); 2118 - } catch (e) { 2119 - console.error(e); 2120 - setUIState('error'); 2121 - alert( 2122 - inList 2123 - ? t`Unable to remove from list.` 2124 - : t`Unable to add to list.`, 2125 - ); 2126 - } 2127 - })(); 2128 - }} 2129 - > 2130 - <Icon icon="check-circle" alt="☑️" /> 2131 - <span>{list.title}</span> 2132 - </button> 2133 - </li> 2134 - ); 2135 - })} 2136 - </ul> 2137 - ) : uiState === 'loading' ? ( 2138 - <p class="ui-state"> 2139 - <Loader abrupt /> 2140 - </p> 2141 - ) : uiState === 'error' ? ( 2142 - <p class="ui-state"> 2143 - <Trans>Unable to load lists.</Trans> 2144 - </p> 2145 - ) : ( 2146 - <p class="ui-state"> 2147 - <Trans>No lists.</Trans> 2148 - </p> 2149 - )} 2150 - <button 2151 - type="button" 2152 - class="plain2" 2153 - onClick={() => setShowListAddEditModal(true)} 2154 - disabled={uiState !== 'default'} 2155 - > 2156 - <Icon icon="plus" size="l" />{' '} 2157 - <span> 2158 - <Trans>New list</Trans> 2159 - </span> 2160 - </button> 2161 - </main> 2162 - {showListAddEditModal && ( 2163 - <Modal 2164 - onClick={(e) => { 2165 - if (e.target === e.currentTarget) { 2166 - setShowListAddEditModal(false); 2167 - } 2168 - }} 2169 - > 2170 - <ListAddEdit 2171 - list={showListAddEditModal?.list} 2172 - onClose={(result) => { 2173 - if (result.state === 'success') { 2174 - reload(); 2175 - } 2176 - setShowListAddEditModal(false); 2177 - }} 2178 - /> 2179 - </Modal> 2180 - )} 2181 - </div> 2182 - ); 2183 - } 2184 - 2185 - function PrivateNoteSheet({ 2186 - account, 2187 - note: initialNote, 2188 - onRelationshipChange = () => {}, 2189 - onClose = () => {}, 2190 - }) { 2191 - const { t } = useLingui(); 2192 - const { masto } = api(); 2193 - const [uiState, setUIState] = useState('default'); 2194 - const textareaRef = useRef(null); 2195 - 2196 - useEffect(() => { 2197 - let timer; 2198 - if (textareaRef.current && !initialNote) { 2199 - timer = setTimeout(() => { 2200 - textareaRef.current.focus?.(); 2201 - }, 100); 2202 - } 2203 - return () => { 2204 - clearTimeout(timer); 2205 - }; 2206 - }, []); 2207 - 2208 - return ( 2209 - <div class="sheet" id="private-note-container"> 2210 - {!!onClose && ( 2211 - <button type="button" class="sheet-close" onClick={onClose}> 2212 - <Icon icon="x" alt={t`Close`} /> 2213 - </button> 2214 - )} 2215 - <header> 2216 - <b> 2217 - <Trans> 2218 - Private note about{' '} 2219 - <span class="bidi-isolate"> 2220 - @{account?.username || account?.acct} 2221 - </span> 2222 - </Trans> 2223 - </b> 2224 - </header> 2225 - <main> 2226 - <form 2227 - onSubmit={(e) => { 2228 - e.preventDefault(); 2229 - const formData = new FormData(e.target); 2230 - const note = formData.get('note'); 2231 - if (note?.trim() !== initialNote?.trim()) { 2232 - setUIState('loading'); 2233 - (async () => { 2234 - try { 2235 - const newRelationship = await masto.v1.accounts 2236 - .$select(account?.id) 2237 - .note.create({ 2238 - comment: note, 2239 - }); 2240 - console.log('updated relationship', newRelationship); 2241 - setUIState('default'); 2242 - onRelationshipChange(newRelationship); 2243 - onClose(); 2244 - } catch (e) { 2245 - console.error(e); 2246 - setUIState('error'); 2247 - alert(e?.message || t`Unable to update private note.`); 2248 - } 2249 - })(); 2250 - } 2251 - }} 2252 - > 2253 - <textarea 2254 - ref={textareaRef} 2255 - name="note" 2256 - disabled={uiState === 'loading'} 2257 - dir="auto" 2258 - > 2259 - {initialNote} 2260 - </textarea> 2261 - <footer> 2262 - <button 2263 - type="button" 2264 - class="light" 2265 - disabled={uiState === 'loading'} 2266 - onClick={() => { 2267 - onClose?.(); 2268 - }} 2269 - > 2270 - <Trans>Cancel</Trans> 2271 - </button> 2272 - <span> 2273 - <Loader abrupt hidden={uiState !== 'loading'} /> 2274 - <button disabled={uiState === 'loading'} type="submit"> 2275 - <Trans>Save &amp; close</Trans> 2276 - </button> 2277 - </span> 2278 - </footer> 2279 - </form> 2280 - </main> 2281 - </div> 2282 - ); 2283 - } 2284 - 2285 - const SUPPORTED_IMAGE_FORMATS = [ 2286 - 'image/jpeg', 2287 - 'image/png', 2288 - 'image/gif', 2289 - 'image/webp', 2290 - ]; 2291 - const SUPPORTED_IMAGE_FORMATS_STR = SUPPORTED_IMAGE_FORMATS.join(','); 2292 - 2293 - function EditProfileSheet({ onClose = () => {} }) { 2294 - const { t } = useLingui(); 2295 - const { masto } = api(); 2296 - const [uiState, setUIState] = useState('loading'); 2297 - const [account, setAccount] = useState(null); 2298 - const [headerPreview, setHeaderPreview] = useState(null); 2299 - const [avatarPreview, setAvatarPreview] = useState(null); 2300 - 2301 - useEffect(() => { 2302 - (async () => { 2303 - try { 2304 - const acc = await masto.v1.accounts.verifyCredentials(); 2305 - setAccount(acc); 2306 - setUIState('default'); 2307 - } catch (e) { 2308 - console.error(e); 2309 - setUIState('error'); 2310 - } 2311 - })(); 2312 - }, []); 2313 - 2314 - console.log('EditProfileSheet', account); 2315 - const { displayName, source, avatar, header } = account || {}; 2316 - const { note, fields } = source || {}; 2317 - const fieldsAttributesRef = useRef(null); 2318 - 2319 - const avatarMediaAttachments = [ 2320 - ...(avatar ? [{ type: 'image', url: avatar }] : []), 2321 - ...(avatarPreview ? [{ type: 'image', url: avatarPreview }] : []), 2322 - ]; 2323 - const headerMediaAttachments = [ 2324 - ...(header ? [{ type: 'image', url: header }] : []), 2325 - ...(headerPreview ? [{ type: 'image', url: headerPreview }] : []), 2326 - ]; 2327 - 2328 - return ( 2329 - <div class="sheet" id="edit-profile-container"> 2330 - {!!onClose && ( 2331 - <button type="button" class="sheet-close" onClick={onClose}> 2332 - <Icon icon="x" alt={t`Close`} /> 2333 - </button> 2334 - )} 2335 - <header> 2336 - <b> 2337 - <Trans>Edit profile</Trans> 2338 - </b> 2339 - </header> 2340 - <main> 2341 - {uiState === 'loading' ? ( 2342 - <p class="ui-state"> 2343 - <Loader abrupt /> 2344 - </p> 2345 - ) : ( 2346 - <form 2347 - onSubmit={(e) => { 2348 - e.preventDefault(); 2349 - const formData = new FormData(e.target); 2350 - const header = formData.get('header'); 2351 - const avatar = formData.get('avatar'); 2352 - const displayName = formData.get('display_name'); 2353 - const note = formData.get('note'); 2354 - const fieldsAttributesFields = 2355 - fieldsAttributesRef.current.querySelectorAll( 2356 - 'input[name^="fields_attributes"]', 2357 - ); 2358 - const fieldsAttributes = []; 2359 - fieldsAttributesFields.forEach((field) => { 2360 - const name = field.name; 2361 - const [_, index, key] = 2362 - name.match(/fields_attributes\[(\d+)\]\[(.+)\]/) || []; 2363 - const value = field.value ? field.value.trim() : ''; 2364 - if (index && key && value) { 2365 - if (!fieldsAttributes[index]) fieldsAttributes[index] = {}; 2366 - fieldsAttributes[index][key] = value; 2367 - } 2368 - }); 2369 - // Fill in the blanks 2370 - fieldsAttributes.forEach((field) => { 2371 - if (field.name && !field.value) { 2372 - field.value = ''; 2373 - } 2374 - }); 2375 - 2376 - (async () => { 2377 - try { 2378 - const newAccount = await masto.v1.accounts.updateCredentials({ 2379 - header, 2380 - avatar, 2381 - displayName, 2382 - note, 2383 - fieldsAttributes, 2384 - }); 2385 - console.log('updated account', newAccount); 2386 - onClose?.({ 2387 - state: 'success', 2388 - account: newAccount, 2389 - }); 2390 - } catch (e) { 2391 - console.error(e); 2392 - alert(e?.message || t`Unable to update profile.`); 2393 - } 2394 - })(); 2395 - }} 2396 - > 2397 - <div class="edit-profile-media-container"> 2398 - <label> 2399 - <Trans>Header picture</Trans>{' '} 2400 - <input 2401 - type="file" 2402 - name="header" 2403 - accept={SUPPORTED_IMAGE_FORMATS_STR} 2404 - onChange={(e) => { 2405 - const file = e.target.files[0]; 2406 - if (file) { 2407 - const blob = URL.createObjectURL(file); 2408 - setHeaderPreview(blob); 2409 - } 2410 - }} 2411 - /> 2412 - </label> 2413 - <div class="edit-profile-media-field"> 2414 - {header ? ( 2415 - <div 2416 - class="edit-media" 2417 - tabIndex="0" 2418 - onClick={() => { 2419 - states.showMediaModal = { 2420 - mediaAttachments: headerMediaAttachments, 2421 - mediaIndex: 0, 2422 - }; 2423 - }} 2424 - > 2425 - <img src={header} alt="" /> 2426 - </div> 2427 - ) : ( 2428 - <div class="edit-media"></div> 2429 - )} 2430 - {headerPreview && ( 2431 - <> 2432 - <Icon icon="arrow-right" /> 2433 - <div 2434 - class="edit-media" 2435 - tabIndex="0" 2436 - onClick={() => { 2437 - states.showMediaModal = { 2438 - mediaAttachments: headerMediaAttachments, 2439 - mediaIndex: 1, 2440 - }; 2441 - }} 2442 - > 2443 - <img src={headerPreview} alt="" /> 2444 - </div> 2445 - </> 2446 - )} 2447 - </div> 2448 - </div> 2449 - <div class="edit-profile-media-container"> 2450 - <label> 2451 - <Trans>Profile picture</Trans>{' '} 2452 - <input 2453 - type="file" 2454 - name="avatar" 2455 - accept={SUPPORTED_IMAGE_FORMATS_STR} 2456 - onChange={(e) => { 2457 - const file = e.target.files[0]; 2458 - if (file) { 2459 - const blob = URL.createObjectURL(file); 2460 - setAvatarPreview(blob); 2461 - } 2462 - }} 2463 - /> 2464 - </label> 2465 - <div class="edit-profile-media-field"> 2466 - {avatar ? ( 2467 - <div 2468 - class="edit-media" 2469 - tabIndex="0" 2470 - onClick={() => { 2471 - states.showMediaModal = { 2472 - mediaAttachments: avatarMediaAttachments, 2473 - mediaIndex: 0, 2474 - }; 2475 - }} 2476 - > 2477 - <img src={avatar} alt="" /> 2478 - </div> 2479 - ) : ( 2480 - <div class="edit-media"></div> 2481 - )} 2482 - {avatarPreview && ( 2483 - <> 2484 - <Icon icon="arrow-right" /> 2485 - <div 2486 - class="edit-media" 2487 - tabIndex="0" 2488 - onClick={() => { 2489 - states.showMediaModal = { 2490 - mediaAttachments: avatarMediaAttachments, 2491 - mediaIndex: 1, 2492 - }; 2493 - }} 2494 - > 2495 - <img src={avatarPreview} alt="" /> 2496 - </div> 2497 - </> 2498 - )} 2499 - </div> 2500 - </div> 2501 - <p> 2502 - <label> 2503 - <Trans>Name</Trans>{' '} 2504 - <input 2505 - type="text" 2506 - name="display_name" 2507 - defaultValue={displayName} 2508 - maxLength={30} 2509 - disabled={uiState === 'loading'} 2510 - dir="auto" 2511 - /> 2512 - </label> 2513 - </p> 2514 - <p> 2515 - <label> 2516 - <Trans>Bio</Trans> 2517 - <textarea 2518 - defaultValue={note} 2519 - name="note" 2520 - maxLength={500} 2521 - rows="5" 2522 - disabled={uiState === 'loading'} 2523 - dir="auto" 2524 - /> 2525 - </label> 2526 - </p> 2527 - {/* Table for fields; name and values are in fields, min 4 rows */} 2528 - <p> 2529 - <Trans>Extra fields</Trans> 2530 - </p> 2531 - <table ref={fieldsAttributesRef}> 2532 - <thead> 2533 - <tr> 2534 - <th> 2535 - <Trans>Label</Trans> 2536 - </th> 2537 - <th> 2538 - <Trans>Content</Trans> 2539 - </th> 2540 - </tr> 2541 - </thead> 2542 - <tbody> 2543 - {Array.from({ length: Math.max(4, fields.length) }).map( 2544 - (_, i) => { 2545 - const { name = '', value = '' } = fields[i] || {}; 2546 - return ( 2547 - <FieldsAttributesRow 2548 - key={i} 2549 - name={name} 2550 - value={value} 2551 - index={i} 2552 - disabled={uiState === 'loading'} 2553 - /> 2554 - ); 2555 - }, 2556 - )} 2557 - </tbody> 2558 - </table> 2559 - <footer> 2560 - <button 2561 - type="button" 2562 - class="light" 2563 - disabled={uiState === 'loading'} 2564 - onClick={() => { 2565 - onClose?.(); 2566 - }} 2567 - > 2568 - <Trans>Cancel</Trans> 2569 - </button> 2570 - <button type="submit" disabled={uiState === 'loading'}> 2571 - <Trans>Save</Trans> 2572 - </button> 2573 - </footer> 2574 - </form> 2575 - )} 2576 - </main> 2577 - </div> 2578 - ); 2579 - } 2580 - 2581 - function FieldsAttributesRow({ name, value, disabled, index: i }) { 2582 - const [hasValue, setHasValue] = useState(!!value); 2583 - return ( 2584 - <tr> 2585 - <td> 2586 - <input 2587 - type="text" 2588 - name={`fields_attributes[${i}][name]`} 2589 - defaultValue={name} 2590 - disabled={disabled} 2591 - maxLength={255} 2592 - required={hasValue} 2593 - dir="auto" 2594 - /> 2595 - </td> 2596 - <td> 2597 - <input 2598 - type="text" 2599 - name={`fields_attributes[${i}][value]`} 2600 - defaultValue={value} 2601 - disabled={disabled} 2602 - maxLength={255} 2603 - onChange={(e) => setHasValue(!!e.currentTarget.value)} 2604 - dir="auto" 2605 - /> 2606 - </td> 2607 - </tr> 2608 - ); 2609 - } 2610 - 2611 - function AccountHandleInfo({ acct, instance }) { 2612 - // acct = username or username@server 2613 - let [username, server] = acct.split('@'); 2614 - if (!server) server = instance; 2615 - const encodedAcct = punycode.toASCII(acct); 2616 - return ( 2617 - <div class="handle-info"> 2618 - <span class="handle-handle" title={encodedAcct}> 2619 - <b class="handle-username">{username}</b> 2620 - <span class="handle-at">@</span> 2621 - <b class="handle-server">{server}</b> 2622 - </span> 2623 - <div class="handle-legend"> 2624 - <span class="ib"> 2625 - <span class="handle-legend-icon username" /> <Trans>username</Trans> 2626 - </span>{' '} 2627 - <span class="ib"> 2628 - <span class="handle-legend-icon server" />{' '} 2629 - <Trans>server domain name</Trans> 2630 - </span> 2631 - </div> 2632 - </div> 2633 - ); 2634 - } 2635 - 2636 - function Endorsements({ 2637 - accountID: id, 2638 - info, 2639 - open = false, 2640 - onlyOpenIfHasEndorsements = false, 2641 - }) { 2642 - const { masto } = api(); 2643 - const endorsementsContainer = useRef(); 2644 - const [endorsementsUIState, setEndorsementsUIState] = useState('default'); 2645 - const [endorsements, setEndorsements] = useState([]); 2646 - const [relationshipsMap, setRelationshipsMap] = useState({}); 2647 - useEffect(() => { 2648 - if (!supports('@mastodon/endorsements')) return; 2649 - if (!open) return; 2650 - (async () => { 2651 - setEndorsementsUIState('loading'); 2652 - try { 2653 - const accounts = await masto.v1.accounts.$select(id).endorsements.list({ 2654 - limit: ENDORSEMENTS_LIMIT, 2655 - }); 2656 - console.log({ endorsements: accounts }); 2657 - if (!accounts.length) { 2658 - setEndorsementsUIState('default'); 2659 - return; 2660 - } 2661 - setEndorsements(accounts); 2662 - setEndorsementsUIState('default'); 2663 - setTimeout(() => { 2664 - endorsementsContainer.current.scrollIntoView({ 2665 - behavior: 'smooth', 2666 - block: 'nearest', 2667 - }); 2668 - }, 300); 2669 - 2670 - const relationships = await fetchRelationships( 2671 - accounts, 2672 - relationshipsMap, 2673 - ); 2674 - if (relationships) { 2675 - setRelationshipsMap(relationships); 2676 - } 2677 - } catch (e) { 2678 - console.error(e); 2679 - setEndorsementsUIState('error'); 2680 - } 2681 - })(); 2682 - }, [open, id]); 2683 - 2684 - const reallyOpen = onlyOpenIfHasEndorsements 2685 - ? open && endorsements.length > 0 2686 - : open; 2687 - 2688 - if (!reallyOpen) return null; 2689 - 2690 - return ( 2691 - <div class="shazam-container"> 2692 - <div class="shazam-container-inner"> 2693 - <div class="endorsements-container" ref={endorsementsContainer}> 2694 - <h3> 2695 - <Trans>Profiles featured by @{info.username}</Trans> 2696 - </h3> 2697 - {endorsementsUIState === 'loading' ? ( 2698 - <p class="ui-state"> 2699 - <Loader abrupt /> 2700 - </p> 2701 - ) : endorsements.length > 0 ? ( 2702 - <ul 2703 - class={`endorsements ${ 2704 - endorsements.length > 10 ? 'expanded' : '' 2705 - }`} 2706 - > 2707 - {endorsements.map((account) => ( 2708 - <li> 2709 - <AccountBlock 2710 - key={account.id} 2711 - account={account} 2712 - showStats 2713 - avatarSize="xxl" 2714 - relationship={relationshipsMap[account.id]} 2715 - /> 2716 - </li> 2717 - ))} 2718 - </ul> 2719 - ) : ( 2720 - <p class="ui-state insignificant"> 2721 - <Trans>No featured profiles.</Trans> 2722 - </p> 2723 - )} 2724 - </div> 2725 - </div> 2726 - </div> 2727 - ); 2728 1056 } 2729 1057 2730 1058 export default AccountInfo;
+152
src/components/add-remove-lists-sheet.jsx
··· 1 + import { Trans, useLingui } from '@lingui/react/macro'; 2 + import { useEffect, useReducer, useState } from 'preact/hooks'; 3 + 4 + import { api } from '../utils/api'; 5 + import { getLists } from '../utils/lists'; 6 + 7 + import Icon from './icon'; 8 + import ListAddEdit from './list-add-edit'; 9 + import Loader from './loader'; 10 + import Modal from './modal'; 11 + 12 + function AddRemoveListsSheet({ accountID, onClose }) { 13 + const { t } = useLingui(); 14 + const { masto } = api(); 15 + const [uiState, setUIState] = useState('default'); 16 + const [lists, setLists] = useState([]); 17 + const [listsContainingAccount, setListsContainingAccount] = useState([]); 18 + const [reloadCount, reload] = useReducer((c) => c + 1, 0); 19 + 20 + useEffect(() => { 21 + setUIState('loading'); 22 + (async () => { 23 + try { 24 + const lists = await getLists(); 25 + setLists(lists); 26 + const listsContainingAccount = await masto.v1.accounts 27 + .$select(accountID) 28 + .lists.list(); 29 + console.log({ lists, listsContainingAccount }); 30 + setListsContainingAccount(listsContainingAccount); 31 + setUIState('default'); 32 + } catch (e) { 33 + console.error(e); 34 + setUIState('error'); 35 + } 36 + })(); 37 + }, [reloadCount]); 38 + 39 + const [showListAddEditModal, setShowListAddEditModal] = useState(false); 40 + 41 + return ( 42 + <div class="sheet" id="list-add-remove-container"> 43 + {!!onClose && ( 44 + <button type="button" class="sheet-close" onClick={onClose}> 45 + <Icon icon="x" alt={t`Close`} /> 46 + </button> 47 + )} 48 + <header> 49 + <h2> 50 + <Trans>Add/Remove from Lists</Trans> 51 + </h2> 52 + </header> 53 + <main> 54 + {lists.length > 0 ? ( 55 + <ul class="list-add-remove"> 56 + {lists.map((list) => { 57 + const inList = listsContainingAccount.some( 58 + (l) => l.id === list.id, 59 + ); 60 + return ( 61 + <li> 62 + <button 63 + type="button" 64 + class={`light ${inList ? 'checked' : ''}`} 65 + disabled={uiState === 'loading'} 66 + onClick={() => { 67 + setUIState('loading'); 68 + (async () => { 69 + try { 70 + if (inList) { 71 + await masto.v1.lists 72 + .$select(list.id) 73 + .accounts.remove({ 74 + accountIds: [accountID], 75 + }); 76 + } else { 77 + await masto.v1.lists 78 + .$select(list.id) 79 + .accounts.create({ 80 + accountIds: [accountID], 81 + }); 82 + } 83 + // setUIState('default'); 84 + reload(); 85 + } catch (e) { 86 + console.error(e); 87 + setUIState('error'); 88 + alert( 89 + inList 90 + ? t`Unable to remove from list.` 91 + : t`Unable to add to list.`, 92 + ); 93 + } 94 + })(); 95 + }} 96 + > 97 + <Icon icon="check-circle" alt="☑️" /> 98 + <span>{list.title}</span> 99 + </button> 100 + </li> 101 + ); 102 + })} 103 + </ul> 104 + ) : uiState === 'loading' ? ( 105 + <p class="ui-state"> 106 + <Loader abrupt /> 107 + </p> 108 + ) : uiState === 'error' ? ( 109 + <p class="ui-state"> 110 + <Trans>Unable to load lists.</Trans> 111 + </p> 112 + ) : ( 113 + <p class="ui-state"> 114 + <Trans>No lists.</Trans> 115 + </p> 116 + )} 117 + <button 118 + type="button" 119 + class="plain2" 120 + onClick={() => setShowListAddEditModal(true)} 121 + disabled={uiState !== 'default'} 122 + > 123 + <Icon icon="plus" size="l" />{' '} 124 + <span> 125 + <Trans>New list</Trans> 126 + </span> 127 + </button> 128 + </main> 129 + {showListAddEditModal && ( 130 + <Modal 131 + onClick={(e) => { 132 + if (e.target === e.currentTarget) { 133 + setShowListAddEditModal(false); 134 + } 135 + }} 136 + > 137 + <ListAddEdit 138 + list={showListAddEditModal?.list} 139 + onClose={(result) => { 140 + if (result.state === 'success') { 141 + reload(); 142 + } 143 + setShowListAddEditModal(false); 144 + }} 145 + /> 146 + </Modal> 147 + )} 148 + </div> 149 + ); 150 + } 151 + 152 + export default AddRemoveListsSheet;
+336
src/components/edit-profile-sheet.jsx
··· 1 + import { Trans, useLingui } from '@lingui/react/macro'; 2 + import { useEffect, useRef, useState } from 'preact/hooks'; 3 + 4 + import { api } from '../utils/api'; 5 + import states from '../utils/states'; 6 + 7 + import Icon from './icon'; 8 + import Loader from './loader'; 9 + 10 + const SUPPORTED_IMAGE_FORMATS = [ 11 + 'image/jpeg', 12 + 'image/png', 13 + 'image/gif', 14 + 'image/webp', 15 + ]; 16 + const SUPPORTED_IMAGE_FORMATS_STR = SUPPORTED_IMAGE_FORMATS.join(','); 17 + 18 + function FieldsAttributesRow({ name, value, disabled, index: i }) { 19 + const [hasValue, setHasValue] = useState(!!value); 20 + return ( 21 + <tr> 22 + <td> 23 + <input 24 + type="text" 25 + name={`fields_attributes[${i}][name]`} 26 + defaultValue={name} 27 + disabled={disabled} 28 + maxLength={255} 29 + required={hasValue} 30 + dir="auto" 31 + /> 32 + </td> 33 + <td> 34 + <input 35 + type="text" 36 + name={`fields_attributes[${i}][value]`} 37 + defaultValue={value} 38 + disabled={disabled} 39 + maxLength={255} 40 + onChange={(e) => setHasValue(!!e.currentTarget.value)} 41 + dir="auto" 42 + /> 43 + </td> 44 + </tr> 45 + ); 46 + } 47 + 48 + function EditProfileSheet({ onClose = () => {} }) { 49 + const { t } = useLingui(); 50 + const { masto } = api(); 51 + const [uiState, setUIState] = useState('loading'); 52 + const [account, setAccount] = useState(null); 53 + const [headerPreview, setHeaderPreview] = useState(null); 54 + const [avatarPreview, setAvatarPreview] = useState(null); 55 + 56 + useEffect(() => { 57 + (async () => { 58 + try { 59 + const acc = await masto.v1.accounts.verifyCredentials(); 60 + setAccount(acc); 61 + setUIState('default'); 62 + } catch (e) { 63 + console.error(e); 64 + setUIState('error'); 65 + } 66 + })(); 67 + }, []); 68 + 69 + console.log('EditProfileSheet', account); 70 + const { displayName, source, avatar, header } = account || {}; 71 + const { note, fields } = source || {}; 72 + const fieldsAttributesRef = useRef(null); 73 + 74 + const avatarMediaAttachments = [ 75 + ...(avatar ? [{ type: 'image', url: avatar }] : []), 76 + ...(avatarPreview ? [{ type: 'image', url: avatarPreview }] : []), 77 + ]; 78 + const headerMediaAttachments = [ 79 + ...(header ? [{ type: 'image', url: header }] : []), 80 + ...(headerPreview ? [{ type: 'image', url: headerPreview }] : []), 81 + ]; 82 + 83 + return ( 84 + <div class="sheet" id="edit-profile-container"> 85 + {!!onClose && ( 86 + <button type="button" class="sheet-close" onClick={onClose}> 87 + <Icon icon="x" alt={t`Close`} /> 88 + </button> 89 + )} 90 + <header> 91 + <b> 92 + <Trans>Edit profile</Trans> 93 + </b> 94 + </header> 95 + <main> 96 + {uiState === 'loading' ? ( 97 + <p class="ui-state"> 98 + <Loader abrupt /> 99 + </p> 100 + ) : ( 101 + <form 102 + onSubmit={(e) => { 103 + e.preventDefault(); 104 + const formData = new FormData(e.target); 105 + const header = formData.get('header'); 106 + const avatar = formData.get('avatar'); 107 + const displayName = formData.get('display_name'); 108 + const note = formData.get('note'); 109 + const fieldsAttributesFields = 110 + fieldsAttributesRef.current.querySelectorAll( 111 + 'input[name^="fields_attributes"]', 112 + ); 113 + const fieldsAttributes = []; 114 + fieldsAttributesFields.forEach((field) => { 115 + const name = field.name; 116 + const [_, index, key] = 117 + name.match(/fields_attributes\[(\d+)\]\[(.+)\]/) || []; 118 + const value = field.value ? field.value.trim() : ''; 119 + if (index && key && value) { 120 + if (!fieldsAttributes[index]) fieldsAttributes[index] = {}; 121 + fieldsAttributes[index][key] = value; 122 + } 123 + }); 124 + // Fill in the blanks 125 + fieldsAttributes.forEach((field) => { 126 + if (field.name && !field.value) { 127 + field.value = ''; 128 + } 129 + }); 130 + 131 + (async () => { 132 + try { 133 + const newAccount = await masto.v1.accounts.updateCredentials({ 134 + header, 135 + avatar, 136 + displayName, 137 + note, 138 + fieldsAttributes, 139 + }); 140 + console.log('updated account', newAccount); 141 + onClose?.({ 142 + state: 'success', 143 + account: newAccount, 144 + }); 145 + } catch (e) { 146 + console.error(e); 147 + alert(e?.message || t`Unable to update profile.`); 148 + } 149 + })(); 150 + }} 151 + > 152 + <div class="edit-profile-media-container"> 153 + <label> 154 + <Trans>Header picture</Trans>{' '} 155 + <input 156 + type="file" 157 + name="header" 158 + accept={SUPPORTED_IMAGE_FORMATS_STR} 159 + onChange={(e) => { 160 + const file = e.target.files[0]; 161 + if (file) { 162 + const blob = URL.createObjectURL(file); 163 + setHeaderPreview(blob); 164 + } 165 + }} 166 + /> 167 + </label> 168 + <div class="edit-profile-media-field"> 169 + {header ? ( 170 + <div 171 + class="edit-media" 172 + tabIndex="0" 173 + onClick={() => { 174 + states.showMediaModal = { 175 + mediaAttachments: headerMediaAttachments, 176 + mediaIndex: 0, 177 + }; 178 + }} 179 + > 180 + <img src={header} alt="" /> 181 + </div> 182 + ) : ( 183 + <div class="edit-media"></div> 184 + )} 185 + {headerPreview && ( 186 + <> 187 + <Icon icon="arrow-right" /> 188 + <div 189 + class="edit-media" 190 + tabIndex="0" 191 + onClick={() => { 192 + states.showMediaModal = { 193 + mediaAttachments: headerMediaAttachments, 194 + mediaIndex: 1, 195 + }; 196 + }} 197 + > 198 + <img src={headerPreview} alt="" /> 199 + </div> 200 + </> 201 + )} 202 + </div> 203 + </div> 204 + <div class="edit-profile-media-container"> 205 + <label> 206 + <Trans>Profile picture</Trans>{' '} 207 + <input 208 + type="file" 209 + name="avatar" 210 + accept={SUPPORTED_IMAGE_FORMATS_STR} 211 + onChange={(e) => { 212 + const file = e.target.files[0]; 213 + if (file) { 214 + const blob = URL.createObjectURL(file); 215 + setAvatarPreview(blob); 216 + } 217 + }} 218 + /> 219 + </label> 220 + <div class="edit-profile-media-field"> 221 + {avatar ? ( 222 + <div 223 + class="edit-media" 224 + tabIndex="0" 225 + onClick={() => { 226 + states.showMediaModal = { 227 + mediaAttachments: avatarMediaAttachments, 228 + mediaIndex: 0, 229 + }; 230 + }} 231 + > 232 + <img src={avatar} alt="" /> 233 + </div> 234 + ) : ( 235 + <div class="edit-media"></div> 236 + )} 237 + {avatarPreview && ( 238 + <> 239 + <Icon icon="arrow-right" /> 240 + <div 241 + class="edit-media" 242 + tabIndex="0" 243 + onClick={() => { 244 + states.showMediaModal = { 245 + mediaAttachments: avatarMediaAttachments, 246 + mediaIndex: 1, 247 + }; 248 + }} 249 + > 250 + <img src={avatarPreview} alt="" /> 251 + </div> 252 + </> 253 + )} 254 + </div> 255 + </div> 256 + <p> 257 + <label> 258 + <Trans>Name</Trans>{' '} 259 + <input 260 + type="text" 261 + name="display_name" 262 + defaultValue={displayName} 263 + maxLength={30} 264 + disabled={uiState === 'loading'} 265 + dir="auto" 266 + /> 267 + </label> 268 + </p> 269 + <p> 270 + <label> 271 + <Trans>Bio</Trans> 272 + <textarea 273 + defaultValue={note} 274 + name="note" 275 + maxLength={500} 276 + rows="5" 277 + disabled={uiState === 'loading'} 278 + dir="auto" 279 + /> 280 + </label> 281 + </p> 282 + {/* Table for fields; name and values are in fields, min 4 rows */} 283 + <p> 284 + <Trans>Extra fields</Trans> 285 + </p> 286 + <table ref={fieldsAttributesRef}> 287 + <thead> 288 + <tr> 289 + <th> 290 + <Trans>Label</Trans> 291 + </th> 292 + <th> 293 + <Trans>Content</Trans> 294 + </th> 295 + </tr> 296 + </thead> 297 + <tbody> 298 + {Array.from({ length: Math.max(4, fields.length) }).map( 299 + (_, i) => { 300 + const { name = '', value = '' } = fields[i] || {}; 301 + return ( 302 + <FieldsAttributesRow 303 + key={i} 304 + name={name} 305 + value={value} 306 + index={i} 307 + disabled={uiState === 'loading'} 308 + /> 309 + ); 310 + }, 311 + )} 312 + </tbody> 313 + </table> 314 + <footer> 315 + <button 316 + type="button" 317 + class="light" 318 + disabled={uiState === 'loading'} 319 + onClick={() => { 320 + onClose?.(); 321 + }} 322 + > 323 + <Trans>Cancel</Trans> 324 + </button> 325 + <button type="submit" disabled={uiState === 'loading'}> 326 + <Trans>Save</Trans> 327 + </button> 328 + </footer> 329 + </form> 330 + )} 331 + </main> 332 + </div> 333 + ); 334 + } 335 + 336 + export default EditProfileSheet;
+107
src/components/endorsements.jsx
··· 1 + import { Trans } from '@lingui/react/macro'; 2 + import { useEffect, useRef, useState } from 'preact/hooks'; 3 + 4 + import { api } from '../utils/api'; 5 + import { fetchRelationships } from '../utils/relationships'; 6 + import supports from '../utils/supports'; 7 + 8 + import AccountBlock from './account-block'; 9 + import Loader from './loader'; 10 + 11 + const ENDORSEMENTS_LIMIT = 80; 12 + 13 + function Endorsements({ 14 + accountID: id, 15 + info, 16 + open = false, 17 + onlyOpenIfHasEndorsements = false, 18 + }) { 19 + const { masto } = api(); 20 + const endorsementsContainer = useRef(); 21 + const [endorsementsUIState, setEndorsementsUIState] = useState('default'); 22 + const [endorsements, setEndorsements] = useState([]); 23 + const [relationshipsMap, setRelationshipsMap] = useState({}); 24 + useEffect(() => { 25 + if (!supports('@mastodon/endorsements')) return; 26 + if (!open) return; 27 + (async () => { 28 + setEndorsementsUIState('loading'); 29 + try { 30 + const accounts = await masto.v1.accounts.$select(id).endorsements.list({ 31 + limit: ENDORSEMENTS_LIMIT, 32 + }); 33 + console.log({ endorsements: accounts }); 34 + if (!accounts.length) { 35 + setEndorsementsUIState('default'); 36 + return; 37 + } 38 + setEndorsements(accounts); 39 + setEndorsementsUIState('default'); 40 + setTimeout(() => { 41 + endorsementsContainer.current.scrollIntoView({ 42 + behavior: 'smooth', 43 + block: 'nearest', 44 + }); 45 + }, 300); 46 + 47 + const relationships = await fetchRelationships( 48 + accounts, 49 + relationshipsMap, 50 + ); 51 + if (relationships) { 52 + setRelationshipsMap(relationships); 53 + } 54 + } catch (e) { 55 + console.error(e); 56 + setEndorsementsUIState('error'); 57 + } 58 + })(); 59 + }, [open, id]); 60 + 61 + const reallyOpen = onlyOpenIfHasEndorsements 62 + ? open && endorsements.length > 0 63 + : open; 64 + 65 + if (!reallyOpen) return null; 66 + 67 + return ( 68 + <div class="shazam-container"> 69 + <div class="shazam-container-inner"> 70 + <div class="endorsements-container" ref={endorsementsContainer}> 71 + <h3> 72 + <Trans>Profiles featured by @{info.username}</Trans> 73 + </h3> 74 + {endorsementsUIState === 'loading' ? ( 75 + <p class="ui-state"> 76 + <Loader abrupt /> 77 + </p> 78 + ) : endorsements.length > 0 ? ( 79 + <ul 80 + class={`endorsements ${ 81 + endorsements.length > 10 ? 'expanded' : '' 82 + }`} 83 + > 84 + {endorsements.map((account) => ( 85 + <li> 86 + <AccountBlock 87 + key={account.id} 88 + account={account} 89 + showStats 90 + avatarSize="xxl" 91 + relationship={relationshipsMap[account.id]} 92 + /> 93 + </li> 94 + ))} 95 + </ul> 96 + ) : ( 97 + <p class="ui-state insignificant"> 98 + <Trans>No featured profiles.</Trans> 99 + </p> 100 + )} 101 + </div> 102 + </div> 103 + </div> 104 + ); 105 + } 106 + 107 + export default Endorsements;
+109
src/components/private-note-sheet.jsx
··· 1 + import { Trans, useLingui } from '@lingui/react/macro'; 2 + import { useEffect, useRef, useState } from 'preact/hooks'; 3 + 4 + import { api } from '../utils/api'; 5 + 6 + import Icon from './icon'; 7 + import Loader from './loader'; 8 + 9 + function PrivateNoteSheet({ 10 + account, 11 + note: initialNote, 12 + onRelationshipChange = () => {}, 13 + onClose = () => {}, 14 + }) { 15 + const { t } = useLingui(); 16 + const { masto } = api(); 17 + const [uiState, setUIState] = useState('default'); 18 + const textareaRef = useRef(null); 19 + 20 + useEffect(() => { 21 + let timer; 22 + if (textareaRef.current && !initialNote) { 23 + timer = setTimeout(() => { 24 + textareaRef.current.focus?.(); 25 + }, 100); 26 + } 27 + return () => { 28 + clearTimeout(timer); 29 + }; 30 + }, []); 31 + 32 + return ( 33 + <div class="sheet" id="private-note-container"> 34 + {!!onClose && ( 35 + <button type="button" class="sheet-close" onClick={onClose}> 36 + <Icon icon="x" alt={t`Close`} /> 37 + </button> 38 + )} 39 + <header> 40 + <b> 41 + <Trans> 42 + Private note about{' '} 43 + <span class="bidi-isolate"> 44 + @{account?.username || account?.acct} 45 + </span> 46 + </Trans> 47 + </b> 48 + </header> 49 + <main> 50 + <form 51 + onSubmit={(e) => { 52 + e.preventDefault(); 53 + const formData = new FormData(e.target); 54 + const note = formData.get('note'); 55 + if (note?.trim() !== initialNote?.trim()) { 56 + setUIState('loading'); 57 + (async () => { 58 + try { 59 + const newRelationship = await masto.v1.accounts 60 + .$select(account?.id) 61 + .note.create({ 62 + comment: note, 63 + }); 64 + console.log('updated relationship', newRelationship); 65 + setUIState('default'); 66 + onRelationshipChange(newRelationship); 67 + onClose(); 68 + } catch (e) { 69 + console.error(e); 70 + setUIState('error'); 71 + alert(e?.message || t`Unable to update private note.`); 72 + } 73 + })(); 74 + } 75 + }} 76 + > 77 + <textarea 78 + ref={textareaRef} 79 + name="note" 80 + disabled={uiState === 'loading'} 81 + dir="auto" 82 + > 83 + {initialNote} 84 + </textarea> 85 + <footer> 86 + <button 87 + type="button" 88 + class="light" 89 + disabled={uiState === 'loading'} 90 + onClick={() => { 91 + onClose?.(); 92 + }} 93 + > 94 + <Trans>Cancel</Trans> 95 + </button> 96 + <span> 97 + <Loader abrupt hidden={uiState !== 'loading'} /> 98 + <button disabled={uiState === 'loading'} type="submit"> 99 + <Trans>Save &amp; close</Trans> 100 + </button> 101 + </span> 102 + </footer> 103 + </form> 104 + </main> 105 + </div> 106 + ); 107 + } 108 + 109 + export default PrivateNoteSheet;
+889
src/components/related-actions.jsx
··· 1 + import { msg } from '@lingui/core/macro'; 2 + import { Trans, useLingui } from '@lingui/react/macro'; 3 + import { MenuDivider, MenuItem } from '@szhsin/react-menu'; 4 + import { useEffect, useRef, useState } from 'preact/hooks'; 5 + import punycode from 'punycode/'; 6 + 7 + import { api } from '../utils/api'; 8 + import i18nDuration from '../utils/i18n-duration'; 9 + import niceDateTime from '../utils/nice-date-time'; 10 + import showCompose from '../utils/show-compose'; 11 + import showToast from '../utils/show-toast'; 12 + import states from '../utils/states'; 13 + import { getCurrentAccountID, updateAccount } from '../utils/store-utils'; 14 + import supports from '../utils/supports'; 15 + 16 + import AddRemoveListsSheet from './add-remove-lists-sheet'; 17 + import Icon from './icon'; 18 + import Loader from './loader'; 19 + import MenuConfirm from './menu-confirm'; 20 + import Menu2 from './menu2'; 21 + import Modal from './modal'; 22 + import PrivateNoteSheet from './private-note-sheet'; 23 + import SubMenu2 from './submenu2'; 24 + import TranslatedBioSheet from './translated-bio-sheet'; 25 + 26 + const MUTE_DURATIONS = [ 27 + 60 * 5, // 5 minutes 28 + 60 * 30, // 30 minutes 29 + 60 * 60, // 1 hour 30 + 60 * 60 * 6, // 6 hours 31 + 60 * 60 * 24, // 1 day 32 + 60 * 60 * 24 * 3, // 3 days 33 + 60 * 60 * 24 * 7, // 1 week 34 + 60 * 60 * 24 * 30, // 30 days 35 + 0, // forever 36 + ]; 37 + const MUTE_DURATIONS_LABELS = { 38 + 0: msg`Forever`, 39 + 300: i18nDuration(5, 'minute'), 40 + 1_800: i18nDuration(30, 'minute'), 41 + 3_600: i18nDuration(1, 'hour'), 42 + 21_600: i18nDuration(6, 'hour'), 43 + 86_400: i18nDuration(1, 'day'), 44 + 259_200: i18nDuration(3, 'day'), 45 + 604_800: i18nDuration(1, 'week'), 46 + 2592_000: i18nDuration(30, 'day'), 47 + }; 48 + 49 + function RelatedActions({ 50 + info, 51 + instance, 52 + standalone, 53 + authenticated, 54 + onRelationshipChange = () => {}, 55 + setShowEditProfile = () => {}, 56 + showEndorsements = false, 57 + renderEndorsements = false, 58 + setRenderEndorsements = () => {}, 59 + }) { 60 + if (!info) return null; 61 + const { _, t } = useLingui(); 62 + const { 63 + masto: currentMasto, 64 + instance: currentInstance, 65 + authenticated: currentAuthenticated, 66 + } = api(); 67 + const sameInstance = instance === currentInstance; 68 + 69 + const [relationshipUIState, setRelationshipUIState] = useState('default'); 70 + const [relationship, setRelationship] = useState(null); 71 + 72 + const { id, acct, url, username, locked, lastStatusAt, note, fields, moved } = 73 + info; 74 + const accountID = useRef(id); 75 + 76 + const { 77 + following, 78 + showingReblogs, 79 + notifying, 80 + followedBy, 81 + blocking, 82 + blockedBy, 83 + muting, 84 + mutingNotifications, 85 + requested, 86 + domainBlocking, 87 + endorsed, 88 + note: privateNote, 89 + } = relationship || {}; 90 + 91 + const [currentInfo, setCurrentInfo] = useState(null); 92 + const [isSelf, setIsSelf] = useState(false); 93 + 94 + const acctWithInstance = acct.includes('@') ? acct : `${acct}@${instance}`; 95 + 96 + const supportsEndorsements = supports('@mastodon/endorsements'); 97 + 98 + useEffect(() => { 99 + if (info) { 100 + const currentAccount = getCurrentAccountID(); 101 + let currentID; 102 + (async () => { 103 + if (sameInstance && authenticated) { 104 + currentID = id; 105 + } else if (!sameInstance && currentAuthenticated) { 106 + // Grab this account from my logged-in instance 107 + const acctHasInstance = info.acct.includes('@'); 108 + try { 109 + const results = await currentMasto.v2.search.list({ 110 + q: acctHasInstance ? info.acct : `${info.username}@${instance}`, 111 + type: 'accounts', 112 + limit: 1, 113 + resolve: true, 114 + }); 115 + console.log('🥏 Fetched account from logged-in instance', results); 116 + if (results.accounts.length) { 117 + currentID = results.accounts[0].id; 118 + setCurrentInfo(results.accounts[0]); 119 + } 120 + } catch (e) { 121 + console.error(e); 122 + } 123 + } 124 + 125 + if (!currentID) return; 126 + 127 + if (currentAccount === currentID) { 128 + // It's myself! 129 + setIsSelf(true); 130 + return; 131 + } 132 + 133 + accountID.current = currentID; 134 + 135 + // if (moved) return; 136 + 137 + setRelationshipUIState('loading'); 138 + 139 + const fetchRelationships = currentMasto.v1.accounts.relationships.fetch( 140 + { 141 + id: [currentID], 142 + }, 143 + ); 144 + 145 + try { 146 + const relationships = await fetchRelationships; 147 + console.log('fetched relationship', relationships); 148 + setRelationshipUIState('default'); 149 + 150 + if (relationships.length) { 151 + const relationship = relationships[0]; 152 + setRelationship(relationship); 153 + onRelationshipChange({ relationship, currentID }); 154 + } 155 + } catch (e) { 156 + console.error(e); 157 + setRelationshipUIState('error'); 158 + } 159 + })(); 160 + } 161 + }, [info, authenticated]); 162 + 163 + useEffect(() => { 164 + if (info && isSelf) { 165 + updateAccount(info); 166 + } 167 + }, [info, isSelf]); 168 + 169 + const loading = relationshipUIState === 'loading'; 170 + 171 + const [showTranslatedBio, setShowTranslatedBio] = useState(false); 172 + const [showAddRemoveLists, setShowAddRemoveLists] = useState(false); 173 + const [showPrivateNoteModal, setShowPrivateNoteModal] = useState(false); 174 + const [lists, setLists] = useState([]); 175 + 176 + return ( 177 + <> 178 + <div class="actions"> 179 + <span> 180 + {followedBy ? ( 181 + <span class="tag"> 182 + <Trans>Follows you</Trans> 183 + </span> 184 + ) : !!lastStatusAt ? ( 185 + <small class="insignificant"> 186 + <Trans> 187 + Last post:{' '} 188 + <span class="ib"> 189 + {niceDateTime(lastStatusAt, { 190 + hideTime: true, 191 + })} 192 + </span> 193 + </Trans> 194 + </small> 195 + ) : ( 196 + <span /> 197 + )} 198 + {muting && ( 199 + <span class="tag danger"> 200 + <Trans>Muted</Trans> 201 + </span> 202 + )} 203 + {blocking && ( 204 + <span class="tag danger"> 205 + <Trans>Blocked</Trans> 206 + </span> 207 + )} 208 + </span>{' '} 209 + <span class="buttons"> 210 + {!!privateNote && ( 211 + <button 212 + type="button" 213 + class="private-note-tag" 214 + title={t`Private note`} 215 + onClick={() => { 216 + setShowPrivateNoteModal(true); 217 + }} 218 + dir="auto" 219 + > 220 + <span>{privateNote}</span> 221 + </button> 222 + )} 223 + <Menu2 224 + portal={{ 225 + target: document.body, 226 + }} 227 + containerProps={{ 228 + style: { 229 + // Higher than the backdrop 230 + zIndex: 1001, 231 + }, 232 + }} 233 + align="center" 234 + position="anchor" 235 + overflow="auto" 236 + menuButton={ 237 + <button type="button" class="plain" disabled={loading}> 238 + <Icon icon="more" size="l" alt={t`More`} /> 239 + </button> 240 + } 241 + onMenuChange={(e) => { 242 + if (following && e.open) { 243 + // Fetch lists that have this account 244 + (async () => { 245 + try { 246 + const lists = await currentMasto.v1.accounts 247 + .$select(accountID.current) 248 + .lists.list(); 249 + console.log('fetched account lists', lists); 250 + setLists(lists); 251 + } catch (e) { 252 + console.error(e); 253 + } 254 + })(); 255 + } 256 + }} 257 + > 258 + {currentAuthenticated && !isSelf ? ( 259 + <> 260 + <MenuItem 261 + onClick={() => { 262 + showCompose({ 263 + draftStatus: { 264 + status: `@${currentInfo?.acct || acct} `, 265 + }, 266 + }); 267 + }} 268 + > 269 + <Icon icon="at" /> 270 + <span> 271 + <Trans> 272 + Mention <span class="bidi-isolate">@{username}</span> 273 + </Trans> 274 + </span> 275 + </MenuItem> 276 + <MenuItem 277 + onClick={() => { 278 + setShowTranslatedBio(true); 279 + }} 280 + > 281 + <Icon icon="translate" /> 282 + <span> 283 + <Trans>Translate bio</Trans> 284 + </span> 285 + </MenuItem> 286 + {supports('@mastodon/profile-private-note') && ( 287 + <MenuItem 288 + onClick={() => { 289 + setShowPrivateNoteModal(true); 290 + }} 291 + > 292 + <Icon icon="pencil" /> 293 + <span> 294 + {privateNote ? t`Edit private note` : t`Add private note`} 295 + </span> 296 + </MenuItem> 297 + )} 298 + {following && !!relationship && ( 299 + <> 300 + <MenuItem 301 + onClick={() => { 302 + setRelationshipUIState('loading'); 303 + (async () => { 304 + try { 305 + const rel = await currentMasto.v1.accounts 306 + .$select(accountID.current) 307 + .follow({ 308 + notify: !notifying, 309 + }); 310 + if (rel) setRelationship(rel); 311 + setRelationshipUIState('default'); 312 + showToast( 313 + rel.notifying 314 + ? t`Notifications enabled for @${username}'s posts.` 315 + : t` Notifications disabled for @${username}'s posts.`, 316 + ); 317 + } catch (e) { 318 + alert(e); 319 + setRelationshipUIState('error'); 320 + } 321 + })(); 322 + }} 323 + > 324 + <Icon icon="notification" /> 325 + <span> 326 + {notifying 327 + ? t`Disable notifications` 328 + : t`Enable notifications`} 329 + </span> 330 + </MenuItem> 331 + <MenuItem 332 + onClick={() => { 333 + setRelationshipUIState('loading'); 334 + (async () => { 335 + try { 336 + const rel = await currentMasto.v1.accounts 337 + .$select(accountID.current) 338 + .follow({ 339 + reblogs: !showingReblogs, 340 + }); 341 + if (rel) setRelationship(rel); 342 + setRelationshipUIState('default'); 343 + showToast( 344 + rel.showingReblogs 345 + ? t`Boosts from @${username} enabled.` 346 + : t`Boosts from @${username} disabled.`, 347 + ); 348 + } catch (e) { 349 + alert(e); 350 + setRelationshipUIState('error'); 351 + } 352 + })(); 353 + }} 354 + > 355 + <Icon icon="rocket" /> 356 + <span> 357 + {showingReblogs ? t`Disable boosts` : t`Enable boosts`} 358 + </span> 359 + </MenuItem> 360 + </> 361 + )} 362 + {supportsEndorsements && following && ( 363 + <MenuItem 364 + onClick={() => { 365 + setRelationshipUIState('loading'); 366 + (async () => { 367 + try { 368 + if (endorsed) { 369 + const newRelationship = 370 + await currentMasto.v1.accounts 371 + .$select(currentInfo?.id || id) 372 + .unpin(); 373 + setRelationship(newRelationship); 374 + setRelationshipUIState('default'); 375 + showToast( 376 + t`@${username} is no longer featured on your profile.`, 377 + ); 378 + } else { 379 + const newRelationship = 380 + await currentMasto.v1.accounts 381 + .$select(currentInfo?.id || id) 382 + .pin(); 383 + setRelationship(newRelationship); 384 + setRelationshipUIState('default'); 385 + showToast( 386 + t`@${username} is now featured on your profile.`, 387 + ); 388 + } 389 + } catch (e) { 390 + console.error(e); 391 + setRelationshipUIState('error'); 392 + if (endorsed) { 393 + showToast( 394 + t`Unable to unfeature @${username} on your profile.`, 395 + ); 396 + } else { 397 + showToast( 398 + t`Unable to feature @${username} on your profile.`, 399 + ); 400 + } 401 + } 402 + })(); 403 + }} 404 + > 405 + <Icon icon="endorsement" /> 406 + {endorsed 407 + ? t`Don't feature on profile` 408 + : t`Feature on profile`} 409 + </MenuItem> 410 + )} 411 + {showEndorsements && 412 + supportsEndorsements && 413 + !renderEndorsements && ( 414 + <MenuItem onClick={() => setRenderEndorsements(true)}> 415 + <Icon icon="endorsement" /> 416 + <span> 417 + <Trans>Show featured profiles</Trans> 418 + </span> 419 + </MenuItem> 420 + )} 421 + {/* Add/remove from lists is only possible if following the account */} 422 + {following && ( 423 + <MenuItem 424 + onClick={() => { 425 + setShowAddRemoveLists(true); 426 + }} 427 + > 428 + <Icon icon="list" /> 429 + {lists.length ? ( 430 + <> 431 + <small class="menu-grow"> 432 + <Trans>Add/Remove from Lists</Trans> 433 + <br /> 434 + <span class="more-insignificant"> 435 + {lists.map((list) => list.title).join(', ')} 436 + </span> 437 + </small> 438 + <small class="more-insignificant">{lists.length}</small> 439 + </> 440 + ) : ( 441 + <span> 442 + <Trans>Add/Remove from Lists</Trans> 443 + </span> 444 + )} 445 + </MenuItem> 446 + )} 447 + <MenuDivider /> 448 + </> 449 + ) : ( 450 + supportsEndorsements && 451 + !renderEndorsements && ( 452 + <> 453 + <MenuItem onClick={() => setRenderEndorsements(true)}> 454 + <Icon icon="endorsement" /> 455 + Show featured profiles 456 + </MenuItem> 457 + <MenuDivider /> 458 + </> 459 + ) 460 + )} 461 + <MenuItem 462 + onClick={() => { 463 + const handle = `@${currentInfo?.acct || acctWithInstance}`; 464 + try { 465 + navigator.clipboard.writeText(handle); 466 + showToast(t`Handle copied`); 467 + } catch (e) { 468 + console.error(e); 469 + showToast(t`Unable to copy handle`); 470 + } 471 + }} 472 + > 473 + <Icon icon="link" /> 474 + <span> 475 + <Trans>Copy handle</Trans> 476 + </span> 477 + </MenuItem> 478 + <MenuItem href={url} target="_blank"> 479 + <Icon icon="external" /> 480 + <small class="menu-double-lines">{niceAccountURL(url)}</small> 481 + </MenuItem> 482 + {currentAuthenticated && !isSelf && ( 483 + <> 484 + <MenuDivider /> 485 + {!muting && ( 486 + <SubMenu2 487 + openTrigger="clickOnly" 488 + direction="left" 489 + overflow="auto" 490 + shift={16} 491 + menuClassName="menu-blur" 492 + menuButton={ 493 + <> 494 + <span class="menu-grow"> 495 + <Trans> 496 + Mute <span class="bidi-isolate">@{username}</span>… 497 + </Trans> 498 + </span> 499 + <span 500 + style={{ 501 + textOverflow: 'clip', 502 + }} 503 + > 504 + <Icon icon="time" /> 505 + <Icon icon="chevron-right" /> 506 + </span> 507 + </> 508 + } 509 + > 510 + <div class="menu-wrap"> 511 + {MUTE_DURATIONS.map((duration) => ( 512 + <MenuItem 513 + onClick={() => { 514 + setRelationshipUIState('loading'); 515 + (async () => { 516 + try { 517 + const newRelationship = 518 + await currentMasto.v1.accounts 519 + .$select(currentInfo?.id || id) 520 + .mute({ 521 + duration, 522 + }); 523 + console.log('muting', newRelationship); 524 + setRelationship(newRelationship); 525 + setRelationshipUIState('default'); 526 + showToast( 527 + t`Muted @${username} for ${ 528 + typeof MUTE_DURATIONS_LABELS[duration] === 529 + 'function' 530 + ? MUTE_DURATIONS_LABELS[duration]() 531 + : _(MUTE_DURATIONS_LABELS[duration]) 532 + }`, 533 + ); 534 + states.reloadGenericAccounts.id = 'mute'; 535 + states.reloadGenericAccounts.counter++; 536 + } catch (e) { 537 + console.error(e); 538 + setRelationshipUIState('error'); 539 + showToast(t`Unable to mute @${username}`); 540 + } 541 + })(); 542 + }} 543 + > 544 + {typeof MUTE_DURATIONS_LABELS[duration] === 'function' 545 + ? MUTE_DURATIONS_LABELS[duration]() 546 + : _(MUTE_DURATIONS_LABELS[duration])} 547 + </MenuItem> 548 + ))} 549 + </div> 550 + </SubMenu2> 551 + )} 552 + {followedBy && ( 553 + <MenuConfirm 554 + subMenu 555 + menuItemClassName="danger" 556 + confirmLabel={ 557 + <> 558 + <Icon icon="user-x" /> 559 + <span> 560 + <Trans> 561 + Remove <span class="bidi-isolate">@{username}</span>{' '} 562 + from followers? 563 + </Trans> 564 + </span> 565 + </> 566 + } 567 + onClick={() => { 568 + setRelationshipUIState('loading'); 569 + (async () => { 570 + try { 571 + const newRelationship = await currentMasto.v1.accounts 572 + .$select(currentInfo?.id || id) 573 + .removeFromFollowers(); 574 + console.log( 575 + 'removing from followers', 576 + newRelationship, 577 + ); 578 + setRelationship(newRelationship); 579 + setRelationshipUIState('default'); 580 + showToast(t`@${username} removed from followers`); 581 + states.reloadGenericAccounts.id = 'followers'; 582 + states.reloadGenericAccounts.counter++; 583 + } catch (e) { 584 + console.error(e); 585 + setRelationshipUIState('error'); 586 + } 587 + })(); 588 + }} 589 + > 590 + <Icon icon="user-x" /> 591 + <span> 592 + <Trans>Remove follower…</Trans> 593 + </span> 594 + </MenuConfirm> 595 + )} 596 + <MenuConfirm 597 + subMenu 598 + confirm={!blocking} 599 + confirmLabel={ 600 + <> 601 + <Icon icon="block" /> 602 + <span> 603 + <Trans> 604 + Block <span class="bidi-isolate">@{username}</span>? 605 + </Trans> 606 + </span> 607 + </> 608 + } 609 + itemProps={{ 610 + className: 'danger', 611 + }} 612 + menuItemClassName="danger" 613 + onClick={() => { 614 + // if (!blocking && !confirm(`Block @${username}?`)) { 615 + // return; 616 + // } 617 + setRelationshipUIState('loading'); 618 + (async () => { 619 + try { 620 + if (blocking) { 621 + const newRelationship = await currentMasto.v1.accounts 622 + .$select(currentInfo?.id || id) 623 + .unblock(); 624 + console.log('unblocking', newRelationship); 625 + setRelationship(newRelationship); 626 + setRelationshipUIState('default'); 627 + showToast(t`Unblocked @${username}`); 628 + } else { 629 + const newRelationship = await currentMasto.v1.accounts 630 + .$select(currentInfo?.id || id) 631 + .block(); 632 + console.log('blocking', newRelationship); 633 + setRelationship(newRelationship); 634 + setRelationshipUIState('default'); 635 + showToast(t`Blocked @${username}`); 636 + } 637 + states.reloadGenericAccounts.id = 'block'; 638 + states.reloadGenericAccounts.counter++; 639 + } catch (e) { 640 + console.error(e); 641 + setRelationshipUIState('error'); 642 + if (blocking) { 643 + showToast(t`Unable to unblock @${username}`); 644 + } else { 645 + showToast(t`Unable to block @${username}`); 646 + } 647 + } 648 + })(); 649 + }} 650 + > 651 + {blocking ? ( 652 + <> 653 + <Icon icon="unblock" /> 654 + <span> 655 + <Trans> 656 + Unblock <span class="bidi-isolate">@{username}</span> 657 + </Trans> 658 + </span> 659 + </> 660 + ) : ( 661 + <> 662 + <Icon icon="block" /> 663 + <span> 664 + <Trans> 665 + Block <span class="bidi-isolate">@{username}</span>… 666 + </Trans> 667 + </span> 668 + </> 669 + )} 670 + </MenuConfirm> 671 + <MenuItem 672 + className="danger" 673 + onClick={() => { 674 + states.showReportModal = { 675 + account: currentInfo || info, 676 + }; 677 + }} 678 + > 679 + <Icon icon="flag" /> 680 + <span> 681 + <Trans> 682 + Report <span class="bidi-isolate">@{username}</span>… 683 + </Trans> 684 + </span> 685 + </MenuItem> 686 + </> 687 + )} 688 + {currentAuthenticated && 689 + isSelf && 690 + standalone && 691 + supports('@mastodon/profile-edit') && ( 692 + <> 693 + <MenuDivider /> 694 + <MenuItem 695 + onClick={() => { 696 + setShowEditProfile(true); 697 + }} 698 + > 699 + <Icon icon="pencil" /> 700 + <span> 701 + <Trans>Edit profile</Trans> 702 + </span> 703 + </MenuItem> 704 + </> 705 + )} 706 + {import.meta.env.DEV && currentAuthenticated && isSelf && ( 707 + <> 708 + <MenuDivider /> 709 + <MenuItem 710 + onClick={async () => { 711 + const relationships = 712 + await currentMasto.v1.accounts.relationships.fetch({ 713 + id: [accountID.current], 714 + }); 715 + const { note } = relationships[0] || {}; 716 + if (note) { 717 + alert(note); 718 + console.log(note); 719 + } 720 + }} 721 + > 722 + <Icon icon="pencil" /> 723 + <span>See note</span> 724 + </MenuItem> 725 + </> 726 + )} 727 + </Menu2> 728 + {!relationship && relationshipUIState === 'loading' && ( 729 + <Loader abrupt /> 730 + )} 731 + {!!relationship && !moved && ( 732 + <MenuConfirm 733 + confirm={following || requested} 734 + confirmLabel={ 735 + <span> 736 + {requested 737 + ? t`Withdraw follow request?` 738 + : t`Unfollow @${info.acct || info.username}?`} 739 + </span> 740 + } 741 + menuItemClassName="danger" 742 + align="end" 743 + disabled={loading} 744 + onClick={() => { 745 + setRelationshipUIState('loading'); 746 + (async () => { 747 + try { 748 + let newRelationship; 749 + 750 + if (following || requested) { 751 + // const yes = confirm( 752 + // requested 753 + // ? 'Withdraw follow request?' 754 + // : `Unfollow @${info.acct || info.username}?`, 755 + // ); 756 + 757 + // if (yes) { 758 + newRelationship = await currentMasto.v1.accounts 759 + .$select(accountID.current) 760 + .unfollow(); 761 + // } 762 + } else { 763 + newRelationship = await currentMasto.v1.accounts 764 + .$select(accountID.current) 765 + .follow(); 766 + } 767 + 768 + if (newRelationship) { 769 + setRelationship(newRelationship); 770 + 771 + // Show endorsements if start following 772 + if ( 773 + showEndorsements && 774 + supportsEndorsements && 775 + !renderEndorsements && 776 + newRelationship.following 777 + ) { 778 + setRenderEndorsements('onlyOpenIfHasEndorsements'); 779 + } 780 + } 781 + setRelationshipUIState('default'); 782 + } catch (e) { 783 + alert(e); 784 + setRelationshipUIState('error'); 785 + } 786 + })(); 787 + }} 788 + > 789 + <button 790 + type="button" 791 + class={`${following || requested ? 'light swap' : ''}`} 792 + data-swap-state={following || requested ? 'danger' : ''} 793 + disabled={loading} 794 + > 795 + {following ? ( 796 + <> 797 + <span> 798 + <Trans>Following</Trans> 799 + </span> 800 + <span> 801 + <Trans>Unfollow…</Trans> 802 + </span> 803 + </> 804 + ) : requested ? ( 805 + <> 806 + <span> 807 + <Trans>Requested</Trans> 808 + </span> 809 + <span> 810 + <Trans>Withdraw…</Trans> 811 + </span> 812 + </> 813 + ) : locked ? ( 814 + <> 815 + <Icon icon="lock" />{' '} 816 + <span> 817 + <Trans>Follow</Trans> 818 + </span> 819 + </> 820 + ) : ( 821 + t`Follow` 822 + )} 823 + </button> 824 + </MenuConfirm> 825 + )} 826 + </span> 827 + </div> 828 + {!!showTranslatedBio && ( 829 + <Modal 830 + onClose={() => { 831 + setShowTranslatedBio(false); 832 + }} 833 + > 834 + <TranslatedBioSheet 835 + note={note} 836 + fields={fields} 837 + onClose={() => setShowTranslatedBio(false)} 838 + /> 839 + </Modal> 840 + )} 841 + {!!showAddRemoveLists && ( 842 + <Modal 843 + onClose={() => { 844 + setShowAddRemoveLists(false); 845 + }} 846 + > 847 + <AddRemoveListsSheet 848 + accountID={accountID.current} 849 + onClose={() => setShowAddRemoveLists(false)} 850 + /> 851 + </Modal> 852 + )} 853 + {!!showPrivateNoteModal && ( 854 + <Modal 855 + onClose={() => { 856 + setShowPrivateNoteModal(false); 857 + }} 858 + > 859 + <PrivateNoteSheet 860 + account={info} 861 + note={privateNote} 862 + onRelationshipChange={(relationship) => { 863 + setRelationship(relationship); 864 + // onRelationshipChange({ relationship, currentID: accountID.current }); 865 + }} 866 + onClose={() => setShowPrivateNoteModal(false)} 867 + /> 868 + </Modal> 869 + )} 870 + </> 871 + ); 872 + } 873 + 874 + function niceAccountURL(url) { 875 + if (!url) return; 876 + const urlObj = URL.parse(url); 877 + if (!urlObj) return; 878 + const { host, pathname } = urlObj; 879 + const path = pathname.replace(/\/$/, '').replace(/^\//, ''); 880 + return ( 881 + <> 882 + <span class="more-insignificant">{punycode.toUnicode(host)}/</span> 883 + <wbr /> 884 + <span>{path}</span> 885 + </> 886 + ); 887 + } 888 + 889 + export default RelatedActions;
+43
src/components/translated-bio-sheet.jsx
··· 1 + import { Trans, useLingui } from '@lingui/react/macro'; 2 + 3 + import getHTMLText from '../utils/getHTMLText'; 4 + 5 + import Icon from './icon'; 6 + import TranslationBlock from './translation-block'; 7 + 8 + function TranslatedBioSheet({ note, fields, onClose }) { 9 + const { t } = useLingui(); 10 + const fieldsText = 11 + fields 12 + ?.map(({ name, value }) => `${name}\n${getHTMLText(value)}`) 13 + .join('\n\n') || ''; 14 + 15 + const text = getHTMLText(note) + (fieldsText ? `\n\n${fieldsText}` : ''); 16 + 17 + return ( 18 + <div class="sheet"> 19 + {!!onClose && ( 20 + <button type="button" class="sheet-close" onClick={onClose}> 21 + <Icon icon="x" alt={t`Close`} /> 22 + </button> 23 + )} 24 + <header> 25 + <h2> 26 + <Trans>Translated Bio</Trans> 27 + </h2> 28 + </header> 29 + <main> 30 + <p 31 + style={{ 32 + whiteSpace: 'pre-wrap', 33 + }} 34 + > 35 + {text} 36 + </p> 37 + <TranslationBlock forceTranslate text={text} /> 38 + </main> 39 + </div> 40 + ); 41 + } 42 + 43 + export default TranslatedBioSheet;
+345 -359
src/locales/en.po
··· 28 28 msgstr "" 29 29 30 30 #: src/components/account-block.jsx:183 31 - #: src/components/account-info.jsx:705 31 + #: src/components/account-info.jsx:670 32 32 msgid "Automated" 33 33 msgstr "" 34 34 35 35 #: src/components/account-block.jsx:190 36 - #: src/components/account-info.jsx:710 36 + #: src/components/account-info.jsx:675 37 37 #: src/components/status.jsx:519 38 38 msgid "Group" 39 39 msgstr "" ··· 43 43 msgstr "" 44 44 45 45 #: src/components/account-block.jsx:204 46 - #: src/components/account-info.jsx:1912 46 + #: src/components/related-actions.jsx:807 47 47 msgid "Requested" 48 48 msgstr "" 49 49 50 50 #: src/components/account-block.jsx:208 51 - #: src/components/account-info.jsx:1903 51 + #: src/components/related-actions.jsx:798 52 52 msgid "Following" 53 53 msgstr "" 54 54 55 55 #: src/components/account-block.jsx:212 56 - #: src/components/account-info.jsx:1212 56 + #: src/components/related-actions.jsx:182 57 57 msgid "Follows you" 58 58 msgstr "" 59 59 ··· 62 62 msgstr "" 63 63 64 64 #: src/components/account-block.jsx:229 65 - #: src/components/account-info.jsx:753 65 + #: src/components/account-info.jsx:718 66 66 msgid "Verified" 67 67 msgstr "" 68 68 69 69 #. placeholder {0}: niceDateTime(createdAt, { hideTime: true, }) 70 70 #. placeholder {0}: niceDateTime(createdAt, { hideTime: true, }) 71 71 #: src/components/account-block.jsx:244 72 - #: src/components/account-info.jsx:892 72 + #: src/components/account-info.jsx:857 73 73 msgid "Joined <0>{0}</0>" 74 74 msgstr "" 75 75 76 - #: src/components/account-info.jsx:64 77 - msgid "Forever" 76 + #: src/components/account-handle-info.jsx:18 77 + msgid "username" 78 78 msgstr "" 79 79 80 - #: src/components/account-info.jsx:398 81 - msgid "Unable to load account." 80 + #: src/components/account-handle-info.jsx:22 81 + msgid "server domain name" 82 82 msgstr "" 83 83 84 - #: src/components/account-info.jsx:413 84 + #: src/components/account-info.jsx:363 85 + msgid "Unable to load account." 86 + msgstr "Unable to load account." 87 + 88 + #: src/components/account-info.jsx:378 85 89 msgid "Go to account page" 86 - msgstr "" 90 + msgstr "Go to account page" 87 91 88 - #: src/components/account-info.jsx:442 89 - #: src/components/account-info.jsx:775 92 + #: src/components/account-info.jsx:407 93 + #: src/components/account-info.jsx:740 90 94 msgid "Followers" 91 - msgstr "" 95 + msgstr "Followers" 92 96 93 97 #. js-lingui-explicit-id 94 - #: src/components/account-info.jsx:446 95 - #: src/components/account-info.jsx:830 98 + #: src/components/account-info.jsx:411 99 + #: src/components/account-info.jsx:795 96 100 msgid "following.stats" 97 101 msgstr "Following" 98 102 99 - #: src/components/account-info.jsx:449 103 + #: src/components/account-info.jsx:414 100 104 #: src/pages/account-statuses.jsx:495 101 105 #: src/pages/search.jsx:345 102 106 #: src/pages/search.jsx:492 103 107 msgid "Posts" 104 108 msgstr "" 105 109 106 - #: src/components/account-info.jsx:457 107 - #: src/components/account-info.jsx:1268 110 + #: src/components/account-info.jsx:422 108 111 #: src/components/media-alt-modal.jsx:55 109 112 #: src/components/media-attachment.jsx:380 110 113 #: src/components/media-modal.jsx:363 114 + #: src/components/related-actions.jsx:238 111 115 #: src/components/status.jsx:1771 112 116 #: src/components/status.jsx:1788 113 117 #: src/components/status.jsx:1919 ··· 124 128 msgid "More" 125 129 msgstr "" 126 130 127 - #: src/components/account-info.jsx:469 131 + #: src/components/account-info.jsx:434 128 132 msgid "<0>{displayName}</0> has indicated that their new account is now:" 129 - msgstr "" 133 + msgstr "<0>{displayName}</0> has indicated that their new account is now:" 130 134 131 - #: src/components/account-info.jsx:614 132 - #: src/components/account-info.jsx:1496 135 + #: src/components/account-info.jsx:579 136 + #: src/components/related-actions.jsx:466 133 137 msgid "Handle copied" 134 138 msgstr "Handle copied" 135 139 136 - #: src/components/account-info.jsx:617 137 - #: src/components/account-info.jsx:1499 140 + #: src/components/account-info.jsx:582 141 + #: src/components/related-actions.jsx:469 138 142 msgid "Unable to copy handle" 139 143 msgstr "Unable to copy handle" 140 144 141 - #: src/components/account-info.jsx:623 142 - #: src/components/account-info.jsx:1505 145 + #: src/components/account-info.jsx:588 146 + #: src/components/related-actions.jsx:475 143 147 msgid "Copy handle" 144 148 msgstr "" 145 149 146 - #: src/components/account-info.jsx:629 150 + #: src/components/account-info.jsx:594 147 151 msgid "Go to original profile page" 148 152 msgstr "" 149 153 150 - #: src/components/account-info.jsx:647 154 + #: src/components/account-info.jsx:612 151 155 msgid "View profile image" 152 - msgstr "" 156 + msgstr "View profile image" 153 157 154 - #: src/components/account-info.jsx:665 158 + #: src/components/account-info.jsx:630 155 159 msgid "View profile header" 156 - msgstr "" 160 + msgstr "View profile header" 157 161 158 - #: src/components/account-info.jsx:681 159 - #: src/components/account-info.jsx:1806 160 - #: src/components/account-info.jsx:2337 162 + #: src/components/account-info.jsx:646 163 + #: src/components/edit-profile-sheet.jsx:92 164 + #: src/components/related-actions.jsx:701 161 165 msgid "Edit profile" 162 166 msgstr "" 163 167 164 - #: src/components/account-info.jsx:700 168 + #: src/components/account-info.jsx:665 165 169 msgid "In Memoriam" 166 170 msgstr "" 167 171 168 - #: src/components/account-info.jsx:782 169 - #: src/components/account-info.jsx:840 172 + #: src/components/account-info.jsx:747 173 + #: src/components/account-info.jsx:805 170 174 msgid "This user has chosen to not make this information available." 171 175 msgstr "This user has chosen to not make this information available." 172 176 173 177 #. placeholder {0}: shortenNumber(followersCount) 174 178 #. placeholder {1}: shortenNumber(followersCount) 175 - #: src/components/account-info.jsx:802 179 + #: src/components/account-info.jsx:767 176 180 msgid "{followersCount, plural, one {<0>{0}</0> Follower} other {<1>{1}</1> Followers}}" 177 181 msgstr "{followersCount, plural, one {<0>{0}</0> Follower} other {<1>{1}</1> Followers}}" 178 182 179 183 #. placeholder {0}: shortenNumber(followingCount) 180 - #: src/components/account-info.jsx:846 184 + #: src/components/account-info.jsx:811 181 185 msgid "{followingCount, plural, other {<0>{0}</0> Following}}" 182 186 msgstr "{followingCount, plural, other {<0>{0}</0> Following}}" 183 187 184 188 #. placeholder {0}: shortenNumber(statusesCount) 185 189 #. placeholder {1}: shortenNumber(statusesCount) 186 - #: src/components/account-info.jsx:870 190 + #: src/components/account-info.jsx:835 187 191 msgid "{statusesCount, plural, one {<0>{0}</0> Post} other {<1>{1}</1> Posts}}" 188 192 msgstr "{statusesCount, plural, one {<0>{0}</0> Post} other {<1>{1}</1> Posts}}" 189 193 190 194 #. placeholder {0}: ( postingStats.originals / postingStats.total ).toLocaleString(i18n.locale || undefined, { style: 'percent', }) 191 195 #. placeholder {1}: ( postingStats.replies / postingStats.total ).toLocaleString(i18n.locale || undefined, { style: 'percent', }) 192 196 #. placeholder {2}: ( postingStats.boosts / postingStats.total ).toLocaleString(i18n.locale || undefined, { style: 'percent', }) 193 - #: src/components/account-info.jsx:917 197 + #: src/components/account-info.jsx:882 194 198 msgid "{0} original posts, {1} replies, {2} boosts" 195 199 msgstr "{0} original posts, {1} replies, {2} boosts" 196 200 ··· 201 205 #. placeholder {4}: postingStats.total 202 206 #. placeholder {5}: postingStats.total 203 207 #. placeholder {6}: postingStats.daysSinceLastPost 204 - #: src/components/account-info.jsx:933 208 + #: src/components/account-info.jsx:898 205 209 msgid "{0, plural, one {{1, plural, one {Last 1 post in the past 1 day} other {Last 1 post in the past {2} days}}} other {{3, plural, one {Last {4} posts in the past 1 day} other {Last {5} posts in the past {6} days}}}}" 206 - msgstr "" 210 + msgstr "{0, plural, one {{1, plural, one {Last 1 post in the past 1 day} other {Last 1 post in the past {2} days}}} other {{3, plural, one {Last {4} posts in the past 1 day} other {Last {5} posts in the past {6} days}}}}" 207 211 208 212 #. placeholder {0}: postingStats.total 209 213 #. placeholder {1}: postingStats.total 210 - #: src/components/account-info.jsx:949 214 + #: src/components/account-info.jsx:914 211 215 msgid "{0, plural, one {Last 1 post in the past year(s)} other {Last {1} posts in the past year(s)}}" 212 - msgstr "" 216 + msgstr "{0, plural, one {Last 1 post in the past year(s)} other {Last {1} posts in the past year(s)}}" 213 217 214 - #: src/components/account-info.jsx:974 218 + #: src/components/account-info.jsx:939 215 219 #: src/pages/catchup.jsx:70 216 220 msgid "Original" 217 221 msgstr "" 218 222 219 - #: src/components/account-info.jsx:978 223 + #: src/components/account-info.jsx:943 220 224 #: src/components/status.jsx:2328 221 225 #: src/pages/catchup.jsx:71 222 226 #: src/pages/catchup.jsx:1448 ··· 226 230 msgid "Replies" 227 231 msgstr "" 228 232 229 - #: src/components/account-info.jsx:982 233 + #: src/components/account-info.jsx:947 230 234 #: src/pages/catchup.jsx:72 231 235 #: src/pages/catchup.jsx:1450 232 236 #: src/pages/catchup.jsx:2073 ··· 234 238 msgid "Boosts" 235 239 msgstr "" 236 240 237 - #: src/components/account-info.jsx:988 241 + #: src/components/account-info.jsx:953 238 242 msgid "Post stats unavailable." 239 - msgstr "" 243 + msgstr "Post stats unavailable." 240 244 241 - #: src/components/account-info.jsx:1019 245 + #: src/components/account-info.jsx:984 242 246 msgid "View post stats" 243 - msgstr "" 244 - 245 - #. placeholder {0}: niceDateTime(lastStatusAt, { hideTime: true, }) 246 - #: src/components/account-info.jsx:1216 247 - msgid "Last post: <0>{0}</0>" 248 - msgstr "" 249 - 250 - #: src/components/account-info.jsx:1230 251 - msgid "Muted" 252 - msgstr "" 253 - 254 - #: src/components/account-info.jsx:1235 255 - msgid "Blocked" 256 - msgstr "" 257 - 258 - #: src/components/account-info.jsx:1244 259 - msgid "Private note" 260 - msgstr "Private note" 261 - 262 - #: src/components/account-info.jsx:1301 263 - msgid "Mention <0>@{username}</0>" 264 - msgstr "" 265 - 266 - #: src/components/account-info.jsx:1313 267 - msgid "Translate bio" 268 - msgstr "" 269 - 270 - #: src/components/account-info.jsx:1324 271 - msgid "Edit private note" 272 - msgstr "Edit private note" 273 - 274 - #: src/components/account-info.jsx:1324 275 - msgid "Add private note" 276 - msgstr "Add private note" 277 - 278 - #: src/components/account-info.jsx:1344 279 - msgid "Notifications enabled for @{username}'s posts." 280 - msgstr "Notifications enabled for @{username}'s posts." 281 - 282 - #: src/components/account-info.jsx:1345 283 - msgid " Notifications disabled for @{username}'s posts." 284 - msgstr " Notifications disabled for @{username}'s posts." 285 - 286 - #: src/components/account-info.jsx:1357 287 - msgid "Disable notifications" 288 - msgstr "Disable notifications" 289 - 290 - #: src/components/account-info.jsx:1358 291 - msgid "Enable notifications" 292 - msgstr "Enable notifications" 293 - 294 - #: src/components/account-info.jsx:1375 295 - msgid "Boosts from @{username} enabled." 296 - msgstr "Boosts from @{username} enabled." 297 - 298 - #: src/components/account-info.jsx:1376 299 - msgid "Boosts from @{username} disabled." 300 - msgstr "Boosts from @{username} disabled." 301 - 302 - #: src/components/account-info.jsx:1387 303 - msgid "Disable boosts" 304 - msgstr "Disable boosts" 305 - 306 - #: src/components/account-info.jsx:1387 307 - msgid "Enable boosts" 308 - msgstr "Enable boosts" 309 - 310 - #: src/components/account-info.jsx:1406 311 - msgid "@{username} is no longer featured on your profile." 312 - msgstr "@{username} is no longer featured on your profile." 313 - 314 - #: src/components/account-info.jsx:1416 315 - msgid "@{username} is now featured on your profile." 316 - msgstr "@{username} is now featured on your profile." 317 - 318 - #: src/components/account-info.jsx:1424 319 - msgid "Unable to unfeature @{username} on your profile." 320 - msgstr "Unable to unfeature @{username} on your profile." 321 - 322 - #: src/components/account-info.jsx:1428 323 - msgid "Unable to feature @{username} on your profile." 324 - msgstr "Unable to feature @{username} on your profile." 325 - 326 - #: src/components/account-info.jsx:1437 327 - msgid "Don't feature on profile" 328 - msgstr "Don't feature on profile" 329 - 330 - #: src/components/account-info.jsx:1438 331 - #: src/pages/hashtag.jsx:333 332 - msgid "Feature on profile" 333 - msgstr "" 334 - 335 - #: src/components/account-info.jsx:1447 336 - msgid "Show featured profiles" 337 - msgstr "Show featured profiles" 338 - 339 - #: src/components/account-info.jsx:1462 340 - #: src/components/account-info.jsx:1472 341 - #: src/components/account-info.jsx:2083 342 - msgid "Add/Remove from Lists" 343 - msgstr "" 344 - 345 - #: src/components/account-info.jsx:1522 346 - #: src/components/status.jsx:1193 347 - msgid "Link copied" 348 - msgstr "" 349 - 350 - #: src/components/account-info.jsx:1525 351 - #: src/components/status.jsx:1196 352 - msgid "Unable to copy link" 353 - msgstr "" 354 - 355 - #: src/components/account-info.jsx:1531 356 - #: src/components/post-embed-modal.jsx:232 357 - #: src/components/shortcuts-settings.jsx:1059 358 - #: src/components/status.jsx:1202 359 - msgid "Copy" 360 - msgstr "" 361 - 362 - #: src/components/account-info.jsx:1546 363 - #: src/components/shortcuts-settings.jsx:1077 364 - #: src/components/status.jsx:1218 365 - msgid "Sharing doesn't seem to work." 366 - msgstr "" 367 - 368 - #: src/components/account-info.jsx:1552 369 - #: src/components/status.jsx:1224 370 - msgid "Share…" 371 - msgstr "" 372 - 373 - #: src/components/account-info.jsx:1572 374 - msgid "Unmuted @{username}" 375 - msgstr "Unmuted @{username}" 376 - 377 - #: src/components/account-info.jsx:1584 378 - msgid "Unmute <0>@{username}</0>" 379 - msgstr "" 380 - 381 - #: src/components/account-info.jsx:1600 382 - msgid "Mute <0>@{username}</0>…" 383 - msgstr "" 384 - 385 - #. placeholder {0}: typeof MUTE_DURATIONS_LABELS[duration] === 'function' ? MUTE_DURATIONS_LABELS[duration]() : _(MUTE_DURATIONS_LABELS[duration]) 386 - #: src/components/account-info.jsx:1632 387 - msgid "Muted @{username} for {0}" 388 - msgstr "Muted @{username} for {0}" 389 - 390 - #: src/components/account-info.jsx:1644 391 - msgid "Unable to mute @{username}" 392 - msgstr "Unable to mute @{username}" 393 - 394 - #: src/components/account-info.jsx:1665 395 - msgid "Remove <0>@{username}</0> from followers?" 396 - msgstr "" 397 - 398 - #: src/components/account-info.jsx:1685 399 - msgid "@{username} removed from followers" 400 - msgstr "@{username} removed from followers" 401 - 402 - #: src/components/account-info.jsx:1697 403 - msgid "Remove follower…" 404 - msgstr "" 405 - 406 - #: src/components/account-info.jsx:1708 407 - msgid "Block <0>@{username}</0>?" 408 - msgstr "" 409 - 410 - #: src/components/account-info.jsx:1732 411 - msgid "Unblocked @{username}" 412 - msgstr "Unblocked @{username}" 413 - 414 - #: src/components/account-info.jsx:1740 415 - msgid "Blocked @{username}" 416 - msgstr "Blocked @{username}" 417 - 418 - #: src/components/account-info.jsx:1748 419 - msgid "Unable to unblock @{username}" 420 - msgstr "Unable to unblock @{username}" 421 - 422 - #: src/components/account-info.jsx:1750 423 - msgid "Unable to block @{username}" 424 - msgstr "Unable to block @{username}" 425 - 426 - #: src/components/account-info.jsx:1760 427 - msgid "Unblock <0>@{username}</0>" 428 - msgstr "" 429 - 430 - #: src/components/account-info.jsx:1769 431 - msgid "Block <0>@{username}</0>…" 432 - msgstr "" 433 - 434 - #: src/components/account-info.jsx:1786 435 - msgid "Report <0>@{username}</0>…" 436 - msgstr "" 437 - 438 - #: src/components/account-info.jsx:1842 439 - msgid "Withdraw follow request?" 440 - msgstr "Withdraw follow request?" 441 - 442 - #. placeholder {0}: info.acct || info.username 443 - #: src/components/account-info.jsx:1843 444 - msgid "Unfollow @{0}?" 445 - msgstr "Unfollow @{0}?" 446 - 447 - #: src/components/account-info.jsx:1906 448 - msgid "Unfollow…" 449 - msgstr "" 450 - 451 - #: src/components/account-info.jsx:1915 452 - msgid "Withdraw…" 453 - msgstr "" 454 - 455 - #: src/components/account-info.jsx:1922 456 - #: src/components/account-info.jsx:1926 457 - #: src/pages/hashtag.jsx:265 458 - msgid "Follow" 459 - msgstr "" 247 + msgstr "View post stats" 460 248 461 - #: src/components/account-info.jsx:2023 462 - #: src/components/account-info.jsx:2078 463 - #: src/components/account-info.jsx:2212 464 - #: src/components/account-info.jsx:2332 465 249 #: src/components/account-sheet.jsx:38 250 + #: src/components/add-remove-lists-sheet.jsx:45 466 251 #: src/components/compose.jsx:779 467 252 #: src/components/custom-emojis-modal.jsx:234 468 253 #: src/components/drafts.jsx:57 254 + #: src/components/edit-profile-sheet.jsx:87 469 255 #: src/components/embed-modal.jsx:13 470 256 #: src/components/generic-accounts.jsx:151 471 257 #: src/components/gif-picker-modal.jsx:71 ··· 477 263 #: src/components/mention-modal.jsx:162 478 264 #: src/components/notification-service.jsx:157 479 265 #: src/components/post-embed-modal.jsx:196 266 + #: src/components/private-note-sheet.jsx:36 480 267 #: src/components/report-modal.jsx:118 481 268 #: src/components/shortcuts-settings.jsx:230 482 269 #: src/components/shortcuts-settings.jsx:583 483 270 #: src/components/shortcuts-settings.jsx:783 484 271 #: src/components/status.jsx:2742 485 272 #: src/components/status.jsx:2954 273 + #: src/components/translated-bio-sheet.jsx:21 486 274 #: src/pages/accounts.jsx:45 487 275 #: src/pages/catchup.jsx:1584 488 276 #: src/pages/filters.jsx:225 ··· 494 282 msgid "Close" 495 283 msgstr "" 496 284 497 - #: src/components/account-info.jsx:2028 498 - msgid "Translated Bio" 285 + #: src/components/add-remove-lists-sheet.jsx:50 286 + #: src/components/related-actions.jsx:432 287 + #: src/components/related-actions.jsx:442 288 + msgid "Add/Remove from Lists" 499 289 msgstr "" 500 290 501 - #: src/components/account-info.jsx:2123 291 + #: src/components/add-remove-lists-sheet.jsx:90 502 292 msgid "Unable to remove from list." 503 293 msgstr "Unable to remove from list." 504 294 505 - #: src/components/account-info.jsx:2124 295 + #: src/components/add-remove-lists-sheet.jsx:91 506 296 msgid "Unable to add to list." 507 297 msgstr "Unable to add to list." 508 298 509 - #: src/components/account-info.jsx:2143 299 + #: src/components/add-remove-lists-sheet.jsx:110 510 300 #: src/pages/lists.jsx:131 511 301 msgid "Unable to load lists." 512 302 msgstr "" 513 303 514 - #: src/components/account-info.jsx:2147 304 + #: src/components/add-remove-lists-sheet.jsx:114 515 305 msgid "No lists." 516 306 msgstr "" 517 307 518 - #: src/components/account-info.jsx:2158 308 + #: src/components/add-remove-lists-sheet.jsx:125 519 309 #: src/components/list-add-edit.jsx:41 520 310 #: src/pages/lists.jsx:62 521 311 msgid "New list" 522 312 msgstr "" 523 - 524 - #. placeholder {0}: account?.username || account?.acct 525 - #: src/components/account-info.jsx:2217 526 - msgid "Private note about <0>@{0}</0>" 527 - msgstr "" 528 - 529 - #: src/components/account-info.jsx:2247 530 - msgid "Unable to update private note." 531 - msgstr "Unable to update private note." 532 - 533 - #: src/components/account-info.jsx:2270 534 - #: src/components/account-info.jsx:2568 535 - msgid "Cancel" 536 - msgstr "" 537 - 538 - #: src/components/account-info.jsx:2275 539 - msgid "Save & close" 540 - msgstr "" 541 - 542 - #: src/components/account-info.jsx:2392 543 - msgid "Unable to update profile." 544 - msgstr "Unable to update profile." 545 - 546 - #: src/components/account-info.jsx:2399 547 - msgid "Header picture" 548 - msgstr "Header picture" 549 - 550 - #: src/components/account-info.jsx:2451 551 - msgid "Profile picture" 552 - msgstr "Profile picture" 553 - 554 - #: src/components/account-info.jsx:2503 555 - #: src/components/list-add-edit.jsx:106 556 - msgid "Name" 557 - msgstr "" 558 - 559 - #: src/components/account-info.jsx:2516 560 - msgid "Bio" 561 - msgstr "" 562 - 563 - #: src/components/account-info.jsx:2529 564 - msgid "Extra fields" 565 - msgstr "" 566 - 567 - #: src/components/account-info.jsx:2535 568 - msgid "Label" 569 - msgstr "" 570 - 571 - #: src/components/account-info.jsx:2538 572 - msgid "Content" 573 - msgstr "" 574 - 575 - #: src/components/account-info.jsx:2571 576 - #: src/components/list-add-edit.jsx:152 577 - #: src/components/shortcuts-settings.jsx:715 578 - #: src/pages/filters.jsx:570 579 - #: src/pages/notifications.jsx:1009 580 - msgid "Save" 581 - msgstr "" 582 - 583 - #: src/components/account-info.jsx:2625 584 - msgid "username" 585 - msgstr "" 586 - 587 - #: src/components/account-info.jsx:2629 588 - msgid "server domain name" 589 - msgstr "" 590 - 591 - #. placeholder {0}: info.username 592 - #: src/components/account-info.jsx:2695 593 - msgid "Profiles featured by @{0}" 594 - msgstr "Profiles featured by @{0}" 595 - 596 - #: src/components/account-info.jsx:2721 597 - msgid "No featured profiles." 598 - msgstr "No featured profiles." 599 313 600 314 #: src/components/background-service.jsx:158 601 315 msgid "Cloak mode disabled" ··· 866 580 msgstr "Failed to download GIF" 867 581 868 582 #. placeholder {0}: i18n.number(emojis.length - max) 869 - #: src/components/custom-emojis-list.jsx:30 870 583 #: src/components/custom-emojis-modal.jsx:98 871 584 msgid "{0} more…" 872 585 msgstr "" ··· 946 659 msgid "Media" 947 660 msgstr "" 948 661 662 + #: src/components/edit-profile-sheet.jsx:147 663 + msgid "Unable to update profile." 664 + msgstr "Unable to update profile." 665 + 666 + #: src/components/edit-profile-sheet.jsx:154 667 + msgid "Header picture" 668 + msgstr "Header picture" 669 + 670 + #: src/components/edit-profile-sheet.jsx:206 671 + msgid "Profile picture" 672 + msgstr "Profile picture" 673 + 674 + #: src/components/edit-profile-sheet.jsx:258 675 + #: src/components/list-add-edit.jsx:106 676 + msgid "Name" 677 + msgstr "" 678 + 679 + #: src/components/edit-profile-sheet.jsx:271 680 + msgid "Bio" 681 + msgstr "" 682 + 683 + #: src/components/edit-profile-sheet.jsx:284 684 + msgid "Extra fields" 685 + msgstr "" 686 + 687 + #: src/components/edit-profile-sheet.jsx:290 688 + msgid "Label" 689 + msgstr "" 690 + 691 + #: src/components/edit-profile-sheet.jsx:293 692 + msgid "Content" 693 + msgstr "" 694 + 695 + #: src/components/edit-profile-sheet.jsx:323 696 + #: src/components/private-note-sheet.jsx:94 697 + msgid "Cancel" 698 + msgstr "" 699 + 700 + #: src/components/edit-profile-sheet.jsx:326 701 + #: src/components/list-add-edit.jsx:152 702 + #: src/components/shortcuts-settings.jsx:715 703 + #: src/pages/filters.jsx:570 704 + #: src/pages/notifications.jsx:1009 705 + msgid "Save" 706 + msgstr "" 707 + 949 708 #: src/components/embed-modal.jsx:18 950 709 msgid "Open in new window" 951 710 msgstr "" 711 + 712 + #. placeholder {0}: info.username 713 + #: src/components/endorsements.jsx:72 714 + msgid "Profiles featured by @{0}" 715 + msgstr "Profiles featured by @{0}" 716 + 717 + #: src/components/endorsements.jsx:98 718 + msgid "No featured profiles." 719 + msgstr "No featured profiles." 952 720 953 721 #: src/components/follow-request-buttons.jsx:43 954 722 #: src/pages/notifications.jsx:993 ··· 1807 1575 msgid "Unable to copy HTML code" 1808 1576 msgstr "" 1809 1577 1578 + #: src/components/post-embed-modal.jsx:232 1579 + #: src/components/shortcuts-settings.jsx:1059 1580 + #: src/components/status.jsx:1202 1581 + msgid "Copy" 1582 + msgstr "" 1583 + 1810 1584 #: src/components/post-embed-modal.jsx:238 1811 1585 msgid "Media attachments:" 1812 1586 msgstr "" ··· 1852 1626 msgid "Note: This preview is lightly styled." 1853 1627 msgstr "" 1854 1628 1629 + #. placeholder {0}: account?.username || account?.acct 1630 + #: src/components/private-note-sheet.jsx:41 1631 + msgid "Private note about <0>@{0}</0>" 1632 + msgstr "" 1633 + 1634 + #: src/components/private-note-sheet.jsx:71 1635 + msgid "Unable to update private note." 1636 + msgstr "Unable to update private note." 1637 + 1638 + #: src/components/private-note-sheet.jsx:99 1639 + msgid "Save & close" 1640 + msgstr "" 1641 + 1855 1642 #: src/components/recent-searches.jsx:27 1856 1643 msgid "Cleared recent searches" 1857 1644 msgstr "Cleared recent searches" ··· 1870 1657 msgid "Clear" 1871 1658 msgstr "" 1872 1659 1660 + #: src/components/related-actions.jsx:38 1661 + msgid "Forever" 1662 + msgstr "" 1663 + 1664 + #. placeholder {0}: niceDateTime(lastStatusAt, { hideTime: true, }) 1665 + #: src/components/related-actions.jsx:186 1666 + msgid "Last post: <0>{0}</0>" 1667 + msgstr "" 1668 + 1669 + #: src/components/related-actions.jsx:200 1670 + msgid "Muted" 1671 + msgstr "" 1672 + 1673 + #: src/components/related-actions.jsx:205 1674 + msgid "Blocked" 1675 + msgstr "" 1676 + 1677 + #: src/components/related-actions.jsx:214 1678 + msgid "Private note" 1679 + msgstr "Private note" 1680 + 1681 + #: src/components/related-actions.jsx:271 1682 + msgid "Mention <0>@{username}</0>" 1683 + msgstr "" 1684 + 1685 + #: src/components/related-actions.jsx:283 1686 + msgid "Translate bio" 1687 + msgstr "" 1688 + 1689 + #: src/components/related-actions.jsx:294 1690 + msgid "Edit private note" 1691 + msgstr "Edit private note" 1692 + 1693 + #: src/components/related-actions.jsx:294 1694 + msgid "Add private note" 1695 + msgstr "Add private note" 1696 + 1697 + #: src/components/related-actions.jsx:314 1698 + msgid "Notifications enabled for @{username}'s posts." 1699 + msgstr "Notifications enabled for @{username}'s posts." 1700 + 1701 + #: src/components/related-actions.jsx:315 1702 + msgid " Notifications disabled for @{username}'s posts." 1703 + msgstr " Notifications disabled for @{username}'s posts." 1704 + 1705 + #: src/components/related-actions.jsx:327 1706 + msgid "Disable notifications" 1707 + msgstr "Disable notifications" 1708 + 1709 + #: src/components/related-actions.jsx:328 1710 + msgid "Enable notifications" 1711 + msgstr "Enable notifications" 1712 + 1713 + #: src/components/related-actions.jsx:345 1714 + msgid "Boosts from @{username} enabled." 1715 + msgstr "Boosts from @{username} enabled." 1716 + 1717 + #: src/components/related-actions.jsx:346 1718 + msgid "Boosts from @{username} disabled." 1719 + msgstr "Boosts from @{username} disabled." 1720 + 1721 + #: src/components/related-actions.jsx:357 1722 + msgid "Disable boosts" 1723 + msgstr "Disable boosts" 1724 + 1725 + #: src/components/related-actions.jsx:357 1726 + msgid "Enable boosts" 1727 + msgstr "Enable boosts" 1728 + 1729 + #: src/components/related-actions.jsx:376 1730 + msgid "@{username} is no longer featured on your profile." 1731 + msgstr "@{username} is no longer featured on your profile." 1732 + 1733 + #: src/components/related-actions.jsx:386 1734 + msgid "@{username} is now featured on your profile." 1735 + msgstr "@{username} is now featured on your profile." 1736 + 1737 + #: src/components/related-actions.jsx:394 1738 + msgid "Unable to unfeature @{username} on your profile." 1739 + msgstr "Unable to unfeature @{username} on your profile." 1740 + 1741 + #: src/components/related-actions.jsx:398 1742 + msgid "Unable to feature @{username} on your profile." 1743 + msgstr "Unable to feature @{username} on your profile." 1744 + 1745 + #: src/components/related-actions.jsx:407 1746 + msgid "Don't feature on profile" 1747 + msgstr "Don't feature on profile" 1748 + 1749 + #: src/components/related-actions.jsx:408 1750 + #: src/pages/hashtag.jsx:333 1751 + msgid "Feature on profile" 1752 + msgstr "" 1753 + 1754 + #: src/components/related-actions.jsx:417 1755 + msgid "Show featured profiles" 1756 + msgstr "Show featured profiles" 1757 + 1758 + #: src/components/related-actions.jsx:495 1759 + msgid "Mute <0>@{username}</0>…" 1760 + msgstr "" 1761 + 1762 + #. placeholder {0}: typeof MUTE_DURATIONS_LABELS[duration] === 'function' ? MUTE_DURATIONS_LABELS[duration]() : _(MUTE_DURATIONS_LABELS[duration]) 1763 + #: src/components/related-actions.jsx:527 1764 + msgid "Muted @{username} for {0}" 1765 + msgstr "Muted @{username} for {0}" 1766 + 1767 + #: src/components/related-actions.jsx:539 1768 + msgid "Unable to mute @{username}" 1769 + msgstr "Unable to mute @{username}" 1770 + 1771 + #: src/components/related-actions.jsx:560 1772 + msgid "Remove <0>@{username}</0> from followers?" 1773 + msgstr "" 1774 + 1775 + #: src/components/related-actions.jsx:580 1776 + msgid "@{username} removed from followers" 1777 + msgstr "@{username} removed from followers" 1778 + 1779 + #: src/components/related-actions.jsx:592 1780 + msgid "Remove follower…" 1781 + msgstr "" 1782 + 1783 + #: src/components/related-actions.jsx:603 1784 + msgid "Block <0>@{username}</0>?" 1785 + msgstr "" 1786 + 1787 + #: src/components/related-actions.jsx:627 1788 + msgid "Unblocked @{username}" 1789 + msgstr "Unblocked @{username}" 1790 + 1791 + #: src/components/related-actions.jsx:635 1792 + msgid "Blocked @{username}" 1793 + msgstr "Blocked @{username}" 1794 + 1795 + #: src/components/related-actions.jsx:643 1796 + msgid "Unable to unblock @{username}" 1797 + msgstr "Unable to unblock @{username}" 1798 + 1799 + #: src/components/related-actions.jsx:645 1800 + msgid "Unable to block @{username}" 1801 + msgstr "Unable to block @{username}" 1802 + 1803 + #: src/components/related-actions.jsx:655 1804 + msgid "Unblock <0>@{username}</0>" 1805 + msgstr "" 1806 + 1807 + #: src/components/related-actions.jsx:664 1808 + msgid "Block <0>@{username}</0>…" 1809 + msgstr "" 1810 + 1811 + #: src/components/related-actions.jsx:681 1812 + msgid "Report <0>@{username}</0>…" 1813 + msgstr "" 1814 + 1815 + #: src/components/related-actions.jsx:737 1816 + msgid "Withdraw follow request?" 1817 + msgstr "Withdraw follow request?" 1818 + 1819 + #. placeholder {0}: info.acct || info.username 1820 + #: src/components/related-actions.jsx:738 1821 + msgid "Unfollow @{0}?" 1822 + msgstr "Unfollow @{0}?" 1823 + 1824 + #: src/components/related-actions.jsx:801 1825 + msgid "Unfollow…" 1826 + msgstr "" 1827 + 1828 + #: src/components/related-actions.jsx:810 1829 + msgid "Withdraw…" 1830 + msgstr "" 1831 + 1832 + #: src/components/related-actions.jsx:817 1833 + #: src/components/related-actions.jsx:821 1834 + #: src/pages/hashtag.jsx:265 1835 + msgid "Follow" 1836 + msgstr "" 1837 + 1873 1838 #. Relative time in seconds, as short as possible 1874 1839 #. placeholder {0}: seconds < 1 ? 1 : Math.floor(seconds) 1875 1840 #: src/components/relative-time.jsx:45 ··· 2282 2247 msgid "Unable to copy shortcut settings" 2283 2248 msgstr "" 2284 2249 2250 + #: src/components/shortcuts-settings.jsx:1077 2251 + #: src/components/status.jsx:1218 2252 + msgid "Sharing doesn't seem to work." 2253 + msgstr "" 2254 + 2285 2255 #: src/components/shortcuts-settings.jsx:1083 2286 2256 msgid "Share" 2287 2257 msgstr "" ··· 2422 2392 2423 2393 #: src/components/status.jsx:1170 2424 2394 msgid "Edited: {editedDateText}" 2395 + msgstr "" 2396 + 2397 + #: src/components/status.jsx:1193 2398 + msgid "Link copied" 2399 + msgstr "" 2400 + 2401 + #: src/components/status.jsx:1196 2402 + msgid "Unable to copy link" 2403 + msgstr "" 2404 + 2405 + #: src/components/status.jsx:1224 2406 + msgid "Share…" 2425 2407 msgstr "" 2426 2408 2427 2409 #: src/components/status.jsx:1251 ··· 2631 2613 #. placeholder {0}: filterInfo.titlesStr 2632 2614 #: src/components/timeline.jsx:999 2633 2615 msgid "<0>Filtered</0>: <1>{0}</1>" 2616 + msgstr "" 2617 + 2618 + #: src/components/translated-bio-sheet.jsx:26 2619 + msgid "Translated Bio" 2634 2620 msgstr "" 2635 2621 2636 2622 #: src/components/translation-block.jsx:196